4 Commits

Author SHA1 Message Date
yohlo
81329e4354 fix refresh issue 2025-09-24 12:20:36 -05:00
yohlo
36f3bb77d4 updates 2025-09-24 11:02:56 -05:00
yohlo
6760ea46f9 update query integration 2025-09-24 08:04:09 -05:00
yohlo
e4164cbc71 attempted upgrade 2025-09-24 00:13:41 -05:00
33 changed files with 1485 additions and 1356 deletions

1
.gitignore vendored
View File

@@ -20,3 +20,4 @@ yarn.lock
/scripts/ /scripts/
/pb_data/ /pb_data/
/.tanstack/ /.tanstack/
/dist/

View File

@@ -6,7 +6,7 @@
"scripts": { "scripts": {
"dev": "vite dev --host 0.0.0.0", "dev": "vite dev --host 0.0.0.0",
"build": "vite build && tsc --noEmit", "build": "vite build && tsc --noEmit",
"start": "vite start" "start": "vite start --host 0.0.0.0"
}, },
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
@@ -24,12 +24,13 @@
"@tanstack/react-router": "^1.130.12", "@tanstack/react-router": "^1.130.12",
"@tanstack/react-router-devtools": "^1.130.13", "@tanstack/react-router-devtools": "^1.130.13",
"@tanstack/react-router-with-query": "^1.130.12", "@tanstack/react-router-with-query": "^1.130.12",
"@tanstack/react-start": "^1.130.15", "@tanstack/react-start": "^1.132.2",
"@tanstack/react-virtual": "^3.13.12", "@tanstack/react-virtual": "^3.13.12",
"@tiptap/pm": "^3.4.3", "@tiptap/pm": "^3.4.3",
"@tiptap/react": "^3.4.3", "@tiptap/react": "^3.4.3",
"@tiptap/starter-kit": "^3.4.3", "@tiptap/starter-kit": "^3.4.3",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"dotenv": "^17.2.2",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"ioredis": "^5.7.0", "ioredis": "^5.7.0",
@@ -51,6 +52,8 @@
"zustand": "^5.0.7" "zustand": "^5.0.7"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/react-router-ssr-query": "^1.132.2",
"@tanstack/router-plugin": "^1.132.2",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
@@ -63,7 +66,7 @@
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^6.3.5", "vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }

View File

