From e4164cbc719414df32e89ecb0ae53626845d0e05 Mon Sep 17 00:00:00 2001 From: yohlo Date: Wed, 24 Sep 2025 00:13:41 -0500 Subject: [PATCH] attempted upgrade --- package.json | 8 +- src/app/routeTree.gen.ts | 507 +++++++++--------- src/app/router.tsx | 4 +- src/app/routes/api/auth.$.ts | 22 +- src/app/routes/api/events.$.ts | 116 ++-- .../api/files/$collection/$recordId/$file.ts | 190 ++++--- src/app/routes/api/spotify/callback.ts | 239 +++++---- src/app/routes/api/spotify/capture.ts | 103 ++-- src/app/routes/api/spotify/playback.ts | 363 ++++++------- src/app/routes/api/spotify/resume.ts | 125 +++-- src/app/routes/api/spotify/search.ts | 120 +++-- src/app/routes/api/spotify/token.ts | 234 ++++---- src/app/routes/api/teams/upload-logo.ts | 242 +++++---- src/app/routes/api/tournaments/upload-logo.ts | 238 ++++---- src/features/admin/components/admin-page.tsx | 2 +- src/features/bracket/server.ts | 2 +- src/features/matches/server.ts | 10 +- src/features/players/server.ts | 20 +- src/features/teams/server.ts | 12 +- src/features/tournaments/server.ts | 18 +- src/lib/logger/index.ts | 2 +- src/lib/pocketbase/client.ts | 7 + src/lib/supertokens/server.ts | 13 +- src/lib/twilio/index.ts | 28 +- src/utils/supertokens.ts | 32 +- vite.config.ts | 6 +- 26 files changed, 1390 insertions(+), 1273 deletions(-) diff --git a/package.json b/package.json index d684658..a1caf07 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev --host 0.0.0.0", "build": "vite build && tsc --noEmit", - "start": "vite start" + "start": "vite start --host 0.0.0.0" }, "dependencies": { "@hello-pangea/dnd": "^18.0.1", @@ -24,12 +24,13 @@ "@tanstack/react-router": "^1.130.12", "@tanstack/react-router-devtools": "^1.130.13", "@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", "@tiptap/pm": "^3.4.3", "@tiptap/react": "^3.4.3", "@tiptap/starter-kit": "^3.4.3", "@types/ioredis": "^4.28.10", + "dotenv": "^17.2.2", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.12", "ioredis": "^5.7.0", @@ -51,6 +52,7 @@ "zustand": "^5.0.7" }, "devDependencies": { + "@tanstack/router-plugin": "^1.132.2", "@types/node": "^22.5.4", "@types/pg": "^8.15.5", "@types/react": "^19.0.8", @@ -63,7 +65,7 @@ "postcss-simple-vars": "^7.0.1", "tsx": "^4.20.3", "typescript": "^5.7.2", - "vite": "^6.3.5", + "vite": "^7.1.7", "vite-tsconfig-paths": "^5.1.4" } } diff --git a/src/app/routeTree.gen.ts b/src/app/routeTree.gen.ts index c9c8758..27b5b1f 100644 --- a/src/app/routeTree.gen.ts +++ b/src/app/routeTree.gen.ts @@ -8,8 +8,6 @@ // 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. -import { createServerRootRoute } from '@tanstack/react-start/server' - import { Route as rootRouteImport } from './routes/__root' import { Route as RefreshSessionRouteImport } from './routes/refresh-session' 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 AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/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 AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId' 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 AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket' 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 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({ id: '/refresh-session', @@ -93,6 +89,57 @@ const AuthedAdminIndexRoute = AuthedAdminIndexRouteImport.update({ path: '/', getParentRoute: () => AuthedAdminRoute, } 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 = AuthedTournamentsTournamentIdRouteImport.update({ id: '/tournaments/$tournamentId', @@ -132,6 +179,12 @@ const AuthedAdminTournamentsIdIndexRoute = path: '/tournaments/$id/', getParentRoute: () => AuthedAdminRoute, } as any) +const ApiFilesCollectionRecordIdFileRoute = + ApiFilesCollectionRecordIdFileRouteImport.update({ + id: '/api/files/$collection/$recordId/$file', + path: '/api/files/$collection/$recordId/$file', + getParentRoute: () => rootRouteImport, + } as any) const AuthedAdminTournamentsRunIdRoute = AuthedAdminTournamentsRunIdRouteImport.update({ id: '/tournaments/run/$id', @@ -144,66 +197,6 @@ const AuthedAdminTournamentsIdTeamsRoute = path: '/tournaments/$id/teams', getParentRoute: () => AuthedAdminRoute, } 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 { '/login': typeof LoginRoute @@ -217,12 +210,23 @@ export interface FileRoutesByFullPath { '/profile/$playerId': typeof AuthedProfilePlayerIdRoute '/teams/$teamId': typeof AuthedTeamsTeamIdRoute '/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 '/tournaments': typeof AuthedTournamentsIndexRoute '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute '/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute + '/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute } export interface FileRoutesByTo { @@ -236,12 +240,23 @@ export interface FileRoutesByTo { '/profile/$playerId': typeof AuthedProfilePlayerIdRoute '/teams/$teamId': typeof AuthedTeamsTeamIdRoute '/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 '/tournaments': typeof AuthedTournamentsIndexRoute '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute '/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute + '/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute } export interface FileRoutesById { @@ -258,12 +273,23 @@ export interface FileRoutesById { '/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute '/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute '/_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/tournaments/': typeof AuthedTournamentsIndexRoute '/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute '/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute + '/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute '/_authed/admin/tournaments/$id/': typeof AuthedAdminTournamentsIdIndexRoute } export interface FileRouteTypes { @@ -280,12 +306,23 @@ export interface FileRouteTypes { | '/profile/$playerId' | '/teams/$teamId' | '/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/' | '/tournaments' | '/tournaments/$id/bracket' | '/admin/tournaments' | '/admin/tournaments/$id/teams' | '/admin/tournaments/run/$id' + | '/api/files/$collection/$recordId/$file' | '/admin/tournaments/$id' fileRoutesByTo: FileRoutesByTo to: @@ -299,12 +336,23 @@ export interface FileRouteTypes { | '/profile/$playerId' | '/teams/$teamId' | '/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' | '/tournaments' | '/tournaments/$id/bracket' | '/admin/tournaments' | '/admin/tournaments/$id/teams' | '/admin/tournaments/run/$id' + | '/api/files/$collection/$recordId/$file' | '/admin/tournaments/$id' id: | '__root__' @@ -320,12 +368,23 @@ export interface FileRouteTypes { | '/_authed/profile/$playerId' | '/_authed/teams/$teamId' | '/_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/tournaments/' | '/_authed/tournaments/$id/bracket' | '/_authed/admin/tournaments/' | '/_authed/admin/tournaments/$id/teams' | '/_authed/admin/tournaments/run/$id' + | '/api/files/$collection/$recordId/$file' | '/_authed/admin/tournaments/$id/' fileRoutesById: FileRoutesById } @@ -334,101 +393,17 @@ export interface RootRouteChildren { LoginRoute: typeof LoginRoute LogoutRoute: typeof LogoutRoute RefreshSessionRoute: typeof RefreshSessionRoute -} -export interface FileServerRoutesByFullPath { - '/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 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 + ApiAuthSplatRoute: typeof ApiAuthSplatRoute + ApiEventsSplatRoute: typeof ApiEventsSplatRoute + ApiSpotifyCallbackRoute: typeof ApiSpotifyCallbackRoute + ApiSpotifyCaptureRoute: typeof ApiSpotifyCaptureRoute + ApiSpotifyPlaybackRoute: typeof ApiSpotifyPlaybackRoute + ApiSpotifyResumeRoute: typeof ApiSpotifyResumeRoute + ApiSpotifySearchRoute: typeof ApiSpotifySearchRoute + ApiSpotifyTokenRoute: typeof ApiSpotifyTokenRoute + ApiTeamsUploadLogoRoute: typeof ApiTeamsUploadLogoRoute + ApiTournamentsUploadLogoRoute: typeof ApiTournamentsUploadLogoRoute + ApiFilesCollectionRecordIdFileRoute: typeof ApiFilesCollectionRecordIdFileRoute } declare module '@tanstack/react-router' { @@ -503,6 +478,76 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedAdminIndexRouteImport 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': { id: '/_authed/tournaments/$tournamentId' path: '/tournaments/$tournamentId' @@ -552,6 +597,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedAdminTournamentsIdIndexRouteImport 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': { id: '/_authed/admin/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 { AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute @@ -704,24 +675,26 @@ const rootRouteChildren: RootRouteChildren = { LoginRoute: LoginRoute, LogoutRoute: LogoutRoute, 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 ._addFileChildren(rootRouteChildren) ._addFileTypes() -const rootServerRouteChildren: RootServerRouteChildren = { - ApiAuthSplatServerRoute: ApiAuthSplatServerRoute, - ApiEventsSplatServerRoute: ApiEventsSplatServerRoute, - ApiSpotifyCallbackServerRoute: ApiSpotifyCallbackServerRoute, - ApiSpotifyCaptureServerRoute: ApiSpotifyCaptureServerRoute, - ApiSpotifyPlaybackServerRoute: ApiSpotifyPlaybackServerRoute, - ApiSpotifyResumeServerRoute: ApiSpotifyResumeServerRoute, - ApiSpotifySearchServerRoute: ApiSpotifySearchServerRoute, - ApiSpotifyTokenServerRoute: ApiSpotifyTokenServerRoute, - ApiTeamsUploadLogoServerRoute: ApiTeamsUploadLogoServerRoute, - ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute, - ApiFilesCollectionRecordIdFileServerRoute: - ApiFilesCollectionRecordIdFileServerRoute, + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + router: Awaited> + } } -export const serverRouteTree = rootServerRouteImport - ._addFileChildren(rootServerRouteChildren) - ._addFileTypes() diff --git a/src/app/router.tsx b/src/app/router.tsx index c8a557a..0138f21 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -5,7 +5,7 @@ import { routeTree } from "./routeTree.gen"; import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary"; import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config"; -export function createRouter() { +export function getRouter() { const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -40,6 +40,6 @@ export function createRouter() { declare module "@tanstack/react-router" { interface Register { - router: ReturnType; + router: ReturnType; } } diff --git a/src/app/routes/api/auth.$.ts b/src/app/routes/api/auth.$.ts index 6d7eff7..3b617e2 100644 --- a/src/app/routes/api/auth.$.ts +++ b/src/app/routes/api/auth.$.ts @@ -1,5 +1,5 @@ // 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 { ensureSuperTokensBackend } from '@/lib/supertokens/server' @@ -12,12 +12,16 @@ const handleRequest = async ({ request }: {request: Request}) => { console.log("Handling auth request:", request.method, request.url); return superTokensHandler(request); }; -export const ServerRoute = createServerFileRoute('/api/auth/$').methods({ - GET: handleRequest, - POST: handleRequest, - PUT: handleRequest, - DELETE: handleRequest, - PATCH: handleRequest, - OPTIONS: handleRequest, - HEAD: handleRequest, +export const Route = createFileRoute('/api/auth/$')({ + server: { + handlers: { + GET: handleRequest, + POST: handleRequest, + PUT: handleRequest, + DELETE: handleRequest, + PATCH: handleRequest, + OPTIONS: handleRequest, + HEAD: handleRequest, + } + } }) diff --git a/src/app/routes/api/events.$.ts b/src/app/routes/api/events.$.ts index 74381ef..30fb9c6 100644 --- a/src/app/routes/api/events.$.ts +++ b/src/app/routes/api/events.$.ts @@ -1,66 +1,74 @@ -import { createServerFileRoute } from "@tanstack/react-start/server"; +import { createFileRoute } from "@tanstack/react-router"; import { serverEvents, type ServerEvent } from "@/lib/events/emitter"; import { logger } from "@/lib/logger"; import { superTokensRequestMiddleware } from "@/utils/supertokens"; -export const ServerRoute = createServerFileRoute("/api/events/$").middleware([superTokensRequestMiddleware]).methods({ - GET: ({ request, context }) => { - logger.info('ServerEvents | New connection', context?.userAuthId); - - const stream = new ReadableStream({ - start(controller) { - const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`; - controller.enqueue(new TextEncoder().encode(connectMessage)); +export const Route = createFileRoute("/api/events/$")({ + server: { + middleware: [superTokensRequestMiddleware], + handlers: { + GET: ({ request, context }) => { + logger.info("ServerEvents | New connection", context?.userAuthId); - const handleEvent = (event: ServerEvent) => { - logger.info('ServerEvents | Event received', event); - const message = `data: ${JSON.stringify(event)}\n\n`; - try { - controller.enqueue(new TextEncoder().encode(message)); - } catch (error) { - logger.error("ServerEvents | Error sending SSE message", error); - } - }; + const stream = new ReadableStream({ + start(controller) { + const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`; + controller.enqueue(new TextEncoder().encode(connectMessage)); - serverEvents.on("test", handleEvent); - serverEvents.on("match", handleEvent); - serverEvents.on("reaction", handleEvent); + const handleEvent = (event: ServerEvent) => { + logger.info("ServerEvents | Event received", event); + const message = `data: ${JSON.stringify(event)}\n\n`; + try { + controller.enqueue(new TextEncoder().encode(message)); + } catch (error) { + logger.error("ServerEvents | Error sending SSE message", error); + } + }; - const pingInterval = setInterval(() => { - try { - const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`; - controller.enqueue(new TextEncoder().encode(pingMessage)); - } catch (e) { - clearInterval(pingInterval); - controller.close(); - } - }, 30000); + serverEvents.on("test", handleEvent); + serverEvents.on("match", handleEvent); + serverEvents.on("reaction", handleEvent); - const cleanup = () => { - serverEvents.off("test", handleEvent); - clearInterval(pingInterval); - try { - logger.info('ServerEvents | Closing connection', context?.userAuthId); - controller.close(); - } catch (e) { - logger.error('ServerEvents | Error closing controller', e); - } - }; + const pingInterval = setInterval(() => { + try { + const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`; + controller.enqueue(new TextEncoder().encode(pingMessage)); + } catch (e) { + clearInterval(pingInterval); + controller.close(); + } + }, 30000); - request.signal?.addEventListener("abort", cleanup); + const cleanup = () => { + serverEvents.off("test", handleEvent); + clearInterval(pingInterval); + try { + logger.info( + "ServerEvents | Closing connection", + context?.userAuthId + ); + controller.close(); + } catch (e) { + logger.error("ServerEvents | Error closing controller", e); + } + }; - return cleanup; + request.signal?.addEventListener("abort", cleanup); + + return cleanup; + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Cache-Control", + }, + }); }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "Cache-Control", - }, - }); + }, }, -}); \ No newline at end of file +}); diff --git a/src/app/routes/api/files/$collection/$recordId/$file.ts b/src/app/routes/api/files/$collection/$recordId/$file.ts index e058f42..76376ef 100644 --- a/src/app/routes/api/files/$collection/$recordId/$file.ts +++ b/src/app/routes/api/files/$collection/$recordId/$file.ts @@ -1,95 +1,113 @@ -import { createServerFileRoute } from "@tanstack/react-start/server"; +import { createFileRoute } from "@tanstack/react-router"; import { logger } from "@/lib/logger"; -export const ServerRoute = createServerFileRoute("/api/files/$collection/$recordId/$file").methods({ - GET: async ({ params, request }) => { - try { - const { collection, recordId, file } = params; - const pocketbaseUrl = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090'; - const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`; - - logger.info('File proxy', { - collection, - recordId, - file, - targetUrl: fileUrl - }); +export const Route = createFileRoute( + "/api/files/$collection/$recordId/$file" +)({ + server: { + handlers: { + GET: async ({ params, request }) => { + try { + const { collection, recordId, file } = params; + const pocketbaseUrl = + import.meta.env.POCKETBASE_URL || "http://127.0.0.1:8090"; + const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`; - const response = await fetch(fileUrl, { - method: 'GET', - headers: { - ...(request.headers.get('range') && { 'Range': request.headers.get('range')! }), - ...(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')! }), - }, - }); + logger.info("File proxy", { + collection, + recordId, + file, + targetUrl: fileUrl, + }); - if (!response.ok) { - logger.error('PocketBase file request failed', { - status: response.status, - statusText: response.statusText, - url: fileUrl - }); + const response = await fetch(fileUrl, { + method: "GET", + headers: { + ...(request.headers.get("range") && { + Range: request.headers.get("range")!, + }), + ...(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.status === 404) { - return new Response('File not found', { status: 404 }); + if (!response.ok) { + logger.error("PocketBase file request failed", { + status: response.status, + statusText: response.statusText, + url: fileUrl, + }); + + if (response.status === 404) { + return new Response("File not found", { status: 404 }); + } + + return new Response(`PocketBase error: ${response.statusText}`, { + status: response.status, + }); + } + + const body = response.body; + const responseHeaders = new Headers(); + const headers = [ + "content-type", + "content-length", + "content-disposition", + "etag", + "last-modified", + "cache-control", + "accept-ranges", + "content-range", + ]; + + headers.forEach((header) => { + const value = response.headers.get(header); + if (value) { + responseHeaders.set(header, value); + } + }); + + responseHeaders.set("Access-Control-Allow-Origin", "*"); + responseHeaders.set( + "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", { + status: response.status, + contentType: response.headers.get("content-type"), + contentLength: response.headers.get("content-length"), + }); + + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error) { + logger.error("File proxy error", error); + return new Response("Internal server error", { status: 500 }); } + }, - return new Response(`PocketBase error: ${response.statusText}`, { - status: response.status + OPTIONS: () => { + return new Response(null, { + status: 200, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Max-Age": "86400", + }, }); - } - - const body = response.body; - const responseHeaders = new Headers(); - const headers = [ - 'content-type', - 'content-length', - 'content-disposition', - 'etag', - 'last-modified', - 'cache-control', - 'accept-ranges', - 'content-range' - ]; - - headers.forEach(header => { - const value = response.headers.get(header); - if (value) { - responseHeaders.set(header, value); - } - }); - - responseHeaders.set('Access-Control-Allow-Origin', '*'); - responseHeaders.set('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', { - status: response.status, - contentType: response.headers.get('content-type'), - contentLength: response.headers.get('content-length') - }); - - return new Response(body, { - status: response.status, - statusText: response.statusText, - headers: responseHeaders - }); - - } catch (error) { - logger.error('File proxy error', error); - return new Response('Internal server error', { status: 500 }); - } + }, + }, }, - - OPTIONS: () => { - return new Response(null, { - status: 200, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Max-Age': '86400', - } - }); - } -}); \ No newline at end of file +}); diff --git a/src/app/routes/api/spotify/callback.ts b/src/app/routes/api/spotify/callback.ts index 0fbcaae..0b728d8 100644 --- a/src/app/routes/api/spotify/callback.ts +++ b/src/app/routes/api/spotify/callback.ts @@ -1,127 +1,146 @@ -import { createServerFileRoute } from '@tanstack/react-start/server' -import { SpotifyAuth } from '@/lib/spotify/auth' +import { createFileRoute } from "@tanstack/react-router"; +import { SpotifyAuth } from "@/lib/spotify/auth"; -const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID! -const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET! -const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI! +const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!; +const SPOTIFY_CLIENT_SECRET = import.meta.env.SPOTIFY_CLIENT_SECRET!; +const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI!; -export const ServerRoute = createServerFileRoute('/api/spotify/callback').methods({ - GET: async ({ request }: { request: Request }) => { - const getReturnPath = (state: string | null): string => { - if (!state) return '/'; - try { - const decodedState = JSON.parse(atob(state)); - return decodedState.returnPath || '/'; - } catch { - return '/'; - } - }; +export const Route = createFileRoute("/api/spotify/callback")({ + server: { + handlers: { + GET: async ({ request }: { request: Request }) => { + const getReturnPath = (state: string | null): string => { + if (!state) return "/"; + try { + const decodedState = JSON.parse(atob(state)); + return decodedState.returnPath || "/"; + } catch { + return "/"; + } + }; - try { - const url = new URL(request.url) - const code = url.searchParams.get('code') - const state = url.searchParams.get('state') - const error = url.searchParams.get('error') + try { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + const error = url.searchParams.get("error"); - const returnPath = getReturnPath(state); + const returnPath = getReturnPath(state); - if (error) { - console.error('Spotify OAuth error:', error) - return new Response(null, { - status: 302, - headers: { - 'Location': returnPath + '?spotify_error=' + encodeURIComponent(error), - }, - }) - } + if (error) { + console.error("Spotify OAuth error:", error); + return new Response(null, { + status: 302, + headers: { + Location: + returnPath + "?spotify_error=" + encodeURIComponent(error), + }, + }); + } - if (!code || !state) { - console.error('Missing code or state:', { code: !!code, state: !!state }) - return new Response(null, { - status: 302, - headers: { - 'Location': returnPath + '?spotify_error=missing_code_or_state', - }, - }) - } + if (!code || !state) { + console.error("Missing code or state:", { + code: !!code, + state: !!state, + }); + return new Response(null, { + status: 302, + headers: { + Location: returnPath + "?spotify_error=missing_code_or_state", + }, + }); + } - console.log('Token exchange attempt:', { - client_id: SPOTIFY_CLIENT_ID, - redirect_uri: SPOTIFY_REDIRECT_URI, - has_code: !!code, - has_state: !!state, - }) + console.log("Token exchange attempt:", { + client_id: SPOTIFY_CLIENT_ID, + redirect_uri: SPOTIFY_REDIRECT_URI, + has_code: !!code, + has_state: !!state, + }); - const tokenResponse = await fetch('https://accounts.spotify.com/api/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`, - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code, - redirect_uri: SPOTIFY_REDIRECT_URI, - }), - }) + const tokenResponse = await fetch( + "https://accounts.spotify.com/api/token", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64")}`, + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: SPOTIFY_REDIRECT_URI, + }), + } + ); - if (!tokenResponse.ok) { - const errorText = await tokenResponse.text() - console.error('Token exchange error:', { - status: tokenResponse.status, - statusText: tokenResponse.statusText, - body: errorText, - redirect_uri: SPOTIFY_REDIRECT_URI, - }) - - const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`) - return new Response(null, { - status: 302, - headers: { - 'Location': `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`, - }, - }) - } + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text(); + console.error("Token exchange error:", { + status: tokenResponse.status, + statusText: tokenResponse.statusText, + body: errorText, + redirect_uri: SPOTIFY_REDIRECT_URI, + }); - const tokens = await tokenResponse.json() + const errorParam = encodeURIComponent( + `${tokenResponse.status}: ${errorText}` + ); + return new Response(null, { + status: 302, + headers: { + Location: `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`, + }, + }); + } - console.log('Token exchange successful:', { - has_access_token: !!tokens.access_token, - has_refresh_token: !!tokens.refresh_token, - expires_in: tokens.expires_in, - }) + const tokens = await tokenResponse.json(); - console.log('Decoded return path:', returnPath); + console.log("Token exchange successful:", { + has_access_token: !!tokens.access_token, + has_refresh_token: !!tokens.refresh_token, + expires_in: tokens.expires_in, + }); - const response = new Response(null, { - status: 302, - headers: { - 'Location': returnPath + '?spotify_auth=success', - }, - }) + console.log("Decoded return path:", returnPath); - const isSecure = process.env.NODE_ENV === 'production' - const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}` + const response = new Response(null, { + status: 302, + headers: { + Location: returnPath + "?spotify_auth=success", + }, + }); - response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`) - - if (tokens.refresh_token) { - 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}`) - } + const isSecure = import.meta.env.NODE_ENV === "production"; + const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`; - return response - } catch (error) { - console.error('Spotify callback error:', error) - const url = new URL(request.url); - const state = url.searchParams.get('state'); - const returnPath = getReturnPath(state); - return new Response(null, { - status: 302, - headers: { - 'Location': returnPath + '?spotify_error=callback_failed', - }, - }) - } + response.headers.append( + "Set-Cookie", + `spotify_access_token=${tokens.access_token}; ${cookieOptions}` + ); + + if (tokens.refresh_token) { + 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}` + ); + } + + return response; + } catch (error) { + console.error("Spotify callback error:", error); + const url = new URL(request.url); + const state = url.searchParams.get("state"); + const returnPath = getReturnPath(state); + return new Response(null, { + status: 302, + headers: { + Location: returnPath + "?spotify_error=callback_failed", + }, + }); + } + }, + }, }, -}) \ No newline at end of file +}); diff --git a/src/app/routes/api/spotify/capture.ts b/src/app/routes/api/spotify/capture.ts index 7952277..4a8b85b 100644 --- a/src/app/routes/api/spotify/capture.ts +++ b/src/app/routes/api/spotify/capture.ts @@ -1,59 +1,60 @@ -import { createServerFileRoute } from '@tanstack/react-start/server' -import { SpotifyWebApiClient } from '@/lib/spotify/client' -import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types' +import { createFileRoute } from "@tanstack/react-router"; +import { SpotifyWebApiClient } from "@/lib/spotify/client"; +import type { SpotifyPlaybackSnapshot } from "@/lib/spotify/types"; -export const ServerRoute = createServerFileRoute('/api/spotify/capture').methods({ - POST: async ({ request }: { request: Request }) => { - try { - // Get access token from cookies - const cookies = request.headers.get('Cookie') || '' - const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/) - - if (!accessTokenMatch) { - return new Response( - JSON.stringify({ error: 'No access token found' }), - { - status: 401, - headers: { 'Content-Type': 'application/json' } +export const Route = createFileRoute("/api/spotify/capture")({ + server: { + handlers: { + POST: async ({ request }: { request: Request }) => { + try { + const cookies = request.headers.get("Cookie") || ""; + const accessTokenMatch = cookies.match( + /spotify_access_token=([^;]+)/ + ); + + if (!accessTokenMatch) { + return new Response( + JSON.stringify({ error: "No access token found" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } - const accessToken = decodeURIComponent(accessTokenMatch[1]) - const spotifyClient = new SpotifyWebApiClient(accessToken) + const accessToken = decodeURIComponent(accessTokenMatch[1]); + const spotifyClient = new SpotifyWebApiClient(accessToken); - // Create a snapshot of the current playback state - const snapshot = await spotifyClient.createPlaybackSnapshot() - - if (!snapshot) { - return new Response( - JSON.stringify({ error: 'No active playback to capture' }), - { - status: 400, - headers: { 'Content-Type': 'application/json' } + const snapshot = await spotifyClient.createPlaybackSnapshot(); + + if (!snapshot) { + return new Response( + JSON.stringify({ error: "No active playback to capture" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } - return new Response( - JSON.stringify({ snapshot }), - { - status: 200, - headers: { 'Content-Type': 'application/json' } + return new Response(JSON.stringify({ snapshot }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Spotify capture error:", error); + + const errorMessage = + error instanceof Error + ? error.message + : "Failed to capture playback state"; + + return new Response(JSON.stringify({ error: errorMessage }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); } - ) - } catch (error) { - console.error('Spotify capture error:', error) - - const errorMessage = error instanceof Error ? error.message : 'Failed to capture playback state' - - return new Response( - JSON.stringify({ error: errorMessage }), - { - status: 500, - headers: { 'Content-Type': 'application/json' } - } - ) - } + }, + }, }, -}) \ No newline at end of file +}); diff --git a/src/app/routes/api/spotify/playback.ts b/src/app/routes/api/spotify/playback.ts index ccb45b2..2cccf34 100644 --- a/src/app/routes/api/spotify/playback.ts +++ b/src/app/routes/api/spotify/playback.ts @@ -1,202 +1,203 @@ -import { createServerFileRoute } from '@tanstack/react-start/server' -import { SpotifyWebApiClient } from '@/lib/spotify/client' +import { createFileRoute } from "@tanstack/react-router"; +import { SpotifyWebApiClient } from "@/lib/spotify/client"; function getAccessTokenFromCookies(request: Request): string | null { - const cookieHeader = request.headers.get('cookie') - if (!cookieHeader) return null + const cookieHeader = request.headers.get("cookie"); + if (!cookieHeader) return null; 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({ - POST: async ({ request }: { request: Request }) => { - try { - const accessToken = getAccessTokenFromCookies(request) - if (!accessToken) { - return new Response( - JSON.stringify({ error: 'No access token found' }), - { - status: 401, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - - const body = await request.json() - const { action, deviceId, volumePercent, trackId, positionMs } = body - - const spotifyClient = new SpotifyWebApiClient(accessToken) - - switch (action) { - case 'play': - await spotifyClient.play(deviceId) - break - case 'playTrack': - if (!trackId) { +export const Route = createFileRoute("/api/spotify/playback")({ + server: { + handlers: { + POST: async ({ request }: { request: Request }) => { + try { + const accessToken = getAccessTokenFromCookies(request); + if (!accessToken) { return new Response( - JSON.stringify({ error: 'trackId is required for playTrack action' }), + JSON.stringify({ error: "No access token found" }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, + status: 401, + headers: { "Content-Type": "application/json" }, } - ) + ); } - await spotifyClient.playTrack(trackId, deviceId, positionMs) - break - case 'pause': - await spotifyClient.pause() - break - case 'next': - await spotifyClient.skipToNext() - break - case 'previous': - await spotifyClient.skipToPrevious() - break - case 'volume': - if (typeof volumePercent !== 'number') { + + const body = await request.json(); + const { action, deviceId, volumePercent, trackId, positionMs } = body; + + const spotifyClient = new SpotifyWebApiClient(accessToken); + + switch (action) { + case "play": + await spotifyClient.play(deviceId); + break; + case "playTrack": + if (!trackId) { + return new Response( + JSON.stringify({ + error: "trackId is required for playTrack action", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + await spotifyClient.playTrack(trackId, deviceId, positionMs); + break; + case "pause": + await spotifyClient.pause(); + break; + case "next": + await spotifyClient.skipToNext(); + break; + case "previous": + await spotifyClient.skipToPrevious(); + break; + case "volume": + if (typeof volumePercent !== "number") { + return new Response( + JSON.stringify({ error: "volumePercent must be a number" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + await spotifyClient.setVolume(volumePercent); + break; + case "transfer": + if (!deviceId) { + return new Response( + JSON.stringify({ + error: "deviceId is required for transfer action", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + await spotifyClient.transferPlayback(deviceId); + break; + default: + return new Response(JSON.stringify({ error: "Invalid action" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Playback control error:", error); + + if (error instanceof Error) { + if (error.message.includes("NO_ACTIVE_DEVICE")) { + return new Response( + JSON.stringify({ + error: + "No active device found. Please select a device first.", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + if (error.message.includes("PREMIUM_REQUIRED")) { + return new Response( + JSON.stringify({ + error: "Spotify Premium is required for playback control.", + }), + { + status: 403, + headers: { "Content-Type": "application/json" }, + } + ); + } + + console.error("Full error details:", { + message: error.message, + stack: error.stack, + name: error.name, + }); + } + + return new Response( + JSON.stringify({ + error: "Playback control failed", + details: error instanceof Error ? error.message : "Unknown error", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); + } + }, + + GET: async ({ request }: { request: Request }) => { + try { + const accessToken = getAccessTokenFromCookies(request); + if (!accessToken) { return new Response( - JSON.stringify({ error: 'volumePercent must be a number' }), + JSON.stringify({ error: "No access token found" }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, + status: 401, + headers: { "Content-Type": "application/json" }, } - ) + ); } - await spotifyClient.setVolume(volumePercent) - break - case 'transfer': - if (!deviceId) { - return new Response( - JSON.stringify({ error: 'deviceId is required for transfer action' }), - { - status: 400, - headers: { 'Content-Type': 'application/json' }, - } - ) + + const url = new URL(request.url); + const type = url.searchParams.get("type"); + + const spotifyClient = new SpotifyWebApiClient(accessToken); + + if (type === "devices") { + const devices = await spotifyClient.getDevices(); + return new Response(JSON.stringify({ devices }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } else if (type === "state") { + const playbackState = await spotifyClient.getPlaybackState(); + return new Response(JSON.stringify({ playbackState }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } else { + const [devices, playbackState] = await Promise.all([ + spotifyClient.getDevices(), + spotifyClient.getPlaybackState(), + ]); + + return new Response(JSON.stringify({ devices, playbackState }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); } - await spotifyClient.transferPlayback(deviceId) - break - default: + } catch (error) { + console.error("Get playback data error:", error); return new Response( - JSON.stringify({ error: 'Invalid action' }), + JSON.stringify({ error: "Failed to get playback data" }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, + status: 500, + headers: { "Content-Type": "application/json" }, } - ) - } - - return new Response( - JSON.stringify({ success: true }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, + ); } - ) - } catch (error) { - console.error('Playback control error:', error) - - if (error instanceof Error) { - if (error.message.includes('NO_ACTIVE_DEVICE')) { - return new Response( - JSON.stringify({ error: 'No active device found. Please select a device first.' }), - { - status: 400, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - - if (error.message.includes('PREMIUM_REQUIRED')) { - return new Response( - JSON.stringify({ error: 'Spotify Premium is required for playback control.' }), - { - status: 403, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - - console.error('Full error details:', { - message: error.message, - stack: error.stack, - name: error.name, - }) - } - - return new Response( - JSON.stringify({ error: 'Playback control failed', details: error instanceof Error ? error.message : 'Unknown error' }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } + }, + }, }, - - GET: async ({ request }: { request: Request }) => { - try { - const accessToken = getAccessTokenFromCookies(request) - if (!accessToken) { - return new Response( - JSON.stringify({ error: 'No access token found' }), - { - status: 401, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - - const url = new URL(request.url) - const type = url.searchParams.get('type') - - const spotifyClient = new SpotifyWebApiClient(accessToken) - - if (type === 'devices') { - const devices = await spotifyClient.getDevices() - return new Response( - JSON.stringify({ devices }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, - } - ) - } else if (type === 'state') { - const playbackState = await spotifyClient.getPlaybackState() - return new Response( - JSON.stringify({ playbackState }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, - } - ) - } else { - const [devices, playbackState] = await Promise.all([ - spotifyClient.getDevices(), - spotifyClient.getPlaybackState(), - ]) - - return new Response( - JSON.stringify({ devices, playbackState }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - } catch (error) { - console.error('Get playback data error:', error) - return new Response( - JSON.stringify({ error: 'Failed to get playback data' }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - }, -}) \ No newline at end of file +}); diff --git a/src/app/routes/api/spotify/resume.ts b/src/app/routes/api/spotify/resume.ts index 38646be..046c4f8 100644 --- a/src/app/routes/api/spotify/resume.ts +++ b/src/app/routes/api/spotify/resume.ts @@ -1,72 +1,71 @@ -import { createServerFileRoute } from '@tanstack/react-start/server' -import { SpotifyWebApiClient } from '@/lib/spotify/client' -import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types' +import { createFileRoute } from "@tanstack/react-router"; +import { SpotifyWebApiClient } from "@/lib/spotify/client"; +import type { SpotifyPlaybackSnapshot } from "@/lib/spotify/types"; -export const ServerRoute = createServerFileRoute('/api/spotify/resume').methods({ - POST: async ({ request }: { request: Request }) => { - try { - // Get access token from cookies - const cookies = request.headers.get('Cookie') || '' - const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/) - - if (!accessTokenMatch) { - return new Response( - JSON.stringify({ error: 'No access token found' }), - { - status: 401, - headers: { 'Content-Type': 'application/json' } +export const Route = createFileRoute("/api/spotify/resume")({ + server: { + handlers: { + POST: async ({ request }: { request: Request }) => { + try { + const cookies = request.headers.get("Cookie") || ""; + const accessTokenMatch = cookies.match( + /spotify_access_token=([^;]+)/ + ); + + if (!accessTokenMatch) { + return new Response( + JSON.stringify({ error: "No access token found" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } - const accessToken = decodeURIComponent(accessTokenMatch[1]) - const spotifyClient = new SpotifyWebApiClient(accessToken) + const accessToken = decodeURIComponent(accessTokenMatch[1]); + const spotifyClient = new SpotifyWebApiClient(accessToken); - // Parse the request body to get the snapshot - const body = await request.json() - const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot } - - if (!snapshot) { - return new Response( - JSON.stringify({ error: 'No snapshot provided' }), - { - status: 400, - headers: { 'Content-Type': 'application/json' } + const body = await request.json(); + const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot }; + + if (!snapshot) { + return new Response( + JSON.stringify({ error: "No snapshot provided" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } - // Restore the playback state from the snapshot - await spotifyClient.restorePlaybackSnapshot(snapshot) + await spotifyClient.restorePlaybackSnapshot(snapshot); - return new Response( - JSON.stringify({ success: true }), - { - status: 200, - headers: { 'Content-Type': 'application/json' } + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Spotify resume error:", error); + + let errorMessage = "Failed to resume playback state"; + + if (error instanceof Error) { + if ( + error.message.includes("Premium") || + error.message.includes("403") + ) { + errorMessage = "Spotify premium required"; + } else { + errorMessage = error.message; + } + } + + return new Response(JSON.stringify({ error: errorMessage }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); } - ) - } catch (error) { - console.error('Spotify resume error:', error) - - let errorMessage = 'Failed to resume playback state' - - // Handle common Spotify Premium requirement error - if (error instanceof Error) { - if (error.message.includes('Premium') || error.message.includes('403')) { - errorMessage = 'Spotify Premium required for playback control' - } else { - errorMessage = error.message - } - } - - return new Response( - JSON.stringify({ error: errorMessage }), - { - status: 500, - headers: { 'Content-Type': 'application/json' } - } - ) - } + }, + }, }, -}) \ No newline at end of file +}); diff --git a/src/app/routes/api/spotify/search.ts b/src/app/routes/api/spotify/search.ts index 8b988b2..518a4c6 100644 --- a/src/app/routes/api/spotify/search.ts +++ b/src/app/routes/api/spotify/search.ts @@ -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 { - const clientId = process.env.VITE_SPOTIFY_CLIENT_ID - const clientSecret = process.env.SPOTIFY_CLIENT_SECRET + const clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID; + const clientSecret = import.meta.env.SPOTIFY_CLIENT_SECRET; 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', { - method: 'POST', + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`, }, - body: 'grant_type=client_credentials', - }) + body: "grant_type=client_credentials", + }); 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() - return data.access_token + const data = await response.json(); + return data.access_token; } -export const ServerRoute = createServerFileRoute('/api/spotify/search').methods({ - GET: async ({ request }: { request: Request }) => { - try { - const url = new URL(request.url) - const query = url.searchParams.get('q') +export const Route = createFileRoute("/api/spotify/search")({ + server: { + handlers: { + GET: async ({ request }: { request: Request }) => { + try { + const url = new URL(request.url); + const query = url.searchParams.get("q"); - if (!query) { - return new Response( - JSON.stringify({ error: 'Query parameter q is required' }), - { - status: 400, - headers: { 'Content-Type': 'application/json' }, + if (!query) { + return new Response( + JSON.stringify({ error: "Query parameter q is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } - // Get client credentials access token - const accessToken = await getClientCredentialsToken() + // Get client credentials access token + const accessToken = await getClientCredentialsToken(); - // Search using Spotify API directly - const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20` + // Search using Spotify API directly + const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`; - const searchResponse = await fetch(searchUrl, { - headers: { - 'Authorization': `Bearer ${accessToken}`, - }, - }) + const searchResponse = await fetch(searchUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); - if (!searchResponse.ok) { - throw new Error('Spotify search request failed') - } + if (!searchResponse.ok) { + throw new Error("Spotify search request failed"); + } - const searchResult = await searchResponse.json() + const searchResult = await searchResponse.json(); - return new Response( - JSON.stringify({ tracks: searchResult.tracks.items }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, + return new Response( + JSON.stringify({ tracks: searchResult.tracks.items }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Search error:", error); + return new Response( + JSON.stringify({ + error: "Search failed", + details: error instanceof Error ? error.message : "Unknown error", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } catch (error) { - console.error('Search error:', error) - return new Response( - JSON.stringify({ error: 'Search failed', details: error instanceof Error ? error.message : 'Unknown error' }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } + }, + }, }, -}) \ No newline at end of file +}); diff --git a/src/app/routes/api/spotify/token.ts b/src/app/routes/api/spotify/token.ts index 49b7aae..7160965 100644 --- a/src/app/routes/api/spotify/token.ts +++ b/src/app/routes/api/spotify/token.ts @@ -1,127 +1,131 @@ -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_SECRET = process.env.SPOTIFY_CLIENT_SECRET! +const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!; +const SPOTIFY_CLIENT_SECRET = import.meta.env.SPOTIFY_CLIENT_SECRET!; -export const ServerRoute = createServerFileRoute('/api/spotify/token').methods({ - POST: async ({ request }: { request: Request }) => { - try { - const body = await request.json() - const { refresh_token } = body +export const Route = createFileRoute("/api/spotify/token")({ + server: { + handlers: { + POST: async ({ request }: { request: Request }) => { + try { + const body = await request.json(); + const { refresh_token } = body; - if (!refresh_token) { - return new Response( - JSON.stringify({ error: 'refresh_token is required' }), - { - status: 400, - headers: { 'Content-Type': 'application/json' }, + if (!refresh_token) { + return new Response( + JSON.stringify({ error: "refresh_token is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } - // Refresh access token - const tokenResponse = await fetch('https://accounts.spotify.com/api/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`, - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token, - }), - }) + const tokenResponse = await fetch( + "https://accounts.spotify.com/api/token", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64")}`, + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token, + }), + } + ); - if (!tokenResponse.ok) { - const error = await tokenResponse.json() - console.error('Token refresh error:', error) - return new Response( - JSON.stringify({ error: 'Failed to refresh token', details: error }), - { - status: tokenResponse.status, - headers: { 'Content-Type': 'application/json' }, + if (!tokenResponse.ok) { + const error = await tokenResponse.json(); + console.error("Token refresh error:", error); + return new Response( + JSON.stringify({ + error: "Failed to refresh token", + details: error, + }), + { + status: tokenResponse.status, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } - const tokens = await tokenResponse.json() + const tokens = await tokenResponse.json(); - // Return new tokens - return new Response( - JSON.stringify({ - access_token: tokens.access_token, - expires_in: tokens.expires_in, - scope: tokens.scope, - token_type: tokens.token_type, - }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, + return new Response( + JSON.stringify({ + access_token: tokens.access_token, + expires_in: tokens.expires_in, + scope: tokens.scope, + token_type: tokens.token_type, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Token refresh endpoint error:", error); + return new Response( + JSON.stringify({ error: "Internal server error" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } catch (error) { - console.error('Token refresh endpoint error:', error) - return new Response( - JSON.stringify({ error: 'Internal server error' }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, + }, + + GET: async ({ request }: { request: Request }) => { + try { + const cookieHeader = request.headers.get("cookie"); + if (!cookieHeader) { + return new Response(JSON.stringify({ error: "No cookies found" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + const cookies = Object.fromEntries( + cookieHeader.split("; ").map((c: string) => c.split("=")) + ); + + const accessToken = cookies.spotify_access_token; + const refreshToken = cookies.spotify_refresh_token; + + if (!accessToken && !refreshToken) { + return new Response( + JSON.stringify({ error: "No Spotify tokens found" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + } + ); + } + + return new Response( + JSON.stringify({ + access_token: accessToken || null, + refresh_token: refreshToken || null, + has_tokens: !!(accessToken || refreshToken), + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Get tokens endpoint error:", error); + return new Response( + JSON.stringify({ error: "Internal server error" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); } - ) - } + }, + }, }, - - // GET endpoint to retrieve current tokens from cookies - GET: async ({ request }: { request: Request }) => { - try { - const cookieHeader = request.headers.get('cookie') - if (!cookieHeader) { - return new Response( - JSON.stringify({ error: 'No cookies found' }), - { - status: 401, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - - const cookies = Object.fromEntries( - cookieHeader.split('; ').map((c: string) => c.split('=')) - ) - - const accessToken = cookies.spotify_access_token - const refreshToken = cookies.spotify_refresh_token - - if (!accessToken && !refreshToken) { - return new Response( - JSON.stringify({ error: 'No Spotify tokens found' }), - { - status: 401, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - - return new Response( - JSON.stringify({ - access_token: accessToken || null, - refresh_token: refreshToken || null, - has_tokens: !!(accessToken || refreshToken), - }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, - } - ) - } catch (error) { - console.error('Get tokens endpoint error:', error) - return new Response( - JSON.stringify({ error: 'Internal server error' }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - }, -}) \ No newline at end of file +}); diff --git a/src/app/routes/api/teams/upload-logo.ts b/src/app/routes/api/teams/upload-logo.ts index cc4be4d..2c6a662 100644 --- a/src/app/routes/api/teams/upload-logo.ts +++ b/src/app/routes/api/teams/upload-logo.ts @@ -1,116 +1,148 @@ -import { createServerFileRoute } from '@tanstack/react-start/server'; -import { superTokensRequestMiddleware } from '@/utils/supertokens'; -import { pbAdmin } from '@/lib/pocketbase/client'; -import { logger } from '@/lib/logger'; -import { z } from 'zod'; +import { createFileRoute } from "@tanstack/react-router"; +import { superTokensRequestMiddleware } from "@/utils/supertokens"; +import { pbAdmin } from "@/lib/pocketbase/client"; +import { logger } from "@/lib/logger"; +import { z } from "zod"; 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') - .middleware([superTokensRequestMiddleware]) - .methods({ - POST: async ({ request, context }) => { - try { - const userId = context.userAuthId; - const isAdmin = context.roles.includes("Admin"); +export const Route = createFileRoute("/api/teams/upload-logo")({ + server: { + middleware: [superTokensRequestMiddleware], + handlers: { + POST: async ({ request, context }) => { + try { + const userId = context.userAuthId; + 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 teamId = formData.get('teamId') as string; - const logoFile = formData.get('logo') as File; + const formData = await request.formData(); + const teamId = formData.get("teamId") as string; + const logoFile = formData.get("logo") as File; - const validationResult = uploadSchema.safeParse({ teamId }); - if (!validationResult.success) { - return new Response(JSON.stringify({ - error: 'Invalid input', - details: validationResult.error.issues - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } + const validationResult = uploadSchema.safeParse({ teamId }); + if (!validationResult.success) { + return new Response( + JSON.stringify({ + error: "Invalid input", + details: validationResult.error.issues, + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + if (!logoFile || logoFile.size === 0) { + return new Response( + JSON.stringify({ + error: "Logo file is required", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const allowedTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + ]; + if (!allowedTypes.includes(logoFile.type)) { + return new Response( + JSON.stringify({ + error: "Invalid file type. Only JPEG, PNG and GIF are allowed.", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const maxSize = 10 * 1024 * 1024; + if (logoFile.size > maxSize) { + return new Response( + JSON.stringify({ + error: "File too large. Maximum size is 10MB.", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const team = await pbAdmin.getTeam(teamId); + if (!team) { + return new Response( + JSON.stringify({ + error: "Team not found", + }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const user = await pbAdmin.getPlayerByAuthId(userId); + if (!team.players.map((p) => p.id).includes(user?.id!) && !isAdmin) + return new Response("Unauthorized", { status: 403 }); + + logger.info("Uploading team logo", { + teamId, + fileName: logoFile.name, + fileSize: logoFile.size, + userId, }); - } - if (!logoFile || logoFile.size === 0) { - return new Response(JSON.stringify({ - error: 'Logo file is required' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } + const pbFormData = new FormData(); + pbFormData.append("logo", logoFile); + + const updatedTeam = await pbAdmin.updateTeam( + teamId, + pbFormData as any + ); + + logger.info("Team logo uploaded successfully", { + teamId, + logo: updatedTeam.logo, }); + + return new Response( + JSON.stringify({ + success: true, + team: updatedTeam, + message: "Logo uploaded successfully", + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error: any) { + logger.error("Error uploading team logo:", error); + + return new Response( + JSON.stringify({ + error: "Failed to upload logo", + message: error.message || "Unknown error occurred", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); } - - const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']; - if (!allowedTypes.includes(logoFile.type)) { - return new Response(JSON.stringify({ - error: 'Invalid file type. Only JPEG, PNG and GIF are allowed.' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } - }); - } - - const maxSize = 10 * 1024 * 1024; - if (logoFile.size > maxSize) { - return new Response(JSON.stringify({ - error: 'File too large. Maximum size is 10MB.' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } - }); - } - - const team = await pbAdmin.getTeam(teamId); - if (!team) { - return new Response(JSON.stringify({ - error: 'Team not found' - }), { - status: 404, - headers: { 'Content-Type': 'application/json' } - }); - } - - if (!team.players.map(p => p.id).includes(context.userId) && !isAdmin) - return new Response('Unauthorized', { status: 403 }); - - logger.info('Uploading team logo', { - teamId, - fileName: logoFile.name, - fileSize: logoFile.size, - userId - }); - - const pbFormData = new FormData(); - pbFormData.append('logo', logoFile); - - const updatedTeam= await pbAdmin.updateTeam(teamId, pbFormData as any); - - logger.info('Team logo uploaded successfully', { - teamId, - logo: updatedTeam.logo - }); - - return new Response(JSON.stringify({ - success: true, - team: updatedTeam, - message: 'Logo uploaded successfully' - }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - - } catch (error: any) { - logger.error('Error uploading team logo:', error); - - return new Response(JSON.stringify({ - error: 'Failed to upload logo', - message: error.message || 'Unknown error occurred' - }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } - } - }); \ No newline at end of file + }, + }, + }, +}); diff --git a/src/app/routes/api/tournaments/upload-logo.ts b/src/app/routes/api/tournaments/upload-logo.ts index 40d45bf..b0f60dc 100644 --- a/src/app/routes/api/tournaments/upload-logo.ts +++ b/src/app/routes/api/tournaments/upload-logo.ts @@ -1,115 +1,145 @@ -import { createServerFileRoute } from '@tanstack/react-start/server'; -import { superTokensRequestMiddleware } from '@/utils/supertokens'; -import { pbAdmin } from '@/lib/pocketbase/client'; -import { logger } from '@/lib/logger'; -import { z } from 'zod'; +import { createFileRoute } from "@tanstack/react-router"; +import { superTokensRequestMiddleware } from "@/utils/supertokens"; +import { pbAdmin } from "@/lib/pocketbase/client"; +import { logger } from "@/lib/logger"; +import { z } from "zod"; 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') - .middleware([superTokensRequestMiddleware]) - .methods({ - POST: async ({ request, context }) => { - try { - const userId = context.userAuthId; - const isAdmin = context.roles.includes("Admin"); +export const Route = createFileRoute("/api/tournaments/upload-logo")({ + server: { + middleware: [superTokensRequestMiddleware], + handlers: { + POST: async ({ request, context }) => { + try { + const userId = context.userAuthId; + const isAdmin = context.roles.includes("Admin"); - if (!userId) return new Response('Unauthenticated', { status: 401 }); - if (!isAdmin) return new Response('Unauthorized', { status: 403 }); + if (!userId) return new Response("Unauthenticated", { status: 401 }); + if (!isAdmin) return new Response("Unauthorized", { status: 403 }); - const formData = await request.formData(); - const tournamentId = formData.get('tournamentId') as string; - const logoFile = formData.get('logo') as File; + const formData = await request.formData(); + const tournamentId = formData.get("tournamentId") as string; + const logoFile = formData.get("logo") as File; - const validationResult = uploadSchema.safeParse({ tournamentId }); - if (!validationResult.success) { - return new Response(JSON.stringify({ - error: 'Invalid input', - details: validationResult.error.issues - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } + const validationResult = uploadSchema.safeParse({ tournamentId }); + if (!validationResult.success) { + return new Response( + JSON.stringify({ + error: "Invalid input", + details: validationResult.error.issues, + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + if (!logoFile || logoFile.size === 0) { + return new Response( + JSON.stringify({ + error: "Logo file is required", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const allowedTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + ]; + if (!allowedTypes.includes(logoFile.type)) { + return new Response( + JSON.stringify({ + error: "Invalid file type. Only JPEG, PNG and GIF are allowed.", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const maxSize = 10 * 1024 * 1024; + if (logoFile.size > maxSize) { + return new Response( + JSON.stringify({ + error: "File too large. Maximum size is 10MB.", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const tournament = await pbAdmin.getTournament(tournamentId); + if (!tournament) { + return new Response( + JSON.stringify({ + error: "Tournament not found", + }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + } + + logger.info("Uploading tournament logo", { + tournamentId, + fileName: logoFile.name, + fileSize: logoFile.size, + userId, }); - } - if (!logoFile || logoFile.size === 0) { - return new Response(JSON.stringify({ - error: 'Logo file is required' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } + const pbFormData = new FormData(); + pbFormData.append("logo", logoFile); + + const updatedTournament = await pbAdmin.updateTournament( + tournamentId, + pbFormData as any + ); + + logger.info("Tournament logo uploaded successfully", { + tournamentId, + logo: updatedTournament.logo, }); + + return new Response( + JSON.stringify({ + success: true, + tournament: updatedTournament, + message: "Logo uploaded successfully", + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error: any) { + logger.error("Error uploading tournament logo:", error); + + return new Response( + JSON.stringify({ + error: "Failed to upload logo", + message: error.message || "Unknown error occurred", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); } - - const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']; - if (!allowedTypes.includes(logoFile.type)) { - return new Response(JSON.stringify({ - error: 'Invalid file type. Only JPEG, PNG and GIF are allowed.' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } - }); - } - - const maxSize = 10 * 1024 * 1024; - if (logoFile.size > maxSize) { - return new Response(JSON.stringify({ - error: 'File too large. Maximum size is 10MB.' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' } - }); - } - - const tournament = await pbAdmin.getTournament(tournamentId); - if (!tournament) { - return new Response(JSON.stringify({ - error: 'Tournament not found' - }), { - status: 404, - headers: { 'Content-Type': 'application/json' } - }); - } - - - logger.info('Uploading tournament logo', { - tournamentId, - fileName: logoFile.name, - fileSize: logoFile.size, - userId - }); - - const pbFormData = new FormData(); - pbFormData.append('logo', logoFile); - - const updatedTournament = await pbAdmin.updateTournament(tournamentId, pbFormData as any); - - logger.info('Tournament logo uploaded successfully', { - tournamentId, - logo: updatedTournament.logo - }); - - return new Response(JSON.stringify({ - success: true, - tournament: updatedTournament, - message: 'Logo uploaded successfully' - }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - - } catch (error: any) { - logger.error('Error uploading tournament logo:', error); - - return new Response(JSON.stringify({ - error: 'Failed to upload logo', - message: error.message || 'Unknown error occurred' - }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } - } - }); \ No newline at end of file + }, + }, + }, +}); diff --git a/src/features/admin/components/admin-page.tsx b/src/features/admin/components/admin-page.tsx index bb5eb67..0adf0a4 100644 --- a/src/features/admin/components/admin-page.tsx +++ b/src/features/admin/components/admin-page.tsx @@ -19,7 +19,7 @@ const AdminPage = () => { label="Open Pocketbase" Icon={DatabaseIcon} onClick={() => - window.location.replace(process.env.POCKETBASE_URL! + "/_/") + window.location.replace(import.meta.env.POCKETBASE_URL! + "/_/") } /> toServerResult(async () => { diff --git a/src/features/matches/server.ts b/src/features/matches/server.ts index 9ba39f2..2e4eb37 100644 --- a/src/features/matches/server.ts +++ b/src/features/matches/server.ts @@ -16,7 +16,7 @@ const orderedTeamsSchema = z.object({ }); export const generateTournamentBracket = createServerFn() - .validator(orderedTeamsSchema) + .inputValidator(orderedTeamsSchema) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data: { tournamentId, orderedTeamIds } }) => toServerResult(async () => { @@ -137,7 +137,7 @@ export const generateTournamentBracket = createServerFn() ); export const startMatch = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data }) => toServerResult(async () => { @@ -170,7 +170,7 @@ const endMatchSchema = z.object({ ot_count: z.number(), }); export const endMatch = createServerFn() - .validator(endMatchSchema) + .inputValidator(endMatchSchema) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) => toServerResult(async () => { @@ -252,7 +252,7 @@ const toggleReactionSchema = z.object({ }); export const toggleMatchReaction = createServerFn() - .validator(toggleReactionSchema) + .inputValidator(toggleReactionSchema) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: { matchId, emoji }, context }) => toServerResult(async () => { @@ -312,7 +312,7 @@ export interface Reaction { players: PlayerInfo[]; } export const getMatchReactions = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: matchId, context }) => toServerResult(async () => { diff --git a/src/features/players/server.ts b/src/features/players/server.ts index 6d95b0b..686b786 100644 --- a/src/features/players/server.ts +++ b/src/features/players/server.ts @@ -5,13 +5,13 @@ import { Match } from "@/features/matches/types"; import { pbAdmin } from "@/lib/pocketbase/client"; import { z } from "zod"; 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"; export const fetchMe = createServerFn() .handler(async () => toServerResult(async () => { - const request = getWebRequest(); + const request = getRequest(); try { const context = await getSessionContext(request); @@ -25,7 +25,7 @@ export const fetchMe = createServerFn() phone: context.phone }; } catch (error: any) { - logger.info("FetchMe: Session error", error) + // logger.info("FetchMe: Session error", error) if (error?.response?.status === 401) { const errorData = error?.response?.data; if (errorData?.error === "SESSION_REFRESH_REQUIRED") { @@ -38,14 +38,14 @@ export const fetchMe = createServerFn() ); export const getPlayer = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data }) => toServerResult(async () => await pbAdmin.getPlayer(data)) ); export const updatePlayer = createServerFn() - .validator(playerUpdateSchema) + .inputValidator(playerUpdateSchema) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => toServerResult(async () => { @@ -72,7 +72,7 @@ export const updatePlayer = createServerFn() ); export const createPlayer = createServerFn() - .validator(playerInputSchema) + .inputValidator(playerInputSchema) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => toServerResult(async () => { @@ -97,7 +97,7 @@ export const createPlayer = createServerFn() ); export const associatePlayer = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => toServerResult(async () => { @@ -129,7 +129,7 @@ export const getUnassociatedPlayers = createServerFn() ); export const getPlayerStats = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data }) => toServerResult(async () => await pbAdmin.getPlayerStats(data)) @@ -142,14 +142,14 @@ export const getAllPlayerStats = createServerFn() ); export const getPlayerMatches = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data }) => toServerResult(async () => await pbAdmin.getPlayerMatches(data)) ); export const getUnenrolledPlayers = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: tournamentId }) => toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId)) diff --git a/src/features/teams/server.ts b/src/features/teams/server.ts index 4074dbd..d3d5322 100644 --- a/src/features/teams/server.ts +++ b/src/features/teams/server.ts @@ -15,21 +15,21 @@ export const listTeamInfos = createServerFn() ); export const getTeam = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: teamId }) => toServerResult(() => pbAdmin.getTeam(teamId)) ); export const getTeamInfo = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: teamId }) => toServerResult(() => pbAdmin.getTeamInfo(teamId)) ); export const createTeam = createServerFn() - .validator(teamInputSchema) + .inputValidator(teamInputSchema) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data, context }) => toServerResult(async () => { @@ -46,7 +46,7 @@ export const createTeam = createServerFn() ); export const updateTeam = createServerFn() - .validator(z.object({ + .inputValidator(z.object({ id: z.string(), updates: teamUpdateSchema })) @@ -72,14 +72,14 @@ export const updateTeam = createServerFn() ); export const getTeamStats = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: teamId }) => toServerResult(() => pbAdmin.getTeamStats(teamId)) ); export const getTeamMatches = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data }) => toServerResult(async () => await pbAdmin.getTeamMatches(data)) diff --git a/src/features/tournaments/server.ts b/src/features/tournaments/server.ts index f339d78..b3fd58c 100644 --- a/src/features/tournaments/server.ts +++ b/src/features/tournaments/server.ts @@ -13,14 +13,14 @@ export const listTournaments = createServerFn() ); export const createTournament = createServerFn() - .validator(tournamentInputSchema) + .inputValidator(tournamentInputSchema) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data }) => toServerResult(() => pbAdmin.createTournament(data)) ); export const updateTournament = createServerFn() - .validator(z.object({ + .inputValidator(z.object({ id: z.string(), updates: tournamentInputSchema.partial() })) @@ -30,7 +30,7 @@ export const updateTournament = createServerFn() ); export const getTournament = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: tournamentId, context }) => { const isAdmin = context.roles.includes("Admin"); @@ -44,7 +44,7 @@ export const getCurrentTournament = createServerFn() ); export const enrollTeam = createServerFn() - .validator(z.object({ + .inputValidator(z.object({ tournamentId: z.string(), teamId: z.string() })) @@ -70,7 +70,7 @@ export const enrollTeam = createServerFn() ); export const unenrollTeam = createServerFn() - .validator(z.object({ + .inputValidator(z.object({ tournamentId: z.string(), teamId: z.string() })) @@ -80,21 +80,21 @@ export const unenrollTeam = createServerFn() ); export const getUnenrolledTeams = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data: tournamentId }) => toServerResult(() => pbAdmin.getUnenrolledTeams(tournamentId)) ); export const getFreeAgents = createServerFn() - .validator(z.string()) + .inputValidator(z.string()) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data: tournamentId }) => toServerResult(() => pbAdmin.getFreeAgents(tournamentId)) ); export const enrollFreeAgent = createServerFn() - .validator(z.object({ phone: z.string(), tournamentId: z.string() })) + .inputValidator(z.object({ phone: z.string(), tournamentId: z.string() })) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => toServerResult(async () => { @@ -108,7 +108,7 @@ export const enrollFreeAgent = createServerFn() ); export const unenrollFreeAgent = createServerFn() - .validator(z.object({ tournamentId: z.string() })) + .inputValidator(z.object({ tournamentId: z.string() })) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => toServerResult(async () => { diff --git a/src/lib/logger/index.ts b/src/lib/logger/index.ts index 8ec76c9..f9067fe 100644 --- a/src/lib/logger/index.ts +++ b/src/lib/logger/index.ts @@ -46,7 +46,7 @@ class Logger { constructor(context?: string, options: LoggerOptions = {}) { this.context = context; this.options = { - enabled: process.env.NODE_ENV !== "production", + enabled: import.meta.env.NODE_ENV !== "production", showTimestamp: true, collapsed: true, colors: true, diff --git a/src/lib/pocketbase/client.ts b/src/lib/pocketbase/client.ts index f4e3b3f..1092d5a 100644 --- a/src/lib/pocketbase/client.ts +++ b/src/lib/pocketbase/client.ts @@ -4,12 +4,19 @@ import { createTournamentsService } from "./services/tournaments"; import { createTeamsService } from "./services/teams"; import { createMatchesService } from "./services/matches"; import { createReactionsService } from "./services/reactions"; +import dotenv from 'dotenv'; +dotenv.config(); class PocketBaseAdminClient { private pb: PocketBase; public authPromise: Promise; 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.beforeSend = (url, options) => { diff --git a/src/lib/supertokens/server.ts b/src/lib/supertokens/server.ts index 77b9aa7..dd6856c 100644 --- a/src/lib/supertokens/server.ts +++ b/src/lib/supertokens/server.ts @@ -6,23 +6,24 @@ import UserRoles from "supertokens-node/recipe/userroles"; import { appInfo } from "./config"; import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode"; import { logger } from "./"; +import passwordlessTwilioVerify from "./recipes/passwordless-twilio-verify"; export const backendConfig = (): TypeInput => { return { framework: "custom", supertokens: { connectionURI: - process.env.SUPERTOKENS_URI || "https://try.supertokens.io", + import.meta.env.SUPERTOKENS_URI || "https://try.supertokens.io", }, appInfo, recipeList: [ PasswordlessDevelopmentMode.init(), Session.init({ cookieSameSite: "lax", - cookieSecure: process.env.NODE_ENV === "production", + cookieSecure: import.meta.env.NODE_ENV === "production", cookieDomain: - process.env.NODE_ENV === "production" ? ".example.com" : undefined, - antiCsrf: process.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE", + import.meta.env.NODE_ENV === "production" ? ".example.com" : undefined, + antiCsrf: import.meta.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE", // Debug only exposeAccessTokenToFrontendInCookieBasedAuth: true, @@ -30,13 +31,13 @@ export const backendConfig = (): TypeInput => { Dashboard.init(), UserRoles.init(), ], - telemetry: process.env.NODE_ENV !== "production", + telemetry: import.meta.env.NODE_ENV !== "production", }; }; let initialized = false; export function ensureSuperTokensBackend() { - if (!initialized) { + if (!initialized && typeof window === 'undefined') { SuperTokens.init(backendConfig()); initialized = true; logger.simple("Backend initialized"); diff --git a/src/lib/twilio/index.ts b/src/lib/twilio/index.ts index eb35b73..5756aec 100644 --- a/src/lib/twilio/index.ts +++ b/src/lib/twilio/index.ts @@ -1,13 +1,23 @@ -import twilio from "twilio"; +import type { Twilio } from "twilio"; -const accountSid = process.env.TWILIO_ACCOUNT_SID!; -const authToken = process.env.TWILIO_AUTH_TOKEN!; -const serviceSid = process.env.TWILIO_SERVICE_SID!; +const accountSid = import.meta.env.TWILIO_ACCOUNT_SID!; +const authToken = import.meta.env.TWILIO_AUTH_TOKEN!; +const serviceSid = import.meta.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) { - const verification = await client.verify.v2 + const twilioClient = getTwilioClient(); + + const verification = await twilioClient!.verify.v2 .services(serviceSid) .verifications.create({ channel: "sms", @@ -23,7 +33,9 @@ export async function sendVerifyCode(phoneNumber: string, code: string) { } export async function updateVerify(sid: string) { - const verification = await client.verify.v2 + const twilioClient = getTwilioClient(); + + const verification = await twilioClient!.verify.v2 .services(serviceSid) .verifications(sid) .update({ status: "approved" }); @@ -31,4 +43,4 @@ export async function updateVerify(sid: string) { if (verification.status !== "approved") { throw new Error("Unknown error updating verification"); } -} +} \ No newline at end of file diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts index 4c4a912..b61d5bc 100644 --- a/src/utils/supertokens.ts +++ b/src/utils/supertokens.ts @@ -1,9 +1,8 @@ import { createMiddleware, createServerFn, - ServerFnResponseType, } 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 UserRoles from "supertokens-node/recipe/userroles"; import UserMetadata from "supertokens-node/recipe/usermetadata"; @@ -15,8 +14,7 @@ import { refreshSession } from "supertokens-node/recipe/session"; const logger = new Logger("Middleware"); export const verifySuperTokensSession = async ( - request: Request, - response?: ServerFnResponseType + request: Request ) => { let session = await getSessionForStart(request, { sessionRequired: false }); @@ -24,13 +22,17 @@ export const verifySuperTokensSession = async ( logger.info("Session needs refresh"); try { - if (response) { - const refreshedSession = await refreshSession(request, response); - if (refreshedSession) { - session = await getSessionForStart(request, { sessionRequired: false }); + const refreshedSession = await refreshSession(request, { + setHeader: (key: string, value: string) => { + setResponseHeader(key, value); + }, + setCookie: (cookie: string) => { + setResponseHeader('Set-Cookie', cookie); } + }); + if (refreshedSession) { + session = await getSessionForStart(request, { sessionRequired: false }); } - if (session?.needsRefresh) { return { context: { session: { tryRefresh: true } } }; } @@ -109,8 +111,8 @@ export const superTokensRequestMiddleware = createMiddleware({ export const superTokensFunctionMiddleware = createMiddleware({ type: "function", -}).server(async ({ next, response }) => { - const request = getWebRequest(); +}).server(async ({ next }) => { + const request = getRequest(); try { const context = await getSessionContext(request, { isServerFunction: true }); @@ -135,7 +137,7 @@ export const superTokensFunctionMiddleware = createMiddleware({ export const superTokensAdminFunctionMiddleware = createMiddleware({ type: "function", }).server(async ({ next }) => { - const request = getWebRequest(); + const request = getRequest(); try { const context = await getSessionContext(request, { isServerFunction: true }); @@ -169,7 +171,7 @@ export const fetchUserRoles = async (userAuthId: string) => { }; export const setUserMetadata = createServerFn({ method: "POST" }) - .validator( + .inputValidator( z .object({ first_name: z @@ -212,7 +214,7 @@ export const setUserMetadata = createServerFn({ method: "POST" }) }); export const updateUserColorScheme = createServerFn({ method: "POST" }) - .validator((data: string) => data) + .inputValidator((data: string) => data) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => { const { userAuthId, metadata } = context; @@ -231,7 +233,7 @@ export const updateUserColorScheme = createServerFn({ method: "POST" }) }); export const updateUserAccentColor = createServerFn({ method: "POST" }) - .validator((data: string) => data) + .inputValidator((data: string) => data) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => { const { userAuthId, metadata } = context; diff --git a/vite.config.ts b/vite.config.ts index de6adc5..2209fba 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,16 +6,14 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ server: { port: 3000, + allowedHosts: ["dev.flexxon.app"] }, plugins: [ tsConfigPaths({ projects: ['./tsconfig.json'], }), tanstackStart({ - customViteReactPlugin: true, - tsr: { - srcDirectory: 'src/app', - }, + srcDirectory: 'src/app', }), react() ]