@@ -8,8 +8,6 @@
// You should NOT make any changes in this file as it will be overwritten. // You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { createServerRootRoute } from '@tanstack/react-start/server'
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as RefreshSessionRouteImport } from './routes/refresh-session' import { Route as RefreshSessionRouteImport } from './routes/refresh-session'
import { Route as LogoutRouteImport } from './routes/logout' import { Route as LogoutRouteImport } from './routes/logout'
@@ -21,6 +19,16 @@ import { Route as AuthedSettingsRouteImport } from './routes/_authed/settings'
import { Route as AuthedAdminRouteImport } from './routes/_authed/admin' import { Route as AuthedAdminRouteImport } from './routes/_authed/admin'
import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index' import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index'
import { Route as AuthedAdminIndexRouteImport } from './routes/_authed/admin/index' import { Route as AuthedAdminIndexRouteImport } from './routes/_authed/admin/index'
import { Route as ApiTournamentsUploadLogoRouteImport } from './routes/api/tournaments/upload-logo'
import { Route as ApiTeamsUploadLogoRouteImport } from './routes/api/teams/upload-logo'
import { Route as ApiSpotifyTokenRouteImport } from './routes/api/spotify/token'
import { Route as ApiSpotifySearchRouteImport } from './routes/api/spotify/search'
import { Route as ApiSpotifyResumeRouteImport } from './routes/api/spotify/resume'
import { Route as ApiSpotifyPlaybackRouteImport } from './routes/api/spotify/playback'
import { Route as ApiSpotifyCaptureRouteImport } from './routes/api/spotify/capture'
import { Route as ApiSpotifyCallbackRouteImport } from './routes/api/spotify/callback'
import { Route as ApiEventsSplatRouteImport } from './routes/api/events.$'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth.$'
import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_authed/tournaments/$tournamentId' import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_authed/tournaments/$tournamentId'
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId' import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId' import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
@@ -28,21 +36,9 @@ import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/p
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index' import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket' import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index' import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id' import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams' import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
import { ServerRoute as ApiTeamsUploadLogoServerRouteImport } from './routes/api/teams/upload-logo'
import { ServerRoute as ApiSpotifyTokenServerRouteImport } from './routes/api/spotify/token'
import { ServerRoute as ApiSpotifySearchServerRouteImport } from './routes/api/spotify/search'
import { ServerRoute as ApiSpotifyResumeServerRouteImport } from './routes/api/spotify/resume'
import { ServerRoute as ApiSpotifyPlaybackServerRouteImport } from './routes/api/spotify/playback'
import { ServerRoute as ApiSpotifyCaptureServerRouteImport } from './routes/api/spotify/capture'
import { ServerRoute as ApiSpotifyCallbackServerRouteImport } from './routes/api/spotify/callback'
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
import { ServerRoute as ApiFilesCollectionRecordIdFileServerRouteImport } from './routes/api/files/$collection/$recordId/$file'
const rootServerRouteImport = createServerRootRoute()
const RefreshSessionRoute = RefreshSessionRouteImport.update({ const RefreshSessionRoute = RefreshSessionRouteImport.update({
id: '/refresh-session', id: '/refresh-session',
@@ -93,6 +89,57 @@ const AuthedAdminIndexRoute = AuthedAdminIndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => AuthedAdminRoute, getParentRoute: () => AuthedAdminRoute,
} as any) } as any)
const ApiTournamentsUploadLogoRoute =
ApiTournamentsUploadLogoRouteImport.update({
id: '/api/tournaments/upload-logo',
path: '/api/tournaments/upload-logo',
getParentRoute: () => rootRouteImport,
} as any)
const ApiTeamsUploadLogoRoute = ApiTeamsUploadLogoRouteImport.update({
id: '/api/teams/upload-logo',
path: '/api/teams/upload-logo',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSpotifyTokenRoute = ApiSpotifyTokenRouteImport.update({
id: '/api/spotify/token',
path: '/api/spotify/token',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSpotifySearchRoute = ApiSpotifySearchRouteImport.update({
id: '/api/spotify/search',
path: '/api/spotify/search',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSpotifyResumeRoute = ApiSpotifyResumeRouteImport.update({
id: '/api/spotify/resume',
path: '/api/spotify/resume',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSpotifyPlaybackRoute = ApiSpotifyPlaybackRouteImport.update({
id: '/api/spotify/playback',
path: '/api/spotify/playback',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSpotifyCaptureRoute = ApiSpotifyCaptureRouteImport.update({
id: '/api/spotify/capture',
path: '/api/spotify/capture',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSpotifyCallbackRoute = ApiSpotifyCallbackRouteImport.update({
id: '/api/spotify/callback',
path: '/api/spotify/callback',
getParentRoute: () => rootRouteImport,
} as any)
const ApiEventsSplatRoute = ApiEventsSplatRouteImport.update({
id: '/api/events/$',
path: '/api/events/$',
getParentRoute: () => rootRouteImport,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
id: '/api/auth/$',
path: '/api/auth/$',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedTournamentsTournamentIdRoute = const AuthedTournamentsTournamentIdRoute =
AuthedTournamentsTournamentIdRouteImport.update({ AuthedTournamentsTournamentIdRouteImport.update({
id: '/tournaments/$tournamentId', id: '/tournaments/$tournamentId',
@@ -132,6 +179,12 @@ const AuthedAdminTournamentsIdIndexRoute =
path: '/tournaments/$id/', path: '/tournaments/$id/',
getParentRoute: () => AuthedAdminRoute, getParentRoute: () => AuthedAdminRoute,
} as any) } as any)
const ApiFilesCollectionRecordIdFileRoute =
ApiFilesCollectionRecordIdFileRouteImport.update({
id: '/api/files/$collection/$recordId/$file',
path: '/api/files/$collection/$recordId/$file',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedAdminTournamentsRunIdRoute = const AuthedAdminTournamentsRunIdRoute =
AuthedAdminTournamentsRunIdRouteImport.update({ AuthedAdminTournamentsRunIdRouteImport.update({
id: '/tournaments/run/$id', id: '/tournaments/run/$id',
@@ -144,66 +197,6 @@ const AuthedAdminTournamentsIdTeamsRoute =
path: '/tournaments/$id/teams', path: '/tournaments/$id/teams',
getParentRoute: () => AuthedAdminRoute, getParentRoute: () => AuthedAdminRoute,
} as any) } as any)
const ApiTournamentsUploadLogoServerRoute =
ApiTournamentsUploadLogoServerRouteImport.update({
id: '/api/tournaments/upload-logo',
path: '/api/tournaments/upload-logo',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiTeamsUploadLogoServerRoute =
ApiTeamsUploadLogoServerRouteImport.update({
id: '/api/teams/upload-logo',
path: '/api/teams/upload-logo',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiSpotifyTokenServerRoute = ApiSpotifyTokenServerRouteImport.update({
id: '/api/spotify/token',
path: '/api/spotify/token',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiSpotifySearchServerRoute = ApiSpotifySearchServerRouteImport.update({
id: '/api/spotify/search',
path: '/api/spotify/search',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiSpotifyResumeServerRoute = ApiSpotifyResumeServerRouteImport.update({
id: '/api/spotify/resume',
path: '/api/spotify/resume',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiSpotifyPlaybackServerRoute =
ApiSpotifyPlaybackServerRouteImport.update({
id: '/api/spotify/playback',
path: '/api/spotify/playback',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiSpotifyCaptureServerRoute = ApiSpotifyCaptureServerRouteImport.update({
id: '/api/spotify/capture',
path: '/api/spotify/capture',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiSpotifyCallbackServerRoute =
ApiSpotifyCallbackServerRouteImport.update({
id: '/api/spotify/callback',
path: '/api/spotify/callback',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({
id: '/api/events/$',
path: '/api/events/$',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiAuthSplatServerRoute = ApiAuthSplatServerRouteImport.update({
id: '/api/auth/$',
path: '/api/auth/$',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiFilesCollectionRecordIdFileServerRoute =
ApiFilesCollectionRecordIdFileServerRouteImport.update({
id: '/api/files/$collection/$recordId/$file',
path: '/api/files/$collection/$recordId/$file',
getParentRoute: () => rootServerRouteImport,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/login': typeof LoginRoute '/login': typeof LoginRoute
@@ -217,12 +210,23 @@ export interface FileRoutesByFullPath {
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute '/profile/$playerId': typeof AuthedProfilePlayerIdRoute
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute '/teams/$teamId': typeof AuthedTeamsTeamIdRoute
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute '/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/events/$': typeof ApiEventsSplatRoute
'/api/spotify/callback': typeof ApiSpotifyCallbackRoute
'/api/spotify/capture': typeof ApiSpotifyCaptureRoute
'/api/spotify/playback': typeof ApiSpotifyPlaybackRoute
'/api/spotify/resume': typeof ApiSpotifyResumeRoute
'/api/spotify/search': typeof ApiSpotifySearchRoute
'/api/spotify/token': typeof ApiSpotifyTokenRoute
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
'/admin/': typeof AuthedAdminIndexRoute '/admin/': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute '/tournaments': typeof AuthedTournamentsIndexRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
@@ -236,12 +240,23 @@ export interface FileRoutesByTo {
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute '/profile/$playerId': typeof AuthedProfilePlayerIdRoute
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute '/teams/$teamId': typeof AuthedTeamsTeamIdRoute
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute '/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/events/$': typeof ApiEventsSplatRoute
'/api/spotify/callback': typeof ApiSpotifyCallbackRoute
'/api/spotify/capture': typeof ApiSpotifyCaptureRoute
'/api/spotify/playback': typeof ApiSpotifyPlaybackRoute
'/api/spotify/resume': typeof ApiSpotifyResumeRoute
'/api/spotify/search': typeof ApiSpotifySearchRoute
'/api/spotify/token': typeof ApiSpotifyTokenRoute
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
'/admin': typeof AuthedAdminIndexRoute '/admin': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute '/tournaments': typeof AuthedTournamentsIndexRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -258,12 +273,23 @@ export interface FileRoutesById {
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute '/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute '/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute '/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/events/$': typeof ApiEventsSplatRoute
'/api/spotify/callback': typeof ApiSpotifyCallbackRoute
'/api/spotify/capture': typeof ApiSpotifyCaptureRoute
'/api/spotify/playback': typeof ApiSpotifyPlaybackRoute
'/api/spotify/resume': typeof ApiSpotifyResumeRoute
'/api/spotify/search': typeof ApiSpotifySearchRoute
'/api/spotify/token': typeof ApiSpotifyTokenRoute
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
'/_authed/admin/': typeof AuthedAdminIndexRoute '/_authed/admin/': typeof AuthedAdminIndexRoute
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute '/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute '/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute '/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
'/_authed/admin/tournaments/$id/': typeof AuthedAdminTournamentsIdIndexRoute '/_authed/admin/tournaments/$id/': typeof AuthedAdminTournamentsIdIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
@@ -280,12 +306,23 @@ export interface FileRouteTypes {
| '/profile/$playerId' | '/profile/$playerId'
| '/teams/$teamId' | '/teams/$teamId'
| '/tournaments/$tournamentId' | '/tournaments/$tournamentId'
| '/api/auth/$'
| '/api/events/$'
| '/api/spotify/callback'
| '/api/spotify/capture'
| '/api/spotify/playback'
| '/api/spotify/resume'
| '/api/spotify/search'
| '/api/spotify/token'
| '/api/teams/upload-logo'
| '/api/tournaments/upload-logo'
| '/admin/' | '/admin/'
| '/tournaments' | '/tournaments'
| '/tournaments/$id/bracket' | '/tournaments/$id/bracket'
| '/admin/tournaments' | '/admin/tournaments'
| '/admin/tournaments/$id/teams' | '/admin/tournaments/$id/teams'
| '/admin/tournaments/run/$id' | '/admin/tournaments/run/$id'
| '/api/files/$collection/$recordId/$file'
| '/admin/tournaments/$id' | '/admin/tournaments/$id'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
@@ -299,12 +336,23 @@ export interface FileRouteTypes {
| '/profile/$playerId' | '/profile/$playerId'
| '/teams/$teamId' | '/teams/$teamId'
| '/tournaments/$tournamentId' | '/tournaments/$tournamentId'
| '/api/auth/$'
| '/api/events/$'
| '/api/spotify/callback'
| '/api/spotify/capture'
| '/api/spotify/playback'
| '/api/spotify/resume'
| '/api/spotify/search'
| '/api/spotify/token'
| '/api/teams/upload-logo'
| '/api/tournaments/upload-logo'
| '/admin' | '/admin'
| '/tournaments' | '/tournaments'
| '/tournaments/$id/bracket' | '/tournaments/$id/bracket'
| '/admin/tournaments' | '/admin/tournaments'
| '/admin/tournaments/$id/teams' | '/admin/tournaments/$id/teams'
| '/admin/tournaments/run/$id' | '/admin/tournaments/run/$id'
| '/api/files/$collection/$recordId/$file'
| '/admin/tournaments/$id' | '/admin/tournaments/$id'
id: id:
| '__root__' | '__root__'
@@ -320,12 +368,23 @@ export interface FileRouteTypes {
| '/_authed/profile/$playerId' | '/_authed/profile/$playerId'
| '/_authed/teams/$teamId' | '/_authed/teams/$teamId'
| '/_authed/tournaments/$tournamentId' | '/_authed/tournaments/$tournamentId'
| '/api/auth/$'
| '/api/events/$'
| '/api/spotify/callback'
| '/api/spotify/capture'
| '/api/spotify/playback'
| '/api/spotify/resume'
| '/api/spotify/search'
| '/api/spotify/token'
| '/api/teams/upload-logo'
| '/api/tournaments/upload-logo'
| '/_authed/admin/' | '/_authed/admin/'
| '/_authed/tournaments/' | '/_authed/tournaments/'
| '/_authed/tournaments/$id/bracket' | '/_authed/tournaments/$id/bracket'
| '/_authed/admin/tournaments/' | '/_authed/admin/tournaments/'
| '/_authed/admin/tournaments/$id/teams' | '/_authed/admin/tournaments/$id/teams'
| '/_authed/admin/tournaments/run/$id' | '/_authed/admin/tournaments/run/$id'
| '/api/files/$collection/$recordId/$file'
| '/_authed/admin/tournaments/$id/' | '/_authed/admin/tournaments/$id/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -334,101 +393,17 @@ export interface RootRouteChildren {
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
LogoutRoute: typeof LogoutRoute LogoutRoute: typeof LogoutRoute
RefreshSessionRoute: typeof RefreshSessionRoute RefreshSessionRoute: typeof RefreshSessionRoute
} ApiAuthSplatRoute: typeof ApiAuthSplatRoute
export interface FileServerRoutesByFullPath { ApiEventsSplatRoute: typeof ApiEventsSplatRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute ApiSpotifyCallbackRoute: typeof ApiSpotifyCallbackRoute
'/api/events/$': typeof ApiEventsSplatServerRoute ApiSpotifyCaptureRoute: typeof ApiSpotifyCaptureRoute
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute ApiSpotifyPlaybackRoute: typeof ApiSpotifyPlaybackRoute
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute ApiSpotifyResumeRoute: typeof ApiSpotifyResumeRoute
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute ApiSpotifySearchRoute: typeof ApiSpotifySearchRoute
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute ApiSpotifyTokenRoute: typeof ApiSpotifyTokenRoute
'/api/spotify/search': typeof ApiSpotifySearchServerRoute ApiTeamsUploadLogoRoute: typeof ApiTeamsUploadLogoRoute
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute ApiTournamentsUploadLogoRoute: typeof ApiTournamentsUploadLogoRoute
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute ApiFilesCollectionRecordIdFileRoute: typeof ApiFilesCollectionRecordIdFileRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
}
export interface FileServerRoutesByTo {
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
'/api/spotify/search': typeof ApiSpotifySearchServerRoute
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
}
export interface FileServerRoutesById {
__root__: typeof rootServerRouteImport
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
'/api/spotify/search': typeof ApiSpotifySearchServerRoute
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
}
export interface FileServerRouteTypes {
fileServerRoutesByFullPath: FileServerRoutesByFullPath
fullPaths:
| '/api/auth/$'
| '/api/events/$'
| '/api/spotify/callback'
| '/api/spotify/capture'
| '/api/spotify/playback'
| '/api/spotify/resume'
| '/api/spotify/search'
| '/api/spotify/token'
| '/api/teams/upload-logo'
| '/api/tournaments/upload-logo'
| '/api/files/$collection/$recordId/$file'
fileServerRoutesByTo: FileServerRoutesByTo
to:
| '/api/auth/$'
| '/api/events/$'
| '/api/spotify/callback'
| '/api/spotify/capture'
| '/api/spotify/playback'
| '/api/spotify/resume'
| '/api/spotify/search'
| '/api/spotify/token'
| '/api/teams/upload-logo'
| '/api/tournaments/upload-logo'
| '/api/files/$collection/$recordId/$file'
id:
| '__root__'
| '/api/auth/$'
| '/api/events/$'
| '/api/spotify/callback'
| '/api/spotify/capture'
| '/api/spotify/playback'
| '/api/spotify/resume'
| '/api/spotify/search'
| '/api/spotify/token'
| '/api/teams/upload-logo'
| '/api/tournaments/upload-logo'
| '/api/files/$collection/$recordId/$file'
fileServerRoutesById: FileServerRoutesById
}
export interface RootServerRouteChildren {
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
ApiSpotifyCallbackServerRoute: typeof ApiSpotifyCallbackServerRoute
ApiSpotifyCaptureServerRoute: typeof ApiSpotifyCaptureServerRoute
ApiSpotifyPlaybackServerRoute: typeof ApiSpotifyPlaybackServerRoute
ApiSpotifyResumeServerRoute: typeof ApiSpotifyResumeServerRoute
ApiSpotifySearchServerRoute: typeof ApiSpotifySearchServerRoute
ApiSpotifyTokenServerRoute: typeof ApiSpotifyTokenServerRoute
ApiTeamsUploadLogoServerRoute: typeof ApiTeamsUploadLogoServerRoute
ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute
ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -503,6 +478,76 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedAdminIndexRouteImport preLoaderRoute: typeof AuthedAdminIndexRouteImport
parentRoute: typeof AuthedAdminRoute parentRoute: typeof AuthedAdminRoute
} }
'/api/tournaments/upload-logo': {
id: '/api/tournaments/upload-logo'
path: '/api/tournaments/upload-logo'
fullPath: '/api/tournaments/upload-logo'
preLoaderRoute: typeof ApiTournamentsUploadLogoRouteImport
parentRoute: typeof rootRouteImport
}
'/api/teams/upload-logo': {
id: '/api/teams/upload-logo'
path: '/api/teams/upload-logo'
fullPath: '/api/teams/upload-logo'
preLoaderRoute: typeof ApiTeamsUploadLogoRouteImport
parentRoute: typeof rootRouteImport
}
'/api/spotify/token': {
id: '/api/spotify/token'
path: '/api/spotify/token'
fullPath: '/api/spotify/token'
preLoaderRoute: typeof ApiSpotifyTokenRouteImport
parentRoute: typeof rootRouteImport
}
'/api/spotify/search': {
id: '/api/spotify/search'
path: '/api/spotify/search'
fullPath: '/api/spotify/search'
preLoaderRoute: typeof ApiSpotifySearchRouteImport
parentRoute: typeof rootRouteImport
}
'/api/spotify/resume': {
id: '/api/spotify/resume'
path: '/api/spotify/resume'
fullPath: '/api/spotify/resume'
preLoaderRoute: typeof ApiSpotifyResumeRouteImport
parentRoute: typeof rootRouteImport
}
'/api/spotify/playback': {
id: '/api/spotify/playback'
path: '/api/spotify/playback'
fullPath: '/api/spotify/playback'
preLoaderRoute: typeof ApiSpotifyPlaybackRouteImport
parentRoute: typeof rootRouteImport
}
'/api/spotify/capture': {
id: '/api/spotify/capture'
path: '/api/spotify/capture'
fullPath: '/api/spotify/capture'
preLoaderRoute: typeof ApiSpotifyCaptureRouteImport
parentRoute: typeof rootRouteImport
}
'/api/spotify/callback': {
id: '/api/spotify/callback'
path: '/api/spotify/callback'
fullPath: '/api/spotify/callback'
preLoaderRoute: typeof ApiSpotifyCallbackRouteImport
parentRoute: typeof rootRouteImport
}
'/api/events/$': {
id: '/api/events/$'
path: '/api/events/$'
fullPath: '/api/events/$'
preLoaderRoute: typeof ApiEventsSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/api/auth/$': {
id: '/api/auth/$'
path: '/api/auth/$'
fullPath: '/api/auth/$'
preLoaderRoute: typeof ApiAuthSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/tournaments/$tournamentId': { '/_authed/tournaments/$tournamentId': {
id: '/_authed/tournaments/$tournamentId' id: '/_authed/tournaments/$tournamentId'
path: '/tournaments/$tournamentId' path: '/tournaments/$tournamentId'
@@ -552,6 +597,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedAdminTournamentsIdIndexRouteImport preLoaderRoute: typeof AuthedAdminTournamentsIdIndexRouteImport
parentRoute: typeof AuthedAdminRoute parentRoute: typeof AuthedAdminRoute
} }
'/api/files/$collection/$recordId/$file': {
id: '/api/files/$collection/$recordId/$file'
path: '/api/files/$collection/$recordId/$file'
fullPath: '/api/files/$collection/$recordId/$file'
preLoaderRoute: typeof ApiFilesCollectionRecordIdFileRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/admin/tournaments/run/$id': { '/_authed/admin/tournaments/run/$id': {
id: '/_authed/admin/tournaments/run/$id' id: '/_authed/admin/tournaments/run/$id'
path: '/tournaments/run/$id' path: '/tournaments/run/$id'
@@ -568,87 +620,6 @@ declare module '@tanstack/react-router' {
} }
} }
} }
declare module '@tanstack/react-start/server' {
interface ServerFileRoutesByPath {
'/api/tournaments/upload-logo': {
id: '/api/tournaments/upload-logo'
path: '/api/tournaments/upload-logo'
fullPath: '/api/tournaments/upload-logo'
preLoaderRoute: typeof ApiTournamentsUploadLogoServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/teams/upload-logo': {
id: '/api/teams/upload-logo'
path: '/api/teams/upload-logo'
fullPath: '/api/teams/upload-logo'
preLoaderRoute: typeof ApiTeamsUploadLogoServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/spotify/token': {
id: '/api/spotify/token'
path: '/api/spotify/token'
fullPath: '/api/spotify/token'
preLoaderRoute: typeof ApiSpotifyTokenServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/spotify/search': {
id: '/api/spotify/search'
path: '/api/spotify/search'
fullPath: '/api/spotify/search'
preLoaderRoute: typeof ApiSpotifySearchServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/spotify/resume': {
id: '/api/spotify/resume'
path: '/api/spotify/resume'
fullPath: '/api/spotify/resume'
preLoaderRoute: typeof ApiSpotifyResumeServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/spotify/playback': {
id: '/api/spotify/playback'
path: '/api/spotify/playback'
fullPath: '/api/spotify/playback'
preLoaderRoute: typeof ApiSpotifyPlaybackServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/spotify/capture': {
id: '/api/spotify/capture'
path: '/api/spotify/capture'
fullPath: '/api/spotify/capture'
preLoaderRoute: typeof ApiSpotifyCaptureServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/spotify/callback': {
id: '/api/spotify/callback'
path: '/api/spotify/callback'
fullPath: '/api/spotify/callback'
preLoaderRoute: typeof ApiSpotifyCallbackServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/events/$': {
id: '/api/events/$'
path: '/api/events/$'
fullPath: '/api/events/$'
preLoaderRoute: typeof ApiEventsSplatServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/auth/$': {
id: '/api/auth/$'
path: '/api/auth/$'
fullPath: '/api/auth/$'
preLoaderRoute: typeof ApiAuthSplatServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/files/$collection/$recordId/$file': {
id: '/api/files/$collection/$recordId/$file'
path: '/api/files/$collection/$recordId/$file'
fullPath: '/api/files/$collection/$recordId/$file'
preLoaderRoute: typeof ApiFilesCollectionRecordIdFileServerRouteImport
parentRoute: typeof rootServerRouteImport
}
}
}
interface AuthedAdminRouteChildren { interface AuthedAdminRouteChildren {
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
@@ -704,24 +675,26 @@ const rootRouteChildren: RootRouteChildren = {
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
LogoutRoute: LogoutRoute, LogoutRoute: LogoutRoute,
RefreshSessionRoute: RefreshSessionRoute, RefreshSessionRoute: RefreshSessionRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiEventsSplatRoute: ApiEventsSplatRoute,
ApiSpotifyCallbackRoute: ApiSpotifyCallbackRoute,
ApiSpotifyCaptureRoute: ApiSpotifyCaptureRoute,
ApiSpotifyPlaybackRoute: ApiSpotifyPlaybackRoute,
ApiSpotifyResumeRoute: ApiSpotifyResumeRoute,
ApiSpotifySearchRoute: ApiSpotifySearchRoute,
ApiSpotifyTokenRoute: ApiSpotifyTokenRoute,
ApiTeamsUploadLogoRoute: ApiTeamsUploadLogoRoute,
ApiTournamentsUploadLogoRoute: ApiTournamentsUploadLogoRoute,
ApiFilesCollectionRecordIdFileRoute: ApiFilesCollectionRecordIdFileRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>()
const rootServerRouteChildren: RootServerRouteChildren = {
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute, import type { getRouter } from './router.tsx'
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute, import type { createStart } from '@tanstack/react-start'
ApiSpotifyCallbackServerRoute: ApiSpotifyCallbackServerRoute, declare module '@tanstack/react-start' {
ApiSpotifyCaptureServerRoute: ApiSpotifyCaptureServerRoute, interface Register {
ApiSpotifyPlaybackServerRoute: ApiSpotifyPlaybackServerRoute, router: Awaited<ReturnType<typeof getRouter>>
ApiSpotifyResumeServerRoute: ApiSpotifyResumeServerRoute, }
ApiSpotifySearchServerRoute: ApiSpotifySearchServerRoute,
ApiSpotifyTokenServerRoute: ApiSpotifyTokenServerRoute,
ApiTeamsUploadLogoServerRoute: ApiTeamsUploadLogoServerRoute,
ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute,
ApiFilesCollectionRecordIdFileServerRoute:
ApiFilesCollectionRecordIdFileServerRoute,
} }
export const serverRouteTree = rootServerRouteImport
._addFileChildren(rootServerRouteChildren)
._addFileTypes<FileServerRouteTypes>()

View File

@@ -1,11 +1,11 @@
import { QueryClient } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query";
import { createRouter as createTanStackRouter } from "@tanstack/react-router"; import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { routerWithQueryClient } from "@tanstack/react-router-with-query"; import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary"; import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config"; import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
export function createRouter() { export function getRouter() {
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
@@ -18,8 +18,7 @@ export function createRouter() {
}, },
}); });
return routerWithQueryClient( const router = createTanStackRouter({
createTanStackRouter({
routeTree, routeTree,
context: { context: {
queryClient, queryClient,
@@ -33,13 +32,18 @@ export function createRouter() {
defaultErrorComponent: DefaultCatchBoundary, defaultErrorComponent: DefaultCatchBoundary,
scrollRestoration: true, scrollRestoration: true,
defaultViewTransition: false, defaultViewTransition: false,
}), });
setupRouterSsrQueryIntegration({
router,
queryClient queryClient
); })
return router;
} }
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: ReturnType<typeof createRouter>; router: ReturnType<typeof getRouter>;
} }
} }

View File

@@ -83,12 +83,20 @@ export const Route = createRootRouteWithContext<{
return {}; return {};
} }
if (location.pathname === '/login' || location.pathname === '/logout') {
return {};
}
try {
// https://github.com/TanStack/router/discussions/3531 // https://github.com/TanStack/router/discussions/3531
const auth = await ensureServerQueryData( const auth = await ensureServerQueryData(
context.queryClient, context.queryClient,
playerQueries.auth() playerQueries.auth()
); );
return { auth }; return { auth };
} catch (error) {
return {};
}
}, },
pendingComponent: () => <Providers><FullScreenLoader /></Providers>, pendingComponent: () => <Providers><FullScreenLoader /></Providers>,
}); });

View File

@@ -1,5 +1,5 @@
// API file that handles all supertokens auth routes // API file that handles all supertokens auth routes
import { createServerFileRoute } from '@tanstack/react-start/server'; import { createFileRoute } from '@tanstack/react-router';
import { handleAuthAPIRequest } from 'supertokens-node/custom' import { handleAuthAPIRequest } from 'supertokens-node/custom'
import { ensureSuperTokensBackend } from '@/lib/supertokens/server' import { ensureSuperTokensBackend } from '@/lib/supertokens/server'
@@ -12,7 +12,9 @@ const handleRequest = async ({ request }: {request: Request}) => {
console.log("Handling auth request:", request.method, request.url); console.log("Handling auth request:", request.method, request.url);
return superTokensHandler(request); return superTokensHandler(request);
}; };
export const ServerRoute = createServerFileRoute('/api/auth/$').methods({ export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: handleRequest, GET: handleRequest,
POST: handleRequest, POST: handleRequest,
PUT: handleRequest, PUT: handleRequest,
@@ -20,4 +22,6 @@ export const ServerRoute = createServerFileRoute('/api/auth/$').methods({
PATCH: handleRequest, PATCH: handleRequest,
OPTIONS: handleRequest, OPTIONS: handleRequest,
HEAD: handleRequest, HEAD: handleRequest,
}
}
}) })

View File

@@ -1,11 +1,14 @@
import { createServerFileRoute } from "@tanstack/react-start/server"; import { createFileRoute } from "@tanstack/react-router";
import { serverEvents, type ServerEvent } from "@/lib/events/emitter"; import { serverEvents, type ServerEvent } from "@/lib/events/emitter";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import { superTokensRequestMiddleware } from "@/utils/supertokens"; import { superTokensRequestMiddleware } from "@/utils/supertokens";
export const ServerRoute = createServerFileRoute("/api/events/$").middleware([superTokensRequestMiddleware]).methods({ export const Route = createFileRoute("/api/events/$")({
server: {
middleware: [superTokensRequestMiddleware],
handlers: {
GET: ({ request, context }) => { GET: ({ request, context }) => {
logger.info('ServerEvents | New connection', context?.userAuthId); logger.info("ServerEvents | New connection", context?.userAuthId);
const stream = new ReadableStream({ const stream = new ReadableStream({
start(controller) { start(controller) {
@@ -13,7 +16,7 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
controller.enqueue(new TextEncoder().encode(connectMessage)); controller.enqueue(new TextEncoder().encode(connectMessage));
const handleEvent = (event: ServerEvent) => { const handleEvent = (event: ServerEvent) => {
logger.info('ServerEvents | Event received', event); logger.info("ServerEvents | Event received", event);
const message = `data: ${JSON.stringify(event)}\n\n`; const message = `data: ${JSON.stringify(event)}\n\n`;
try { try {
controller.enqueue(new TextEncoder().encode(message)); controller.enqueue(new TextEncoder().encode(message));
@@ -40,15 +43,16 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
serverEvents.off("test", handleEvent); serverEvents.off("test", handleEvent);
clearInterval(pingInterval); clearInterval(pingInterval);
try { try {
logger.info('ServerEvents | Closing connection', context?.userAuthId); logger.info(
controller.close(); "ServerEvents | Closing connection",
context?.userAuthId
);
} catch (e) { } catch (e) {
logger.error('ServerEvents | Error closing controller', e); logger.error("ServerEvents | Error closing controller", e);
} }
}; };
request.signal?.addEventListener("abort", cleanup); request.signal?.addEventListener("abort", cleanup);
return cleanup; return cleanup;
}, },
}); });
@@ -63,4 +67,6 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
}, },
}); });
}, },
},
},
}); });

View File

@@ -1,84 +1,100 @@
import { createServerFileRoute } from "@tanstack/react-start/server"; import { createFileRoute } from "@tanstack/react-router";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
export const ServerRoute = createServerFileRoute("/api/files/$collection/$recordId/$file").methods({ export const Route = createFileRoute(
"/api/files/$collection/$recordId/$file"
)({
server: {
handlers: {
GET: async ({ params, request }) => { GET: async ({ params, request }) => {
try { try {
const { collection, recordId, file } = params; const { collection, recordId, file } = params;
const pocketbaseUrl = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090'; const pocketbaseUrl =
process.env.POCKETBASE_URL || "http://127.0.0.1:8090";
const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`; const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`;
logger.info('File proxy', { logger.info("File proxy", {
collection, collection,
recordId, recordId,
file, file,
targetUrl: fileUrl targetUrl: fileUrl,
}); });
const response = await fetch(fileUrl, { const response = await fetch(fileUrl, {
method: 'GET', method: "GET",
headers: { headers: {
...(request.headers.get('range') && { 'Range': request.headers.get('range')! }), ...(request.headers.get("range") && {
...(request.headers.get('if-none-match') && { 'If-None-Match': request.headers.get('if-none-match')! }), Range: request.headers.get("range")!,
...(request.headers.get('if-modified-since') && { 'If-Modified-Since': request.headers.get('if-modified-since')! }), }),
...(request.headers.get("if-none-match") && {
"If-None-Match": request.headers.get("if-none-match")!,
}),
...(request.headers.get("if-modified-since") && {
"If-Modified-Since": request.headers.get("if-modified-since")!,
}),
}, },
}); });
if (!response.ok) { if (!response.ok) {
logger.error('PocketBase file request failed', { logger.error("PocketBase file request failed", {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
url: fileUrl url: fileUrl,
}); });
if (response.status === 404) { if (response.status === 404) {
return new Response('File not found', { status: 404 }); return new Response("File not found", { status: 404 });
} }
return new Response(`PocketBase error: ${response.statusText}`, { return new Response(`PocketBase error: ${response.statusText}`, {
status: response.status status: response.status,
}); });
} }
const body = response.body; const body = response.body;
const responseHeaders = new Headers(); const responseHeaders = new Headers();
const headers = [ const headers = [
'content-type', "content-type",
'content-length', "content-length",
'content-disposition', "content-disposition",
'etag', "etag",
'last-modified', "last-modified",
'cache-control', "cache-control",
'accept-ranges', "accept-ranges",
'content-range' "content-range",
]; ];
headers.forEach(header => { headers.forEach((header) => {
const value = response.headers.get(header); const value = response.headers.get(header);
if (value) { if (value) {
responseHeaders.set(header, value); responseHeaders.set(header, value);
} }
}); });
responseHeaders.set('Access-Control-Allow-Origin', '*'); responseHeaders.set("Access-Control-Allow-Origin", "*");
responseHeaders.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); responseHeaders.set(
responseHeaders.set('Access-Control-Allow-Headers', 'Range, If-None-Match, If-Modified-Since'); "Access-Control-Allow-Methods",
"GET, HEAD, OPTIONS"
);
responseHeaders.set(
"Access-Control-Allow-Headers",
"Range, If-None-Match, If-Modified-Since"
);
logger.info('File proxy response', { logger.info("File proxy response", {
status: response.status, status: response.status,
contentType: response.headers.get('content-type'), contentType: response.headers.get("content-type"),
contentLength: response.headers.get('content-length') contentLength: response.headers.get("content-length"),
}); });
return new Response(body, { return new Response(body, {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
headers: responseHeaders headers: responseHeaders,
}); });
} catch (error) { } catch (error) {
logger.error('File proxy error', error); logger.error("File proxy error", error);
return new Response('Internal server error', { status: 500 }); return new Response("Internal server error", { status: 500 });
} }
}, },
@@ -86,10 +102,12 @@ export const ServerRoute = createServerFileRoute("/api/files/$collection/$record
return new Response(null, { return new Response(null, {
status: 200, status: 200,
headers: { headers: {
'Access-Control-Allow-Origin': '*', "Access-Control-Allow-Origin": "*",
'Access-Control-Allow-Methods': 'GET, OPTIONS', "Access-Control-Allow-Methods": "GET, OPTIONS",
'Access-Control-Max-Age': '86400', "Access-Control-Max-Age": "86400",
} },
}); });
} },
},
},
}); });

View File

@@ -1,127 +1,146 @@
import { createServerFileRoute } from '@tanstack/react-start/server' import { createFileRoute } from "@tanstack/react-router";
import { SpotifyAuth } from '@/lib/spotify/auth' import { SpotifyAuth } from "@/lib/spotify/auth";
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID! const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!;
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET! const SPOTIFY_CLIENT_SECRET = import.meta.env.SPOTIFY_CLIENT_SECRET!;
const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI! const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI!;
export const ServerRoute = createServerFileRoute('/api/spotify/callback').methods({ export const Route = createFileRoute("/api/spotify/callback")({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => { GET: async ({ request }: { request: Request }) => {
const getReturnPath = (state: string | null): string => { const getReturnPath = (state: string | null): string => {
if (!state) return '/'; if (!state) return "/";
try { try {
const decodedState = JSON.parse(atob(state)); const decodedState = JSON.parse(atob(state));
return decodedState.returnPath || '/'; return decodedState.returnPath || "/";
} catch { } catch {
return '/'; return "/";
} }
}; };
try { try {
const url = new URL(request.url) const url = new URL(request.url);
const code = url.searchParams.get('code') const code = url.searchParams.get("code");
const state = url.searchParams.get('state') const state = url.searchParams.get("state");
const error = url.searchParams.get('error') const error = url.searchParams.get("error");
const returnPath = getReturnPath(state); const returnPath = getReturnPath(state);
if (error) { if (error) {
console.error('Spotify OAuth error:', error) console.error("Spotify OAuth error:", error);
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
'Location': returnPath + '?spotify_error=' + encodeURIComponent(error), Location:
returnPath + "?spotify_error=" + encodeURIComponent(error),
}, },
}) });
} }
if (!code || !state) { if (!code || !state) {
console.error('Missing code or state:', { code: !!code, state: !!state }) console.error("Missing code or state:", {
code: !!code,
state: !!state,
});
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
'Location': returnPath + '?spotify_error=missing_code_or_state', Location: returnPath + "?spotify_error=missing_code_or_state",
}, },
}) });
} }
console.log('Token exchange attempt:', { console.log("Token exchange attempt:", {
client_id: SPOTIFY_CLIENT_ID, client_id: SPOTIFY_CLIENT_ID,
redirect_uri: SPOTIFY_REDIRECT_URI, redirect_uri: SPOTIFY_REDIRECT_URI,
has_code: !!code, has_code: !!code,
has_state: !!state, has_state: !!state,
}) });
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', { const tokenResponse = await fetch(
method: 'POST', "https://accounts.spotify.com/api/token",
{
method: "POST",
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`, Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64")}`,
}, },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: 'authorization_code', grant_type: "authorization_code",
code, code,
redirect_uri: SPOTIFY_REDIRECT_URI, redirect_uri: SPOTIFY_REDIRECT_URI,
}), }),
}) }
);
if (!tokenResponse.ok) { if (!tokenResponse.ok) {
const errorText = await tokenResponse.text() const errorText = await tokenResponse.text();
console.error('Token exchange error:', { console.error("Token exchange error:", {
status: tokenResponse.status, status: tokenResponse.status,
statusText: tokenResponse.statusText, statusText: tokenResponse.statusText,
body: errorText, body: errorText,
redirect_uri: SPOTIFY_REDIRECT_URI, redirect_uri: SPOTIFY_REDIRECT_URI,
}) });
const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`) const errorParam = encodeURIComponent(
`${tokenResponse.status}: ${errorText}`
);
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
'Location': `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`, Location: `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`,
}, },
}) });
} }
const tokens = await tokenResponse.json() const tokens = await tokenResponse.json();
console.log('Token exchange successful:', { console.log("Token exchange successful:", {
has_access_token: !!tokens.access_token, has_access_token: !!tokens.access_token,
has_refresh_token: !!tokens.refresh_token, has_refresh_token: !!tokens.refresh_token,
expires_in: tokens.expires_in, expires_in: tokens.expires_in,
}) });
console.log('Decoded return path:', returnPath); console.log("Decoded return path:", returnPath);
const response = new Response(null, { const response = new Response(null, {
status: 302, status: 302,
headers: { headers: {
'Location': returnPath + '?spotify_auth=success', Location: returnPath + "?spotify_auth=success",
}, },
}) });
const isSecure = process.env.NODE_ENV === 'production' const isSecure = import.meta.env.NODE_ENV === "production";
const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}` const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`;
response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`) response.headers.append(
"Set-Cookie",
`spotify_access_token=${tokens.access_token}; ${cookieOptions}`
);
if (tokens.refresh_token) { if (tokens.refresh_token) {
const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}` // 30 days const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}`; // 30 days
response.headers.append('Set-Cookie', `spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`) response.headers.append(
"Set-Cookie",
`spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`
);
} }
return response return response;
} catch (error) { } catch (error) {
console.error('Spotify callback error:', error) console.error("Spotify callback error:", error);
const url = new URL(request.url); const url = new URL(request.url);
const state = url.searchParams.get('state'); const state = url.searchParams.get("state");
const returnPath = getReturnPath(state); const returnPath = getReturnPath(state);
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
'Location': returnPath + '?spotify_error=callback_failed', Location: returnPath + "?spotify_error=callback_failed",
}, },
}) });
} }
}, },
}) },
},
});

View File

@@ -1,59 +1,60 @@
import { createServerFileRoute } from '@tanstack/react-start/server' import { createFileRoute } from "@tanstack/react-router";
import { SpotifyWebApiClient } from '@/lib/spotify/client' import { SpotifyWebApiClient } from "@/lib/spotify/client";
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types' import type { SpotifyPlaybackSnapshot } from "@/lib/spotify/types";
export const ServerRoute = createServerFileRoute('/api/spotify/capture').methods({ export const Route = createFileRoute("/api/spotify/capture")({
server: {
handlers: {
POST: async ({ request }: { request: Request }) => { POST: async ({ request }: { request: Request }) => {
try { try {
// Get access token from cookies const cookies = request.headers.get("Cookie") || "";
const cookies = request.headers.get('Cookie') || '' const accessTokenMatch = cookies.match(
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/) /spotify_access_token=([^;]+)/
);
if (!accessTokenMatch) { if (!accessTokenMatch) {
return new Response( return new Response(
JSON.stringify({ error: 'No access token found' }), JSON.stringify({ error: "No access token found" }),
{ {
status: 401, status: 401,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} }
) );
} }
const accessToken = decodeURIComponent(accessTokenMatch[1]) const accessToken = decodeURIComponent(accessTokenMatch[1]);
const spotifyClient = new SpotifyWebApiClient(accessToken) const spotifyClient = new SpotifyWebApiClient(accessToken);
// Create a snapshot of the current playback state const snapshot = await spotifyClient.createPlaybackSnapshot();
const snapshot = await spotifyClient.createPlaybackSnapshot()
if (!snapshot) { if (!snapshot) {
return new Response( return new Response(
JSON.stringify({ error: 'No active playback to capture' }), JSON.stringify({ error: "No active playback to capture" }),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} }
) );
} }
return new Response( return new Response(JSON.stringify({ snapshot }), {
JSON.stringify({ snapshot }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} });
)
} catch (error) { } catch (error) {
console.error('Spotify capture error:', error) console.error("Spotify capture error:", error);
const errorMessage = error instanceof Error ? error.message : 'Failed to capture playback state' const errorMessage =
error instanceof Error
? error.message
: "Failed to capture playback state";
return new Response( return new Response(JSON.stringify({ error: errorMessage }), {
JSON.stringify({ error: errorMessage }),
{
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} });
)
} }
}, },
}) },
},
});

View File

@@ -1,202 +1,203 @@
import { createServerFileRoute } from '@tanstack/react-start/server' import { createFileRoute } from "@tanstack/react-router";
import { SpotifyWebApiClient } from '@/lib/spotify/client' import { SpotifyWebApiClient } from "@/lib/spotify/client";
function getAccessTokenFromCookies(request: Request): string | null { function getAccessTokenFromCookies(request: Request): string | null {
const cookieHeader = request.headers.get('cookie') const cookieHeader = request.headers.get("cookie");
if (!cookieHeader) return null if (!cookieHeader) return null;
const cookies = Object.fromEntries( const cookies = Object.fromEntries(
cookieHeader.split('; ').map(c => c.split('=')) cookieHeader.split("; ").map((c) => c.split("="))
) );
return cookies.spotify_access_token || null return cookies.spotify_access_token || null;
} }
export const ServerRoute = createServerFileRoute('/api/spotify/playback').methods({ export const Route = createFileRoute("/api/spotify/playback")({
server: {
handlers: {
POST: async ({ request }: { request: Request }) => { POST: async ({ request }: { request: Request }) => {
try { try {
const accessToken = getAccessTokenFromCookies(request) const accessToken = getAccessTokenFromCookies(request);
if (!accessToken) { if (!accessToken) {
return new Response( return new Response(
JSON.stringify({ error: 'No access token found' }), JSON.stringify({ error: "No access token found" }),
{ {
status: 401, status: 401,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
const body = await request.json() const body = await request.json();
const { action, deviceId, volumePercent, trackId, positionMs } = body const { action, deviceId, volumePercent, trackId, positionMs } = body;
const spotifyClient = new SpotifyWebApiClient(accessToken) const spotifyClient = new SpotifyWebApiClient(accessToken);
switch (action) { switch (action) {
case 'play': case "play":
await spotifyClient.play(deviceId) await spotifyClient.play(deviceId);
break break;
case 'playTrack': case "playTrack":
if (!trackId) { if (!trackId) {
return new Response( return new Response(
JSON.stringify({ error: 'trackId is required for playTrack action' }), JSON.stringify({
error: "trackId is required for playTrack action",
}),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
await spotifyClient.playTrack(trackId, deviceId, positionMs) await spotifyClient.playTrack(trackId, deviceId, positionMs);
break break;
case 'pause': case "pause":
await spotifyClient.pause() await spotifyClient.pause();
break break;
case 'next': case "next":
await spotifyClient.skipToNext() await spotifyClient.skipToNext();
break break;
case 'previous': case "previous":
await spotifyClient.skipToPrevious() await spotifyClient.skipToPrevious();
break break;
case 'volume': case "volume":
if (typeof volumePercent !== 'number') { if (typeof volumePercent !== "number") {
return new Response( return new Response(
JSON.stringify({ error: 'volumePercent must be a number' }), JSON.stringify({ error: "volumePercent must be a number" }),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
await spotifyClient.setVolume(volumePercent) await spotifyClient.setVolume(volumePercent);
break break;
case 'transfer': case "transfer":
if (!deviceId) { if (!deviceId) {
return new Response( return new Response(
JSON.stringify({ error: 'deviceId is required for transfer action' }), JSON.stringify({
error: "deviceId is required for transfer action",
}),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
await spotifyClient.transferPlayback(deviceId) await spotifyClient.transferPlayback(deviceId);
break break;
default: default:
return new Response( return new Response(JSON.stringify({ error: "Invalid action" }), {
JSON.stringify({ error: 'Invalid action' }),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} });
)
} }
return new Response( return new Response(JSON.stringify({ success: true }), {
JSON.stringify({ success: true }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} });
)
} catch (error) { } catch (error) {
console.error('Playback control error:', error) console.error("Playback control error:", error);
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes('NO_ACTIVE_DEVICE')) { if (error.message.includes("NO_ACTIVE_DEVICE")) {
return new Response( return new Response(
JSON.stringify({ error: 'No active device found. Please select a device first.' }), JSON.stringify({
error:
"No active device found. Please select a device first.",
}),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
if (error.message.includes('PREMIUM_REQUIRED')) { if (error.message.includes("PREMIUM_REQUIRED")) {
return new Response( return new Response(
JSON.stringify({ error: 'Spotify Premium is required for playback control.' }), JSON.stringify({
error: "Spotify Premium is required for playback control.",
}),
{ {
status: 403, status: 403,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
console.error('Full error details:', { console.error("Full error details:", {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
name: error.name, name: error.name,
}) });
} }
return new Response( return new Response(
JSON.stringify({ error: 'Playback control failed', details: error instanceof Error ? error.message : 'Unknown error' }), JSON.stringify({
error: "Playback control failed",
details: error instanceof Error ? error.message : "Unknown error",
}),
{ {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
}, },
GET: async ({ request }: { request: Request }) => { GET: async ({ request }: { request: Request }) => {
try { try {
const accessToken = getAccessTokenFromCookies(request) const accessToken = getAccessTokenFromCookies(request);
if (!accessToken) { if (!accessToken) {
return new Response( return new Response(
JSON.stringify({ error: 'No access token found' }), JSON.stringify({ error: "No access token found" }),
{ {
status: 401, status: 401,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
const url = new URL(request.url) const url = new URL(request.url);
const type = url.searchParams.get('type') const type = url.searchParams.get("type");
const spotifyClient = new SpotifyWebApiClient(accessToken) const spotifyClient = new SpotifyWebApiClient(accessToken);
if (type === 'devices') { if (type === "devices") {
const devices = await spotifyClient.getDevices() const devices = await spotifyClient.getDevices();
return new Response( return new Response(JSON.stringify({ devices }), {
JSON.stringify({ devices }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} });
) } else if (type === "state") {
} else if (type === 'state') { const playbackState = await spotifyClient.getPlaybackState();
const playbackState = await spotifyClient.getPlaybackState() return new Response(JSON.stringify({ playbackState }), {
return new Response(
JSON.stringify({ playbackState }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} });
)
} else { } else {
const [devices, playbackState] = await Promise.all([ const [devices, playbackState] = await Promise.all([
spotifyClient.getDevices(), spotifyClient.getDevices(),
spotifyClient.getPlaybackState(), spotifyClient.getPlaybackState(),
]) ]);
return new Response( return new Response(JSON.stringify({ devices, playbackState }), {
JSON.stringify({ devices, playbackState }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} });
)
} }
} catch (error) { } catch (error) {
console.error('Get playback data error:', error) console.error("Get playback data error:", error);
return new Response( return new Response(
JSON.stringify({ error: 'Failed to get playback data' }), JSON.stringify({ error: "Failed to get playback data" }),
{ {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
}, },
}) },
},
});

View File

@@ -1,72 +1,71 @@
import { createServerFileRoute } from '@tanstack/react-start/server' import { createFileRoute } from "@tanstack/react-router";
import { SpotifyWebApiClient } from '@/lib/spotify/client' import { SpotifyWebApiClient } from "@/lib/spotify/client";
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types' import type { SpotifyPlaybackSnapshot } from "@/lib/spotify/types";
export const ServerRoute = createServerFileRoute('/api/spotify/resume').methods({ export const Route = createFileRoute("/api/spotify/resume")({
server: {
handlers: {
POST: async ({ request }: { request: Request }) => { POST: async ({ request }: { request: Request }) => {
try { try {
// Get access token from cookies const cookies = request.headers.get("Cookie") || "";
const cookies = request.headers.get('Cookie') || '' const accessTokenMatch = cookies.match(
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/) /spotify_access_token=([^;]+)/
);
if (!accessTokenMatch) { if (!accessTokenMatch) {
return new Response( return new Response(
JSON.stringify({ error: 'No access token found' }), JSON.stringify({ error: "No access token found" }),
{ {
status: 401, status: 401,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} }
) );
} }
const accessToken = decodeURIComponent(accessTokenMatch[1]) const accessToken = decodeURIComponent(accessTokenMatch[1]);
const spotifyClient = new SpotifyWebApiClient(accessToken) const spotifyClient = new SpotifyWebApiClient(accessToken);
// Parse the request body to get the snapshot const body = await request.json();
const body = await request.json() const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot };
const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot }
if (!snapshot) { if (!snapshot) {
return new Response( return new Response(
JSON.stringify({ error: 'No snapshot provided' }), JSON.stringify({ error: "No snapshot provided" }),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} }
) );
} }
// Restore the playback state from the snapshot await spotifyClient.restorePlaybackSnapshot(snapshot);
await spotifyClient.restorePlaybackSnapshot(snapshot)
return new Response( return new Response(JSON.stringify({ success: true }), {
JSON.stringify({ success: true }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} });
)
} catch (error) { } catch (error) {
console.error('Spotify resume error:', error) console.error("Spotify resume error:", error);
let errorMessage = 'Failed to resume playback state' let errorMessage = "Failed to resume playback state";
// Handle common Spotify Premium requirement error
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes('Premium') || error.message.includes('403')) { if (
errorMessage = 'Spotify Premium required for playback control' error.message.includes("Premium") ||
error.message.includes("403")
) {
errorMessage = "Spotify premium required";
} else { } else {
errorMessage = error.message errorMessage = error.message;
} }
} }
return new Response( return new Response(JSON.stringify({ error: errorMessage }), {
JSON.stringify({ error: errorMessage }),
{
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
} });
)
} }
}, },
}) },
},
});

View File

@@ -1,81 +1,87 @@
import { createServerFileRoute } from '@tanstack/react-start/server' import { createFileRoute } from "@tanstack/react-router";
// Function to get Client Credentials access token
async function getClientCredentialsToken(): Promise<string> { async function getClientCredentialsToken(): Promise<string> {
const clientId = process.env.VITE_SPOTIFY_CLIENT_ID const clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID;
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET const clientSecret = import.meta.env.SPOTIFY_CLIENT_SECRET;
if (!clientId || !clientSecret) { if (!clientId || !clientSecret) {
throw new Error('Missing Spotify client credentials') throw new Error("Missing Spotify client credentials");
} }
const response = await fetch('https://accounts.spotify.com/api/token', { const response = await fetch("https://accounts.spotify.com/api/token", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
}, },
body: 'grant_type=client_credentials', body: "grant_type=client_credentials",
}) });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to get Spotify access token') throw new Error("Failed to get Spotify access token");
} }
const data = await response.json() const data = await response.json();
return data.access_token return data.access_token;
} }
export const ServerRoute = createServerFileRoute('/api/spotify/search').methods({ export const Route = createFileRoute("/api/spotify/search")({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => { GET: async ({ request }: { request: Request }) => {
try { try {
const url = new URL(request.url) const url = new URL(request.url);
const query = url.searchParams.get('q') const query = url.searchParams.get("q");
if (!query) { if (!query) {
return new Response( return new Response(
JSON.stringify({ error: 'Query parameter q is required' }), JSON.stringify({ error: "Query parameter q is required" }),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
// Get client credentials access token // Get client credentials access token
const accessToken = await getClientCredentialsToken() const accessToken = await getClientCredentialsToken();
// Search using Spotify API directly // Search using Spotify API directly
const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20` const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`;
const searchResponse = await fetch(searchUrl, { const searchResponse = await fetch(searchUrl, {
headers: { headers: {
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}, },
}) });
if (!searchResponse.ok) { if (!searchResponse.ok) {
throw new Error('Spotify search request failed') throw new Error("Spotify search request failed");
} }
const searchResult = await searchResponse.json() const searchResult = await searchResponse.json();
return new Response( return new Response(
JSON.stringify({ tracks: searchResult.tracks.items }), JSON.stringify({ tracks: searchResult.tracks.items }),
{ {
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} catch (error) { } catch (error) {
console.error('Search error:', error) console.error("Search error:", error);
return new Response( return new Response(
JSON.stringify({ error: 'Search failed', details: error instanceof Error ? error.message : 'Unknown error' }), JSON.stringify({
error: "Search failed",
details: error instanceof Error ? error.message : "Unknown error",
}),
{ {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
}, },
}) },
},
});

View File

@@ -1,52 +1,58 @@
import { createServerFileRoute } from '@tanstack/react-start/server' import { createFileRoute } from "@tanstack/react-router";
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID! const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!;
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET! const SPOTIFY_CLIENT_SECRET = import.meta.env.SPOTIFY_CLIENT_SECRET!;
export const ServerRoute = createServerFileRoute('/api/spotify/token').methods({ export const Route = createFileRoute("/api/spotify/token")({
server: {
handlers: {
POST: async ({ request }: { request: Request }) => { POST: async ({ request }: { request: Request }) => {
try { try {
const body = await request.json() const body = await request.json();
const { refresh_token } = body const { refresh_token } = body;
if (!refresh_token) { if (!refresh_token) {
return new Response( return new Response(
JSON.stringify({ error: 'refresh_token is required' }), JSON.stringify({ error: "refresh_token is required" }),
{ {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
// Refresh access token const tokenResponse = await fetch(
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', { "https://accounts.spotify.com/api/token",
method: 'POST', {
method: "POST",
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`, Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64")}`,
}, },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: 'refresh_token', grant_type: "refresh_token",
refresh_token, refresh_token,
}), }),
}) }
);
if (!tokenResponse.ok) { if (!tokenResponse.ok) {
const error = await tokenResponse.json() const error = await tokenResponse.json();
console.error('Token refresh error:', error) console.error("Token refresh error:", error);
return new Response( return new Response(
JSON.stringify({ error: 'Failed to refresh token', details: error }), JSON.stringify({
error: "Failed to refresh token",
details: error,
}),
{ {
status: tokenResponse.status, status: tokenResponse.status,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
const tokens = await tokenResponse.json() const tokens = await tokenResponse.json();
// Return new tokens
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
access_token: tokens.access_token, access_token: tokens.access_token,
@@ -56,50 +62,46 @@ export const ServerRoute = createServerFileRoute('/api/spotify/token').methods({
}), }),
{ {
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} catch (error) { } catch (error) {
console.error('Token refresh endpoint error:', error) console.error("Token refresh endpoint error:", error);
return new Response( return new Response(
JSON.stringify({ error: 'Internal server error' }), JSON.stringify({ error: "Internal server error" }),
{ {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
}, },
// GET endpoint to retrieve current tokens from cookies
GET: async ({ request }: { request: Request }) => { GET: async ({ request }: { request: Request }) => {
try { try {
const cookieHeader = request.headers.get('cookie') const cookieHeader = request.headers.get("cookie");
if (!cookieHeader) { if (!cookieHeader) {
return new Response( return new Response(JSON.stringify({ error: "No cookies found" }), {
JSON.stringify({ error: 'No cookies found' }),
{
status: 401, status: 401,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} });
)
} }
const cookies = Object.fromEntries( const cookies = Object.fromEntries(
cookieHeader.split('; ').map((c: string) => c.split('=')) cookieHeader.split("; ").map((c: string) => c.split("="))
) );
const accessToken = cookies.spotify_access_token const accessToken = cookies.spotify_access_token;
const refreshToken = cookies.spotify_refresh_token const refreshToken = cookies.spotify_refresh_token;
if (!accessToken && !refreshToken) { if (!accessToken && !refreshToken) {
return new Response( return new Response(
JSON.stringify({ error: 'No Spotify tokens found' }), JSON.stringify({ error: "No Spotify tokens found" }),
{ {
status: 401, status: 401,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
return new Response( return new Response(
@@ -110,18 +112,20 @@ export const ServerRoute = createServerFileRoute('/api/spotify/token').methods({
}), }),
{ {
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} catch (error) { } catch (error) {
console.error('Get tokens endpoint error:', error) console.error("Get tokens endpoint error:", error);
return new Response( return new Response(
JSON.stringify({ error: 'Internal server error' }), JSON.stringify({ error: "Internal server error" }),
{ {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
} }
) );
} }
}, },
}) },
},
});

View File

@@ -1,116 +1,148 @@
import { createServerFileRoute } from '@tanstack/react-start/server'; import { createFileRoute } from "@tanstack/react-router";
import { superTokensRequestMiddleware } from '@/utils/supertokens'; import { superTokensRequestMiddleware } from "@/utils/supertokens";
import { pbAdmin } from '@/lib/pocketbase/client'; import { pbAdmin } from "@/lib/pocketbase/client";
import { logger } from '@/lib/logger'; import { logger } from "@/lib/logger";
import { z } from 'zod'; import { z } from "zod";
const uploadSchema = z.object({ const uploadSchema = z.object({
teamId: z.string().min(1, 'Team ID is required'), teamId: z.string().min(1, "Team ID is required"),
}); });
export const ServerRoute = createServerFileRoute('/api/teams/upload-logo') export const Route = createFileRoute("/api/teams/upload-logo")({
.middleware([superTokensRequestMiddleware]) server: {
.methods({ middleware: [superTokensRequestMiddleware],
handlers: {
POST: async ({ request, context }) => { POST: async ({ request, context }) => {
try { try {
const userId = context.userAuthId; const userId = context.userAuthId;
const isAdmin = context.roles.includes("Admin"); const isAdmin = context.roles.includes("Admin");
if (!userId) return new Response('Unauthenticated', { status: 401 }); if (!userId) return new Response("Unauthenticated", { status: 401 });
const formData = await request.formData(); const formData = await request.formData();
const teamId = formData.get('teamId') as string; const teamId = formData.get("teamId") as string;
const logoFile = formData.get('logo') as File; const logoFile = formData.get("logo") as File;
const validationResult = uploadSchema.safeParse({ teamId }); const validationResult = uploadSchema.safeParse({ teamId });
if (!validationResult.success) { if (!validationResult.success) {
return new Response(JSON.stringify({ return new Response(
error: 'Invalid input', JSON.stringify({
details: validationResult.error.issues error: "Invalid input",
}), { details: validationResult.error.issues,
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
if (!logoFile || logoFile.size === 0) { if (!logoFile || logoFile.size === 0) {
return new Response(JSON.stringify({ return new Response(
error: 'Logo file is required' JSON.stringify({
}), { error: "Logo file is required",
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']; const allowedTypes = [
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
];
if (!allowedTypes.includes(logoFile.type)) { if (!allowedTypes.includes(logoFile.type)) {
return new Response(JSON.stringify({ return new Response(
error: 'Invalid file type. Only JPEG, PNG and GIF are allowed.' JSON.stringify({
}), { error: "Invalid file type. Only JPEG, PNG and GIF are allowed.",
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
const maxSize = 10 * 1024 * 1024; const maxSize = 10 * 1024 * 1024;
if (logoFile.size > maxSize) { if (logoFile.size > maxSize) {
return new Response(JSON.stringify({ return new Response(
error: 'File too large. Maximum size is 10MB.' JSON.stringify({
}), { error: "File too large. Maximum size is 10MB.",
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
const team = await pbAdmin.getTeam(teamId); const team = await pbAdmin.getTeam(teamId);
if (!team) { if (!team) {
return new Response(JSON.stringify({ return new Response(
error: 'Team not found' JSON.stringify({
}), { error: "Team not found",
}),
{
status: 404, status: 404,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
if (!team.players.map(p => p.id).includes(context.userId) && !isAdmin) const user = await pbAdmin.getPlayerByAuthId(userId);
return new Response('Unauthorized', { status: 403 }); if (!team.players.map((p) => p.id).includes(user?.id!) && !isAdmin)
return new Response("Unauthorized", { status: 403 });
logger.info('Uploading team logo', { logger.info("Uploading team logo", {
teamId, teamId,
fileName: logoFile.name, fileName: logoFile.name,
fileSize: logoFile.size, fileSize: logoFile.size,
userId userId,
}); });
const pbFormData = new FormData(); const pbFormData = new FormData();
pbFormData.append('logo', logoFile); pbFormData.append("logo", logoFile);
const updatedTeam= await pbAdmin.updateTeam(teamId, pbFormData as any); const updatedTeam = await pbAdmin.updateTeam(
logger.info('Team logo uploaded successfully', {
teamId, teamId,
logo: updatedTeam.logo pbFormData as any
);
logger.info("Team logo uploaded successfully", {
teamId,
logo: updatedTeam.logo,
}); });
return new Response(JSON.stringify({ return new Response(
JSON.stringify({
success: true, success: true,
team: updatedTeam, team: updatedTeam,
message: 'Logo uploaded successfully' message: "Logo uploaded successfully",
}), { }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} catch (error: any) { } catch (error: any) {
logger.error('Error uploading team logo:', error); logger.error("Error uploading team logo:", error);
return new Response(JSON.stringify({ return new Response(
error: 'Failed to upload logo', JSON.stringify({
message: error.message || 'Unknown error occurred' error: "Failed to upload logo",
}), { message: error.message || "Unknown error occurred",
}),
{
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
});
} }
);
} }
},
},
},
}); });

View File

@@ -1,115 +1,145 @@
import { createServerFileRoute } from '@tanstack/react-start/server'; import { createFileRoute } from "@tanstack/react-router";
import { superTokensRequestMiddleware } from '@/utils/supertokens'; import { superTokensRequestMiddleware } from "@/utils/supertokens";
import { pbAdmin } from '@/lib/pocketbase/client'; import { pbAdmin } from "@/lib/pocketbase/client";
import { logger } from '@/lib/logger'; import { logger } from "@/lib/logger";
import { z } from 'zod'; import { z } from "zod";
const uploadSchema = z.object({ const uploadSchema = z.object({
tournamentId: z.string().min(1, 'Tournament ID is required'), tournamentId: z.string().min(1, "Tournament ID is required"),
}); });
export const ServerRoute = createServerFileRoute('/api/tournaments/upload-logo') export const Route = createFileRoute("/api/tournaments/upload-logo")({
.middleware([superTokensRequestMiddleware]) server: {
.methods({ middleware: [superTokensRequestMiddleware],
handlers: {
POST: async ({ request, context }) => { POST: async ({ request, context }) => {
try { try {
const userId = context.userAuthId; const userId = context.userAuthId;
const isAdmin = context.roles.includes("Admin"); const isAdmin = context.roles.includes("Admin");
if (!userId) return new Response('Unauthenticated', { status: 401 }); if (!userId) return new Response("Unauthenticated", { status: 401 });
if (!isAdmin) return new Response('Unauthorized', { status: 403 }); if (!isAdmin) return new Response("Unauthorized", { status: 403 });
const formData = await request.formData(); const formData = await request.formData();
const tournamentId = formData.get('tournamentId') as string; const tournamentId = formData.get("tournamentId") as string;
const logoFile = formData.get('logo') as File; const logoFile = formData.get("logo") as File;
const validationResult = uploadSchema.safeParse({ tournamentId }); const validationResult = uploadSchema.safeParse({ tournamentId });
if (!validationResult.success) { if (!validationResult.success) {
return new Response(JSON.stringify({ return new Response(
error: 'Invalid input', JSON.stringify({
details: validationResult.error.issues error: "Invalid input",
}), { details: validationResult.error.issues,
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
if (!logoFile || logoFile.size === 0) { if (!logoFile || logoFile.size === 0) {
return new Response(JSON.stringify({ return new Response(
error: 'Logo file is required' JSON.stringify({
}), { error: "Logo file is required",
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']; const allowedTypes = [
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
];
if (!allowedTypes.includes(logoFile.type)) { if (!allowedTypes.includes(logoFile.type)) {
return new Response(JSON.stringify({ return new Response(
error: 'Invalid file type. Only JPEG, PNG and GIF are allowed.' JSON.stringify({
}), { error: "Invalid file type. Only JPEG, PNG and GIF are allowed.",
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
const maxSize = 10 * 1024 * 1024; const maxSize = 10 * 1024 * 1024;
if (logoFile.size > maxSize) { if (logoFile.size > maxSize) {
return new Response(JSON.stringify({ return new Response(
error: 'File too large. Maximum size is 10MB.' JSON.stringify({
}), { error: "File too large. Maximum size is 10MB.",
}),
{
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
const tournament = await pbAdmin.getTournament(tournamentId); const tournament = await pbAdmin.getTournament(tournamentId);
if (!tournament) { if (!tournament) {
return new Response(JSON.stringify({ return new Response(
error: 'Tournament not found' JSON.stringify({
}), { error: "Tournament not found",
}),
{
status: 404, status: 404,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} }
logger.info("Uploading tournament logo", {
logger.info('Uploading tournament logo', {
tournamentId, tournamentId,
fileName: logoFile.name, fileName: logoFile.name,
fileSize: logoFile.size, fileSize: logoFile.size,
userId userId,
}); });
const pbFormData = new FormData(); const pbFormData = new FormData();
pbFormData.append('logo', logoFile); pbFormData.append("logo", logoFile);
const updatedTournament = await pbAdmin.updateTournament(tournamentId, pbFormData as any); const updatedTournament = await pbAdmin.updateTournament(
logger.info('Tournament logo uploaded successfully', {
tournamentId, tournamentId,
logo: updatedTournament.logo pbFormData as any
);
logger.info("Tournament logo uploaded successfully", {
tournamentId,
logo: updatedTournament.logo,
}); });
return new Response(JSON.stringify({ return new Response(
JSON.stringify({
success: true, success: true,
tournament: updatedTournament, tournament: updatedTournament,
message: 'Logo uploaded successfully' message: "Logo uploaded successfully",
}), { }),
{
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}); }
);
} catch (error: any) { } catch (error: any) {
logger.error('Error uploading tournament logo:', error); logger.error("Error uploading tournament logo:", error);
return new Response(JSON.stringify({ return new Response(
error: 'Failed to upload logo', JSON.stringify({
message: error.message || 'Unknown error occurred' error: "Failed to upload logo",
}), { message: error.message || "Unknown error occurred",
}),
{
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
});
} }
);
} }
},
},
},
}); });

View File

@@ -1,38 +1,33 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { useEffect } from 'react' import { useEffect, useRef } from 'react'
import FullScreenLoader from '@/components/full-screen-loader' import FullScreenLoader from '@/components/full-screen-loader'
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session' import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'
import { resetRefreshFlag } from '@/lib/supertokens/client'
export const Route = createFileRoute('/refresh-session')({ export const Route = createFileRoute('/refresh-session')({
component: RouteComponent, component: RouteComponent,
}) })
// https://supertokens.com/docs/additional-verification/session-verification/ssr?uiType=custom
function RouteComponent() { function RouteComponent() {
const hasAttemptedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (hasAttemptedRef.current) return;
hasAttemptedRef.current = true;
const handleRefresh = async () => { const handleRefresh = async () => {
try { try {
resetRefreshFlag();
const refreshed = await attemptRefreshingSession() const refreshed = await attemptRefreshingSession()
if (refreshed) { if (refreshed) {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const redirect = urlParams.get('redirect') const redirect = urlParams.get('redirect')
const isServerFunction = redirect && ( if (redirect && !redirect.includes('_serverFn') && !redirect.includes('/api/')) {
redirect.startsWith('_serverFn') ||
redirect.startsWith('api/') ||
redirect.includes('_serverFn')
);
if (redirect && !isServerFunction) {
window.location.href = decodeURIComponent(redirect) window.location.href = decodeURIComponent(redirect)
} else { } else {
const referrer = document.referrer; window.location.href = '/';
const referrerUrl = referrer && !referrer.includes('/_serverFn') && !referrer.includes('/api/')
? referrer
: '/';
window.location.href = referrerUrl;
} }
} else { } else {
window.location.href = '/login' window.location.href = '/login'
@@ -42,8 +37,7 @@ function RouteComponent() {
} }
} }
const timeout = setTimeout(handleRefresh, 100) setTimeout(handleRefresh, 100)
return () => clearTimeout(timeout)
}, []) }, [])
return <FullScreenLoader /> return <FullScreenLoader />

View File

@@ -58,13 +58,13 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
const value = useMemo( const value = useMemo(
() => ({ () => ({
user: data?.user || defaultAuthData.user, user: data?.user,
metadata: data?.metadata || defaultAuthData.metadata, metadata: data?.metadata || { accentColor: "blue" as MantineColor, colorScheme: "dark" as MantineColorScheme },
roles: data?.roles || defaultAuthData.roles, roles: data?.roles || [],
phone: data?.phone || "", phone: data?.phone || "",
set, set,
}), }),
[data, defaultAuthData] [data, set]
); );
return <AuthContext value={value}>{children}</AuthContext>; return <AuthContext value={value}>{children}</AuthContext>;

View File

@@ -9,7 +9,7 @@ import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
const logger = new Logger("Bracket Generation"); const logger = new Logger("Bracket Generation");
export const previewBracket = createServerFn() export const previewBracket = createServerFn()
.validator(z.number()) .inputValidator(z.number())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: teams }) => .handler(async ({ data: teams }) =>
toServerResult(async () => { toServerResult(async () => {

View File

@@ -16,7 +16,7 @@ const orderedTeamsSchema = z.object({
}); });
export const generateTournamentBracket = createServerFn() export const generateTournamentBracket = createServerFn()
.validator(orderedTeamsSchema) .inputValidator(orderedTeamsSchema)
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data: { tournamentId, orderedTeamIds } }) => .handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -137,7 +137,7 @@ export const generateTournamentBracket = createServerFn()
); );
export const startMatch = createServerFn() export const startMatch = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -170,7 +170,7 @@ const endMatchSchema = z.object({
ot_count: z.number(), ot_count: z.number(),
}); });
export const endMatch = createServerFn() export const endMatch = createServerFn()
.validator(endMatchSchema) .inputValidator(endMatchSchema)
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) => .handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -252,7 +252,7 @@ const toggleReactionSchema = z.object({
}); });
export const toggleMatchReaction = createServerFn() export const toggleMatchReaction = createServerFn()
.validator(toggleReactionSchema) .inputValidator(toggleReactionSchema)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: { matchId, emoji }, context }) => .handler(async ({ data: { matchId, emoji }, context }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -312,7 +312,7 @@ export interface Reaction {
players: PlayerInfo[]; players: PlayerInfo[];
} }
export const getMatchReactions = createServerFn() export const getMatchReactions = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: matchId, context }) => .handler(async ({ data: matchId, context }) =>
toServerResult(async () => { toServerResult(async () => {

View File

@@ -5,13 +5,13 @@ import { Match } from "@/features/matches/types";
import { pbAdmin } from "@/lib/pocketbase/client"; import { pbAdmin } from "@/lib/pocketbase/client";
import { z } from "zod"; import { z } from "zod";
import { logger } from "."; import { logger } from ".";
import { getWebRequest } from "@tanstack/react-start/server"; import { getRequest } from "@tanstack/react-start/server";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result"; import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
export const fetchMe = createServerFn() export const fetchMe = createServerFn()
.handler(async () => .handler(async () =>
toServerResult(async () => { toServerResult(async () => {
const request = getWebRequest(); const request = getRequest();
try { try {
const context = await getSessionContext(request); const context = await getSessionContext(request);
@@ -25,7 +25,7 @@ export const fetchMe = createServerFn()
phone: context.phone phone: context.phone
}; };
} catch (error: any) { } catch (error: any) {
logger.info("FetchMe: Session error", error) // logger.info("FetchMe: Session error", error)
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
const errorData = error?.response?.data; const errorData = error?.response?.data;
if (errorData?.error === "SESSION_REFRESH_REQUIRED") { if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
@@ -38,14 +38,14 @@ export const fetchMe = createServerFn()
); );
export const getPlayer = createServerFn() export const getPlayer = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult<Player>(async () => await pbAdmin.getPlayer(data)) toServerResult<Player>(async () => await pbAdmin.getPlayer(data))
); );
export const updatePlayer = createServerFn() export const updatePlayer = createServerFn()
.validator(playerUpdateSchema) .inputValidator(playerUpdateSchema)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -72,7 +72,7 @@ export const updatePlayer = createServerFn()
); );
export const createPlayer = createServerFn() export const createPlayer = createServerFn()
.validator(playerInputSchema) .inputValidator(playerInputSchema)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -97,7 +97,7 @@ export const createPlayer = createServerFn()
); );
export const associatePlayer = createServerFn() export const associatePlayer = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -129,7 +129,7 @@ export const getUnassociatedPlayers = createServerFn()
); );
export const getPlayerStats = createServerFn() export const getPlayerStats = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult<PlayerStats>(async () => await pbAdmin.getPlayerStats(data)) toServerResult<PlayerStats>(async () => await pbAdmin.getPlayerStats(data))
@@ -142,14 +142,14 @@ export const getAllPlayerStats = createServerFn()
); );
export const getPlayerMatches = createServerFn() export const getPlayerMatches = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult<Match[]>(async () => await pbAdmin.getPlayerMatches(data)) toServerResult<Match[]>(async () => await pbAdmin.getPlayerMatches(data))
); );
export const getUnenrolledPlayers = createServerFn() export const getUnenrolledPlayers = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: tournamentId }) => .handler(async ({ data: tournamentId }) =>
toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId)) toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId))

View File

@@ -15,21 +15,21 @@ export const listTeamInfos = createServerFn()
); );
export const getTeam = createServerFn() export const getTeam = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: teamId }) => .handler(async ({ data: teamId }) =>
toServerResult(() => pbAdmin.getTeam(teamId)) toServerResult(() => pbAdmin.getTeam(teamId))
); );
export const getTeamInfo = createServerFn() export const getTeamInfo = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: teamId }) => .handler(async ({ data: teamId }) =>
toServerResult(() => pbAdmin.getTeamInfo(teamId)) toServerResult(() => pbAdmin.getTeamInfo(teamId))
); );
export const createTeam = createServerFn() export const createTeam = createServerFn()
.validator(teamInputSchema) .inputValidator(teamInputSchema)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data, context }) => .handler(async ({ data, context }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -46,7 +46,7 @@ export const createTeam = createServerFn()
); );
export const updateTeam = createServerFn() export const updateTeam = createServerFn()
.validator(z.object({ .inputValidator(z.object({
id: z.string(), id: z.string(),
updates: teamUpdateSchema updates: teamUpdateSchema
})) }))
@@ -72,14 +72,14 @@ export const updateTeam = createServerFn()
); );
export const getTeamStats = createServerFn() export const getTeamStats = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: teamId }) => .handler(async ({ data: teamId }) =>
toServerResult(() => pbAdmin.getTeamStats(teamId)) toServerResult(() => pbAdmin.getTeamStats(teamId))
); );
export const getTeamMatches = createServerFn() export const getTeamMatches = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult<Match[]>(async () => await pbAdmin.getTeamMatches(data)) toServerResult<Match[]>(async () => await pbAdmin.getTeamMatches(data))

View File

@@ -13,14 +13,14 @@ export const listTournaments = createServerFn()
); );
export const createTournament = createServerFn() export const createTournament = createServerFn()
.validator(tournamentInputSchema) .inputValidator(tournamentInputSchema)
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) => .handler(async ({ data }) =>
toServerResult(() => pbAdmin.createTournament(data)) toServerResult(() => pbAdmin.createTournament(data))
); );
export const updateTournament = createServerFn() export const updateTournament = createServerFn()
.validator(z.object({ .inputValidator(z.object({
id: z.string(), id: z.string(),
updates: tournamentInputSchema.partial() updates: tournamentInputSchema.partial()
})) }))
@@ -30,7 +30,7 @@ export const updateTournament = createServerFn()
); );
export const getTournament = createServerFn() export const getTournament = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: tournamentId, context }) => { .handler(async ({ data: tournamentId, context }) => {
const isAdmin = context.roles.includes("Admin"); const isAdmin = context.roles.includes("Admin");
@@ -44,7 +44,7 @@ export const getCurrentTournament = createServerFn()
); );
export const enrollTeam = createServerFn() export const enrollTeam = createServerFn()
.validator(z.object({ .inputValidator(z.object({
tournamentId: z.string(), tournamentId: z.string(),
teamId: z.string() teamId: z.string()
})) }))
@@ -70,7 +70,7 @@ export const enrollTeam = createServerFn()
); );
export const unenrollTeam = createServerFn() export const unenrollTeam = createServerFn()
.validator(z.object({ .inputValidator(z.object({
tournamentId: z.string(), tournamentId: z.string(),
teamId: z.string() teamId: z.string()
})) }))
@@ -80,21 +80,21 @@ export const unenrollTeam = createServerFn()
); );
export const getUnenrolledTeams = createServerFn() export const getUnenrolledTeams = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data: tournamentId }) => .handler(async ({ data: tournamentId }) =>
toServerResult(() => pbAdmin.getUnenrolledTeams(tournamentId)) toServerResult(() => pbAdmin.getUnenrolledTeams(tournamentId))
); );
export const getFreeAgents = createServerFn() export const getFreeAgents = createServerFn()
.validator(z.string()) .inputValidator(z.string())
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: tournamentId }) => .handler(async ({ data: tournamentId }) =>
toServerResult(() => pbAdmin.getFreeAgents(tournamentId)) toServerResult(() => pbAdmin.getFreeAgents(tournamentId))
); );
export const enrollFreeAgent = createServerFn() export const enrollFreeAgent = createServerFn()
.validator(z.object({ phone: z.string(), tournamentId: z.string() })) .inputValidator(z.object({ phone: z.string(), tournamentId: z.string() }))
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {
@@ -108,7 +108,7 @@ export const enrollFreeAgent = createServerFn()
); );
export const unenrollFreeAgent = createServerFn() export const unenrollFreeAgent = createServerFn()
.validator(z.object({ tournamentId: z.string() })) .inputValidator(z.object({ tournamentId: z.string() }))
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => .handler(async ({ context, data }) =>
toServerResult(async () => { toServerResult(async () => {

View File

@@ -37,6 +37,7 @@ export function useServerEvents() {
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return;
if (!user?.id) return; if (!user?.id) return;
shouldConnectRef.current = true; shouldConnectRef.current = true;

View File

@@ -46,7 +46,7 @@ class Logger {
constructor(context?: string, options: LoggerOptions = {}) { constructor(context?: string, options: LoggerOptions = {}) {
this.context = context; this.context = context;
this.options = { this.options = {
enabled: process.env.NODE_ENV !== "production", enabled: import.meta.env.NODE_ENV !== "production",
showTimestamp: true, showTimestamp: true,
collapsed: true, collapsed: true,
colors: true, colors: true,

View File

@@ -4,12 +4,19 @@ import { createTournamentsService } from "./services/tournaments";
import { createTeamsService } from "./services/teams"; import { createTeamsService } from "./services/teams";
import { createMatchesService } from "./services/matches"; import { createMatchesService } from "./services/matches";
import { createReactionsService } from "./services/reactions"; import { createReactionsService } from "./services/reactions";
import dotenv from 'dotenv';
dotenv.config();
class PocketBaseAdminClient { class PocketBaseAdminClient {
private pb: PocketBase; private pb: PocketBase;
public authPromise: Promise<void>; public authPromise: Promise<void>;
constructor() { constructor() {
console.log('Environment variables loaded:', {
POCKETBASE_URL: process.env.POCKETBASE_URL,
POCKETBASE_ADMIN_EMAIL: process.env.POCKETBASE_ADMIN_EMAIL,
POCKETBASE_ADMIN_PASSWORD: process.env.POCKETBASE_ADMIN_PASSWORD,
});
this.pb = new PocketBase(process.env.POCKETBASE_URL); this.pb = new PocketBase(process.env.POCKETBASE_URL);
this.pb.beforeSend = (url, options) => { this.pb.beforeSend = (url, options) => {

View File

@@ -4,6 +4,31 @@ import Passwordless from "supertokens-web-js/recipe/passwordless";
import { appInfo } from "./config"; import { appInfo } from "./config";
import { logger } from "./"; import { logger } from "./";
let refreshAttemptCount = 0;
export const resetRefreshFlag = () => {
refreshAttemptCount = 0;
};
const setupFetchInterceptor = () => {
if (typeof window === 'undefined') return;
const originalFetch = window.fetch;
window.fetch = async (resource: RequestInfo | URL, options?: RequestInit) => {
const url = typeof resource === 'string' ? resource :
resource instanceof URL ? resource.toString() : resource.url;
if (url.includes('/api/auth/session/refresh')) {
refreshAttemptCount++;
if (refreshAttemptCount > 1) {
throw new Error('Duplicate refresh attempt blocked');
}
}
return originalFetch.call(window, resource, options);
};
};
export const frontendConfig = () => { export const frontendConfig = () => {
return { return {
appInfo, appInfo,
@@ -12,7 +37,6 @@ export const frontendConfig = () => {
Session.init({ Session.init({
tokenTransferMethod: "cookie", tokenTransferMethod: "cookie",
sessionTokenBackendDomain: undefined, sessionTokenBackendDomain: undefined,
preAPIHook: async (context) => { preAPIHook: async (context) => {
context.requestInit.credentials = "include"; context.requestInit.credentials = "include";
return context; return context;
@@ -23,16 +47,14 @@ export const frontendConfig = () => {
}; };
let initialized = false; let initialized = false;
export function ensureSuperTokensFrontend() { export function ensureSuperTokensFrontend() {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
if (!initialized) { if (!initialized) {
setupFetchInterceptor();
SuperTokens.init(frontendConfig()); SuperTokens.init(frontendConfig());
initialized = true; initialized = true;
logger.info("Initialized"); logger.info("SuperTokens initialized");
Session.doesSessionExist().then((exists) => {
logger.info(`Session does${exists ? "" : "NOT"} exist on load!`);
});
} }
} }

View File

@@ -6,6 +6,7 @@ import UserRoles from "supertokens-node/recipe/userroles";
import { appInfo } from "./config"; import { appInfo } from "./config";
import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode"; import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode";
import { logger } from "./"; import { logger } from "./";
import passwordlessTwilioVerify from "./recipes/passwordless-twilio-verify";
export const backendConfig = (): TypeInput => { export const backendConfig = (): TypeInput => {
return { return {
@@ -19,10 +20,10 @@ export const backendConfig = (): TypeInput => {
PasswordlessDevelopmentMode.init(), PasswordlessDevelopmentMode.init(),
Session.init({ Session.init({
cookieSameSite: "lax", cookieSameSite: "lax",
cookieSecure: process.env.NODE_ENV === "production", cookieSecure: import.meta.env.NODE_ENV === "production",
cookieDomain: cookieDomain:
process.env.NODE_ENV === "production" ? ".example.com" : undefined, import.meta.env.NODE_ENV === "production" ? ".example.com" : undefined,
antiCsrf: process.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE", antiCsrf: import.meta.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE",
// Debug only // Debug only
exposeAccessTokenToFrontendInCookieBasedAuth: true, exposeAccessTokenToFrontendInCookieBasedAuth: true,
@@ -30,13 +31,13 @@ export const backendConfig = (): TypeInput => {
Dashboard.init(), Dashboard.init(),
UserRoles.init(), UserRoles.init(),
], ],
telemetry: process.env.NODE_ENV !== "production", telemetry: import.meta.env.NODE_ENV !== "production",
}; };
}; };
let initialized = false; let initialized = false;
export function ensureSuperTokensBackend() { export function ensureSuperTokensBackend() {
if (!initialized) { if (!initialized && typeof window === 'undefined') {
SuperTokens.init(backendConfig()); SuperTokens.init(backendConfig());
initialized = true; initialized = true;
logger.simple("Backend initialized"); logger.simple("Backend initialized");

View File

@@ -23,15 +23,15 @@ export function useOptimisticMutation<TData, TVariables = unknown>(
return { previousData }; return { previousData };
}, },
onError: (error, variables, context) => { onError: (error, variables, onMutateResult, context) => {
if (context && typeof context === 'object' && 'previousData' in context && context.previousData) { if (context && typeof context === 'object' && 'previousData' in context && context.previousData) {
queryClient.setQueryData(queryKey, context.previousData); queryClient.setQueryData(queryKey, context.previousData);
} }
mutationOptions.onError?.(error, variables, context); mutationOptions.onError?.(error, variables, onMutateResult, context);
}, },
onSettled: (data, error, variables, context) => { onSettled: (data, error, variables, onMutateResult, context) => {
queryClient.invalidateQueries({ queryKey }); queryClient.invalidateQueries({ queryKey });
mutationOptions.onSettled?.(data, error, variables, context); mutationOptions.onSettled?.(data, error, variables, onMutateResult, context);
} }
}); });
} }

View File

@@ -2,6 +2,8 @@ import { useMutation, UseMutationOptions } from "@tanstack/react-query";
import { ServerResult } from "../types"; import { ServerResult } from "../types";
import toast from '@/lib/sonner' import toast from '@/lib/sonner'
let isMutationRefreshingSession = false;
export function useServerMutation<TData, TVariables = unknown>( export function useServerMutation<TData, TVariables = unknown>(
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & { options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>; mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
@@ -42,8 +44,14 @@ export function useServerMutation<TData, TVariables = unknown>(
: error.response.data; : error.response.data;
if (errorData?.error === "SESSION_REFRESH_REQUIRED") { if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
if (!isMutationRefreshingSession) {
isMutationRefreshingSession = true;
const currentUrl = window.location.pathname + window.location.search; const currentUrl = window.location.pathname + window.location.search;
setTimeout(() => {
isMutationRefreshingSession = false;
}, 1000);
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`; window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
}
throw new Error("SESSION_REFRESH_REQUIRED"); throw new Error("SESSION_REFRESH_REQUIRED");
} }
} catch (parseError) {} } catch (parseError) {}
@@ -52,14 +60,14 @@ export function useServerMutation<TData, TVariables = unknown>(
throw error; throw error;
} }
}, },
onSuccess: (data, variables, context) => { onSuccess: (data, variables, onMutateResult, context) => {
if (showSuccessToast && successMessage) { if (showSuccessToast && successMessage) {
toast.success(successMessage); toast.success(successMessage);
} }
onSuccess?.(data, variables, context); onSuccess?.(data, variables, onMutateResult, context);
}, },
onError: (error, variables, context) => { onError: (error, variables, onMutateResult, context) => {
onError?.(error, variables, context); onError?.(error, variables, onMutateResult, context);
} }
}); });
} }

View File

@@ -1,13 +1,23 @@
import twilio from "twilio"; import type { Twilio } from "twilio";
const accountSid = process.env.TWILIO_ACCOUNT_SID!; const accountSid = process.env.TWILIO_ACCOUNT_SID!;
const authToken = process.env.TWILIO_AUTH_TOKEN!; const authToken = process.env.TWILIO_AUTH_TOKEN!;
const serviceSid = process.env.TWILIO_SERVICE_SID!; const serviceSid = process.env.TWILIO_SERVICE_SID!;
const client = twilio(accountSid, authToken); let client: Twilio;
function getTwilioClient() {
if (!client) {
const twilio = require("twilio");
client = twilio(accountSid, authToken);
}
return client;
}
export async function sendVerifyCode(phoneNumber: string, code: string) { export async function sendVerifyCode(phoneNumber: string, code: string) {
const verification = await client.verify.v2 const twilioClient = getTwilioClient();
const verification = await twilioClient!.verify.v2
.services(serviceSid) .services(serviceSid)
.verifications.create({ .verifications.create({
channel: "sms", channel: "sms",
@@ -23,7 +33,9 @@ export async function sendVerifyCode(phoneNumber: string, code: string) {
} }
export async function updateVerify(sid: string) { export async function updateVerify(sid: string) {
const verification = await client.verify.v2 const twilioClient = getTwilioClient();
const verification = await twilioClient!.verify.v2
.services(serviceSid) .services(serviceSid)
.verifications(sid) .verifications(sid)
.update({ status: "approved" }); .update({ status: "approved" });

View File

@@ -1,49 +1,26 @@
import { import {
createMiddleware, createMiddleware,
createServerFn, createServerFn,
ServerFnResponseType, createServerOnlyFn,
} from "@tanstack/react-start"; } from "@tanstack/react-start";
import { getWebRequest } from "@tanstack/react-start/server"; import { getRequest, setResponseHeader } from "@tanstack/react-start/server";
import { redirect as redirect } from "@tanstack/react-router"; import { redirect as redirect } from "@tanstack/react-router";
import UserRoles from "supertokens-node/recipe/userroles"; import UserRoles from "supertokens-node/recipe/userroles";
import UserMetadata from "supertokens-node/recipe/usermetadata"; import UserMetadata from "supertokens-node/recipe/usermetadata";
import { getSessionForStart } from "@/lib/supertokens/recipes/start-session"; import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
import { Logger } from "@/lib/logger"; import { Logger } from "@/lib/logger";
import z from "zod"; import z from "zod";
import { refreshSession } from "supertokens-node/recipe/session";
const logger = new Logger("Middleware"); const logger = new Logger("Middleware");
export const verifySuperTokensSession = async ( const verifySuperTokensSession = async (
request: Request, request: Request
response?: ServerFnResponseType
) => { ) => {
let session = await getSessionForStart(request, { sessionRequired: false }); let session = await getSessionForStart(request, { sessionRequired: false });
if (session?.needsRefresh) { if (session?.needsRefresh) {
logger.info("Session needs refresh"); logger.info("Session needs refresh - redirecting to client");
try {
if (response) {
const refreshedSession = await refreshSession(request, response);
if (refreshedSession) {
session = await getSessionForStart(request, { sessionRequired: false });
}
}
if (session?.needsRefresh) {
return { context: { session: { tryRefresh: true } } }; return { context: { session: { tryRefresh: true } } };
} }
} catch (error: any) {
logger.error("Session refresh error", error);
if (error.type === 'UNAUTHORISED' || error.type === 'TOKEN_THEFT_DETECTED') {
return { context: { userAuthId: null, roles: [] } };
}
return { context: { session: { tryRefresh: true } } };
}
}
const userAuthId = session?.userId; const userAuthId = session?.userId;
@@ -69,7 +46,7 @@ export const verifySuperTokensSession = async (
}; };
}; };
export const getSessionContext = async (request: Request, options?: { isServerFunction?: boolean }) => { export const getSessionContext = createServerOnlyFn(async (request: Request, options?: { isServerFunction?: boolean }) => {
const session = await verifySuperTokensSession(request); const session = await verifySuperTokensSession(request);
if (session.context.session?.tryRefresh) { if (session.context.session?.tryRefresh) {
@@ -98,7 +75,7 @@ export const getSessionContext = async (request: Request, options?: { isServerFu
}; };
return context; return context;
}; });
export const superTokensRequestMiddleware = createMiddleware({ export const superTokensRequestMiddleware = createMiddleware({
type: "request", type: "request",
@@ -109,8 +86,8 @@ export const superTokensRequestMiddleware = createMiddleware({
export const superTokensFunctionMiddleware = createMiddleware({ export const superTokensFunctionMiddleware = createMiddleware({
type: "function", type: "function",
}).server(async ({ next, response }) => { }).server(async ({ next }) => {
const request = getWebRequest(); const request = getRequest();
try { try {
const context = await getSessionContext(request, { isServerFunction: true }); const context = await getSessionContext(request, { isServerFunction: true });
@@ -135,7 +112,7 @@ export const superTokensFunctionMiddleware = createMiddleware({
export const superTokensAdminFunctionMiddleware = createMiddleware({ export const superTokensAdminFunctionMiddleware = createMiddleware({
type: "function", type: "function",
}).server(async ({ next }) => { }).server(async ({ next }) => {
const request = getWebRequest(); const request = getRequest();
try { try {
const context = await getSessionContext(request, { isServerFunction: true }); const context = await getSessionContext(request, { isServerFunction: true });
@@ -169,7 +146,7 @@ export const fetchUserRoles = async (userAuthId: string) => {
}; };
export const setUserMetadata = createServerFn({ method: "POST" }) export const setUserMetadata = createServerFn({ method: "POST" })
.validator( .inputValidator(
z z
.object({ .object({
first_name: z first_name: z
@@ -212,7 +189,7 @@ export const setUserMetadata = createServerFn({ method: "POST" })
}); });
export const updateUserColorScheme = createServerFn({ method: "POST" }) export const updateUserColorScheme = createServerFn({ method: "POST" })
.validator((data: string) => data) .inputValidator((data: string) => data)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => { .handler(async ({ context, data }) => {
const { userAuthId, metadata } = context; const { userAuthId, metadata } = context;
@@ -231,7 +208,7 @@ export const updateUserColorScheme = createServerFn({ method: "POST" })
}); });
export const updateUserAccentColor = createServerFn({ method: "POST" }) export const updateUserAccentColor = createServerFn({ method: "POST" })
.validator((data: string) => data) .inputValidator((data: string) => data)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => { .handler(async ({ context, data }) => {
const { userAuthId, metadata } = context; const { userAuthId, metadata } = context;

View File

@@ -6,16 +6,14 @@ import react from '@vitejs/plugin-react';
export default defineConfig({ export default defineConfig({
server: { server: {
port: 3000, port: 3000,
allowedHosts: ["dev.flexxon.app"]
}, },
plugins: [ plugins: [
tsConfigPaths({ tsConfigPaths({
projects: ['./tsconfig.json'], projects: ['./tsconfig.json'],
}), }),
tanstackStart({ tanstackStart({
customViteReactPlugin: true,
tsr: {
srcDirectory: 'src/app', srcDirectory: 'src/app',
},
}), }),
react() react()
] ]