Compare commits
1 Commits
81329e4354
...
main_old
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed77dd471 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,4 +20,3 @@ yarn.lock
|
|||||||
/scripts/
|
/scripts/
|
||||||
/pb_data/
|
/pb_data/
|
||||||
/.tanstack/
|
/.tanstack/
|
||||||
/dist/
|
|
||||||
15
package.json
15
package.json
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0",
|
"dev": "vite dev --host 0.0.0.0",
|
||||||
"build": "vite build && tsc --noEmit",
|
"build": "vite build && tsc --noEmit",
|
||||||
"start": "vite start --host 0.0.0.0"
|
"start": "node .output/server/index.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
@@ -21,11 +21,10 @@
|
|||||||
"@svgmoji/noto": "^3.2.0",
|
"@svgmoji/noto": "^3.2.0",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
"@tanstack/react-query-devtools": "^5.66.0",
|
"@tanstack/react-query-devtools": "^5.66.0",
|
||||||
"@tanstack/react-router": "^1.130.12",
|
"@tanstack/react-router": "1.130.12",
|
||||||
"@tanstack/react-router-devtools": "^1.130.13",
|
"@tanstack/react-router-devtools": "1.130.13",
|
||||||
"@tanstack/react-router-with-query": "^1.130.12",
|
"@tanstack/react-router-with-query": "1.130.12",
|
||||||
"@tanstack/react-start": "^1.132.2",
|
"@tanstack/react-start": "1.130.15",
|
||||||
"@tanstack/react-virtual": "^3.13.12",
|
|
||||||
"@tiptap/pm": "^3.4.3",
|
"@tiptap/pm": "^3.4.3",
|
||||||
"@tiptap/react": "^3.4.3",
|
"@tiptap/react": "^3.4.3",
|
||||||
"@tiptap/starter-kit": "^3.4.3",
|
"@tiptap/starter-kit": "^3.4.3",
|
||||||
@@ -52,8 +51,6 @@
|
|||||||
"zustand": "^5.0.7"
|
"zustand": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/react-router-ssr-query": "^1.132.2",
|
|
||||||
"@tanstack/router-plugin": "^1.132.2",
|
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
@@ -66,7 +63,7 @@
|
|||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^7.1.7",
|
"vite": "^6.3.5",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
// You should NOT make any changes in this file as it will be overwritten.
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { createServerRootRoute } from '@tanstack/react-start/server'
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as RefreshSessionRouteImport } from './routes/refresh-session'
|
import { Route as RefreshSessionRouteImport } from './routes/refresh-session'
|
||||||
import { Route as LogoutRouteImport } from './routes/logout'
|
import { Route as LogoutRouteImport } from './routes/logout'
|
||||||
@@ -19,16 +21,6 @@ import { Route as AuthedSettingsRouteImport } from './routes/_authed/settings'
|
|||||||
import { Route as AuthedAdminRouteImport } from './routes/_authed/admin'
|
import { Route as AuthedAdminRouteImport } from './routes/_authed/admin'
|
||||||
import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index'
|
import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index'
|
||||||
import { Route as AuthedAdminIndexRouteImport } from './routes/_authed/admin/index'
|
import { Route as AuthedAdminIndexRouteImport } from './routes/_authed/admin/index'
|
||||||
import { Route as ApiTournamentsUploadLogoRouteImport } from './routes/api/tournaments/upload-logo'
|
|
||||||
import { Route as ApiTeamsUploadLogoRouteImport } from './routes/api/teams/upload-logo'
|
|
||||||
import { Route as ApiSpotifyTokenRouteImport } from './routes/api/spotify/token'
|
|
||||||
import { Route as ApiSpotifySearchRouteImport } from './routes/api/spotify/search'
|
|
||||||
import { Route as ApiSpotifyResumeRouteImport } from './routes/api/spotify/resume'
|
|
||||||
import { Route as ApiSpotifyPlaybackRouteImport } from './routes/api/spotify/playback'
|
|
||||||
import { Route as ApiSpotifyCaptureRouteImport } from './routes/api/spotify/capture'
|
|
||||||
import { Route as ApiSpotifyCallbackRouteImport } from './routes/api/spotify/callback'
|
|
||||||
import { Route as ApiEventsSplatRouteImport } from './routes/api/events.$'
|
|
||||||
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth.$'
|
|
||||||
import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_authed/tournaments/$tournamentId'
|
import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_authed/tournaments/$tournamentId'
|
||||||
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
|
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
|
||||||
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
|
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
|
||||||
@@ -36,9 +28,21 @@ import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/p
|
|||||||
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
||||||
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
||||||
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
||||||
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
|
||||||
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
||||||
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
|
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
|
||||||
|
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
||||||
|
import { ServerRoute as ApiTeamsUploadLogoServerRouteImport } from './routes/api/teams/upload-logo'
|
||||||
|
import { ServerRoute as ApiSpotifyTokenServerRouteImport } from './routes/api/spotify/token'
|
||||||
|
import { ServerRoute as ApiSpotifySearchServerRouteImport } from './routes/api/spotify/search'
|
||||||
|
import { ServerRoute as ApiSpotifyResumeServerRouteImport } from './routes/api/spotify/resume'
|
||||||
|
import { ServerRoute as ApiSpotifyPlaybackServerRouteImport } from './routes/api/spotify/playback'
|
||||||
|
import { ServerRoute as ApiSpotifyCaptureServerRouteImport } from './routes/api/spotify/capture'
|
||||||
|
import { ServerRoute as ApiSpotifyCallbackServerRouteImport } from './routes/api/spotify/callback'
|
||||||
|
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
||||||
|
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
|
||||||
|
import { ServerRoute as ApiFilesCollectionRecordIdFileServerRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
||||||
|
|
||||||
|
const rootServerRouteImport = createServerRootRoute()
|
||||||
|
|
||||||
const RefreshSessionRoute = RefreshSessionRouteImport.update({
|
const RefreshSessionRoute = RefreshSessionRouteImport.update({
|
||||||
id: '/refresh-session',
|
id: '/refresh-session',
|
||||||
@@ -89,57 +93,6 @@ const AuthedAdminIndexRoute = AuthedAdminIndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const ApiTournamentsUploadLogoRoute =
|
|
||||||
ApiTournamentsUploadLogoRouteImport.update({
|
|
||||||
id: '/api/tournaments/upload-logo',
|
|
||||||
path: '/api/tournaments/upload-logo',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiTeamsUploadLogoRoute = ApiTeamsUploadLogoRouteImport.update({
|
|
||||||
id: '/api/teams/upload-logo',
|
|
||||||
path: '/api/teams/upload-logo',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiSpotifyTokenRoute = ApiSpotifyTokenRouteImport.update({
|
|
||||||
id: '/api/spotify/token',
|
|
||||||
path: '/api/spotify/token',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiSpotifySearchRoute = ApiSpotifySearchRouteImport.update({
|
|
||||||
id: '/api/spotify/search',
|
|
||||||
path: '/api/spotify/search',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiSpotifyResumeRoute = ApiSpotifyResumeRouteImport.update({
|
|
||||||
id: '/api/spotify/resume',
|
|
||||||
path: '/api/spotify/resume',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiSpotifyPlaybackRoute = ApiSpotifyPlaybackRouteImport.update({
|
|
||||||
id: '/api/spotify/playback',
|
|
||||||
path: '/api/spotify/playback',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiSpotifyCaptureRoute = ApiSpotifyCaptureRouteImport.update({
|
|
||||||
id: '/api/spotify/capture',
|
|
||||||
path: '/api/spotify/capture',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiSpotifyCallbackRoute = ApiSpotifyCallbackRouteImport.update({
|
|
||||||
id: '/api/spotify/callback',
|
|
||||||
path: '/api/spotify/callback',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiEventsSplatRoute = ApiEventsSplatRouteImport.update({
|
|
||||||
id: '/api/events/$',
|
|
||||||
path: '/api/events/$',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
|
||||||
id: '/api/auth/$',
|
|
||||||
path: '/api/auth/$',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const AuthedTournamentsTournamentIdRoute =
|
const AuthedTournamentsTournamentIdRoute =
|
||||||
AuthedTournamentsTournamentIdRouteImport.update({
|
AuthedTournamentsTournamentIdRouteImport.update({
|
||||||
id: '/tournaments/$tournamentId',
|
id: '/tournaments/$tournamentId',
|
||||||
@@ -179,12 +132,6 @@ const AuthedAdminTournamentsIdIndexRoute =
|
|||||||
path: '/tournaments/$id/',
|
path: '/tournaments/$id/',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const ApiFilesCollectionRecordIdFileRoute =
|
|
||||||
ApiFilesCollectionRecordIdFileRouteImport.update({
|
|
||||||
id: '/api/files/$collection/$recordId/$file',
|
|
||||||
path: '/api/files/$collection/$recordId/$file',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminTournamentsRunIdRoute =
|
const AuthedAdminTournamentsRunIdRoute =
|
||||||
AuthedAdminTournamentsRunIdRouteImport.update({
|
AuthedAdminTournamentsRunIdRouteImport.update({
|
||||||
id: '/tournaments/run/$id',
|
id: '/tournaments/run/$id',
|
||||||
@@ -197,6 +144,66 @@ const AuthedAdminTournamentsIdTeamsRoute =
|
|||||||
path: '/tournaments/$id/teams',
|
path: '/tournaments/$id/teams',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiTournamentsUploadLogoServerRoute =
|
||||||
|
ApiTournamentsUploadLogoServerRouteImport.update({
|
||||||
|
id: '/api/tournaments/upload-logo',
|
||||||
|
path: '/api/tournaments/upload-logo',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiTeamsUploadLogoServerRoute =
|
||||||
|
ApiTeamsUploadLogoServerRouteImport.update({
|
||||||
|
id: '/api/teams/upload-logo',
|
||||||
|
path: '/api/teams/upload-logo',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiSpotifyTokenServerRoute = ApiSpotifyTokenServerRouteImport.update({
|
||||||
|
id: '/api/spotify/token',
|
||||||
|
path: '/api/spotify/token',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiSpotifySearchServerRoute = ApiSpotifySearchServerRouteImport.update({
|
||||||
|
id: '/api/spotify/search',
|
||||||
|
path: '/api/spotify/search',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiSpotifyResumeServerRoute = ApiSpotifyResumeServerRouteImport.update({
|
||||||
|
id: '/api/spotify/resume',
|
||||||
|
path: '/api/spotify/resume',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiSpotifyPlaybackServerRoute =
|
||||||
|
ApiSpotifyPlaybackServerRouteImport.update({
|
||||||
|
id: '/api/spotify/playback',
|
||||||
|
path: '/api/spotify/playback',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiSpotifyCaptureServerRoute = ApiSpotifyCaptureServerRouteImport.update({
|
||||||
|
id: '/api/spotify/capture',
|
||||||
|
path: '/api/spotify/capture',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiSpotifyCallbackServerRoute =
|
||||||
|
ApiSpotifyCallbackServerRouteImport.update({
|
||||||
|
id: '/api/spotify/callback',
|
||||||
|
path: '/api/spotify/callback',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({
|
||||||
|
id: '/api/events/$',
|
||||||
|
path: '/api/events/$',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiAuthSplatServerRoute = ApiAuthSplatServerRouteImport.update({
|
||||||
|
id: '/api/auth/$',
|
||||||
|
path: '/api/auth/$',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiFilesCollectionRecordIdFileServerRoute =
|
||||||
|
ApiFilesCollectionRecordIdFileServerRouteImport.update({
|
||||||
|
id: '/api/files/$collection/$recordId/$file',
|
||||||
|
path: '/api/files/$collection/$recordId/$file',
|
||||||
|
getParentRoute: () => rootServerRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
@@ -210,23 +217,12 @@ export interface FileRoutesByFullPath {
|
|||||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
|
||||||
'/api/events/$': typeof ApiEventsSplatRoute
|
|
||||||
'/api/spotify/callback': typeof ApiSpotifyCallbackRoute
|
|
||||||
'/api/spotify/capture': typeof ApiSpotifyCaptureRoute
|
|
||||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackRoute
|
|
||||||
'/api/spotify/resume': typeof ApiSpotifyResumeRoute
|
|
||||||
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
|
||||||
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
|
||||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
|
||||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
|
||||||
'/admin/': typeof AuthedAdminIndexRoute
|
'/admin/': typeof AuthedAdminIndexRoute
|
||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
|
||||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
|
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
@@ -240,23 +236,12 @@ export interface FileRoutesByTo {
|
|||||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
|
||||||
'/api/events/$': typeof ApiEventsSplatRoute
|
|
||||||
'/api/spotify/callback': typeof ApiSpotifyCallbackRoute
|
|
||||||
'/api/spotify/capture': typeof ApiSpotifyCaptureRoute
|
|
||||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackRoute
|
|
||||||
'/api/spotify/resume': typeof ApiSpotifyResumeRoute
|
|
||||||
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
|
||||||
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
|
||||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
|
||||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
|
||||||
'/admin': typeof AuthedAdminIndexRoute
|
'/admin': typeof AuthedAdminIndexRoute
|
||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
|
||||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
|
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -273,23 +258,12 @@ export interface FileRoutesById {
|
|||||||
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
|
||||||
'/api/events/$': typeof ApiEventsSplatRoute
|
|
||||||
'/api/spotify/callback': typeof ApiSpotifyCallbackRoute
|
|
||||||
'/api/spotify/capture': typeof ApiSpotifyCaptureRoute
|
|
||||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackRoute
|
|
||||||
'/api/spotify/resume': typeof ApiSpotifyResumeRoute
|
|
||||||
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
|
||||||
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
|
||||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
|
||||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
|
||||||
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
||||||
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||||
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
||||||
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
|
||||||
'/_authed/admin/tournaments/$id/': typeof AuthedAdminTournamentsIdIndexRoute
|
'/_authed/admin/tournaments/$id/': typeof AuthedAdminTournamentsIdIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@@ -306,23 +280,12 @@ export interface FileRouteTypes {
|
|||||||
| '/profile/$playerId'
|
| '/profile/$playerId'
|
||||||
| '/teams/$teamId'
|
| '/teams/$teamId'
|
||||||
| '/tournaments/$tournamentId'
|
| '/tournaments/$tournamentId'
|
||||||
| '/api/auth/$'
|
|
||||||
| '/api/events/$'
|
|
||||||
| '/api/spotify/callback'
|
|
||||||
| '/api/spotify/capture'
|
|
||||||
| '/api/spotify/playback'
|
|
||||||
| '/api/spotify/resume'
|
|
||||||
| '/api/spotify/search'
|
|
||||||
| '/api/spotify/token'
|
|
||||||
| '/api/teams/upload-logo'
|
|
||||||
| '/api/tournaments/upload-logo'
|
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
| '/tournaments/$id/bracket'
|
| '/tournaments/$id/bracket'
|
||||||
| '/admin/tournaments'
|
| '/admin/tournaments'
|
||||||
| '/admin/tournaments/$id/teams'
|
| '/admin/tournaments/$id/teams'
|
||||||
| '/admin/tournaments/run/$id'
|
| '/admin/tournaments/run/$id'
|
||||||
| '/api/files/$collection/$recordId/$file'
|
|
||||||
| '/admin/tournaments/$id'
|
| '/admin/tournaments/$id'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
@@ -336,23 +299,12 @@ export interface FileRouteTypes {
|
|||||||
| '/profile/$playerId'
|
| '/profile/$playerId'
|
||||||
| '/teams/$teamId'
|
| '/teams/$teamId'
|
||||||
| '/tournaments/$tournamentId'
|
| '/tournaments/$tournamentId'
|
||||||
| '/api/auth/$'
|
|
||||||
| '/api/events/$'
|
|
||||||
| '/api/spotify/callback'
|
|
||||||
| '/api/spotify/capture'
|
|
||||||
| '/api/spotify/playback'
|
|
||||||
| '/api/spotify/resume'
|
|
||||||
| '/api/spotify/search'
|
|
||||||
| '/api/spotify/token'
|
|
||||||
| '/api/teams/upload-logo'
|
|
||||||
| '/api/tournaments/upload-logo'
|
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
| '/tournaments/$id/bracket'
|
| '/tournaments/$id/bracket'
|
||||||
| '/admin/tournaments'
|
| '/admin/tournaments'
|
||||||
| '/admin/tournaments/$id/teams'
|
| '/admin/tournaments/$id/teams'
|
||||||
| '/admin/tournaments/run/$id'
|
| '/admin/tournaments/run/$id'
|
||||||
| '/api/files/$collection/$recordId/$file'
|
|
||||||
| '/admin/tournaments/$id'
|
| '/admin/tournaments/$id'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
@@ -368,6 +320,64 @@ export interface FileRouteTypes {
|
|||||||
| '/_authed/profile/$playerId'
|
| '/_authed/profile/$playerId'
|
||||||
| '/_authed/teams/$teamId'
|
| '/_authed/teams/$teamId'
|
||||||
| '/_authed/tournaments/$tournamentId'
|
| '/_authed/tournaments/$tournamentId'
|
||||||
|
| '/_authed/admin/'
|
||||||
|
| '/_authed/tournaments/'
|
||||||
|
| '/_authed/tournaments/$id/bracket'
|
||||||
|
| '/_authed/admin/tournaments/'
|
||||||
|
| '/_authed/admin/tournaments/$id/teams'
|
||||||
|
| '/_authed/admin/tournaments/run/$id'
|
||||||
|
| '/_authed/admin/tournaments/$id/'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
AuthedRoute: typeof AuthedRouteWithChildren
|
||||||
|
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/auth/$'
|
||||||
| '/api/events/$'
|
| '/api/events/$'
|
||||||
| '/api/spotify/callback'
|
| '/api/spotify/callback'
|
||||||
@@ -378,32 +388,47 @@ export interface FileRouteTypes {
|
|||||||
| '/api/spotify/token'
|
| '/api/spotify/token'
|
||||||
| '/api/teams/upload-logo'
|
| '/api/teams/upload-logo'
|
||||||
| '/api/tournaments/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'
|
| '/api/files/$collection/$recordId/$file'
|
||||||
| '/_authed/admin/tournaments/$id/'
|
fileServerRoutesByTo: FileServerRoutesByTo
|
||||||
fileRoutesById: FileRoutesById
|
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 RootRouteChildren {
|
export interface RootServerRouteChildren {
|
||||||
AuthedRoute: typeof AuthedRouteWithChildren
|
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
|
||||||
LoginRoute: typeof LoginRoute
|
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
|
||||||
LogoutRoute: typeof LogoutRoute
|
ApiSpotifyCallbackServerRoute: typeof ApiSpotifyCallbackServerRoute
|
||||||
RefreshSessionRoute: typeof RefreshSessionRoute
|
ApiSpotifyCaptureServerRoute: typeof ApiSpotifyCaptureServerRoute
|
||||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
ApiSpotifyPlaybackServerRoute: typeof ApiSpotifyPlaybackServerRoute
|
||||||
ApiEventsSplatRoute: typeof ApiEventsSplatRoute
|
ApiSpotifyResumeServerRoute: typeof ApiSpotifyResumeServerRoute
|
||||||
ApiSpotifyCallbackRoute: typeof ApiSpotifyCallbackRoute
|
ApiSpotifySearchServerRoute: typeof ApiSpotifySearchServerRoute
|
||||||
ApiSpotifyCaptureRoute: typeof ApiSpotifyCaptureRoute
|
ApiSpotifyTokenServerRoute: typeof ApiSpotifyTokenServerRoute
|
||||||
ApiSpotifyPlaybackRoute: typeof ApiSpotifyPlaybackRoute
|
ApiTeamsUploadLogoServerRoute: typeof ApiTeamsUploadLogoServerRoute
|
||||||
ApiSpotifyResumeRoute: typeof ApiSpotifyResumeRoute
|
ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute
|
||||||
ApiSpotifySearchRoute: typeof ApiSpotifySearchRoute
|
ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||||
ApiSpotifyTokenRoute: typeof ApiSpotifyTokenRoute
|
|
||||||
ApiTeamsUploadLogoRoute: typeof ApiTeamsUploadLogoRoute
|
|
||||||
ApiTournamentsUploadLogoRoute: typeof ApiTournamentsUploadLogoRoute
|
|
||||||
ApiFilesCollectionRecordIdFileRoute: typeof ApiFilesCollectionRecordIdFileRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -478,76 +503,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminIndexRouteImport
|
preLoaderRoute: typeof AuthedAdminIndexRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
'/api/tournaments/upload-logo': {
|
|
||||||
id: '/api/tournaments/upload-logo'
|
|
||||||
path: '/api/tournaments/upload-logo'
|
|
||||||
fullPath: '/api/tournaments/upload-logo'
|
|
||||||
preLoaderRoute: typeof ApiTournamentsUploadLogoRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/teams/upload-logo': {
|
|
||||||
id: '/api/teams/upload-logo'
|
|
||||||
path: '/api/teams/upload-logo'
|
|
||||||
fullPath: '/api/teams/upload-logo'
|
|
||||||
preLoaderRoute: typeof ApiTeamsUploadLogoRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/spotify/token': {
|
|
||||||
id: '/api/spotify/token'
|
|
||||||
path: '/api/spotify/token'
|
|
||||||
fullPath: '/api/spotify/token'
|
|
||||||
preLoaderRoute: typeof ApiSpotifyTokenRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/spotify/search': {
|
|
||||||
id: '/api/spotify/search'
|
|
||||||
path: '/api/spotify/search'
|
|
||||||
fullPath: '/api/spotify/search'
|
|
||||||
preLoaderRoute: typeof ApiSpotifySearchRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/spotify/resume': {
|
|
||||||
id: '/api/spotify/resume'
|
|
||||||
path: '/api/spotify/resume'
|
|
||||||
fullPath: '/api/spotify/resume'
|
|
||||||
preLoaderRoute: typeof ApiSpotifyResumeRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/spotify/playback': {
|
|
||||||
id: '/api/spotify/playback'
|
|
||||||
path: '/api/spotify/playback'
|
|
||||||
fullPath: '/api/spotify/playback'
|
|
||||||
preLoaderRoute: typeof ApiSpotifyPlaybackRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/spotify/capture': {
|
|
||||||
id: '/api/spotify/capture'
|
|
||||||
path: '/api/spotify/capture'
|
|
||||||
fullPath: '/api/spotify/capture'
|
|
||||||
preLoaderRoute: typeof ApiSpotifyCaptureRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/spotify/callback': {
|
|
||||||
id: '/api/spotify/callback'
|
|
||||||
path: '/api/spotify/callback'
|
|
||||||
fullPath: '/api/spotify/callback'
|
|
||||||
preLoaderRoute: typeof ApiSpotifyCallbackRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/events/$': {
|
|
||||||
id: '/api/events/$'
|
|
||||||
path: '/api/events/$'
|
|
||||||
fullPath: '/api/events/$'
|
|
||||||
preLoaderRoute: typeof ApiEventsSplatRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/auth/$': {
|
|
||||||
id: '/api/auth/$'
|
|
||||||
path: '/api/auth/$'
|
|
||||||
fullPath: '/api/auth/$'
|
|
||||||
preLoaderRoute: typeof ApiAuthSplatRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/_authed/tournaments/$tournamentId': {
|
'/_authed/tournaments/$tournamentId': {
|
||||||
id: '/_authed/tournaments/$tournamentId'
|
id: '/_authed/tournaments/$tournamentId'
|
||||||
path: '/tournaments/$tournamentId'
|
path: '/tournaments/$tournamentId'
|
||||||
@@ -597,13 +552,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminTournamentsIdIndexRouteImport
|
preLoaderRoute: typeof AuthedAdminTournamentsIdIndexRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
'/api/files/$collection/$recordId/$file': {
|
|
||||||
id: '/api/files/$collection/$recordId/$file'
|
|
||||||
path: '/api/files/$collection/$recordId/$file'
|
|
||||||
fullPath: '/api/files/$collection/$recordId/$file'
|
|
||||||
preLoaderRoute: typeof ApiFilesCollectionRecordIdFileRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/_authed/admin/tournaments/run/$id': {
|
'/_authed/admin/tournaments/run/$id': {
|
||||||
id: '/_authed/admin/tournaments/run/$id'
|
id: '/_authed/admin/tournaments/run/$id'
|
||||||
path: '/tournaments/run/$id'
|
path: '/tournaments/run/$id'
|
||||||
@@ -620,6 +568,87 @@ declare module '@tanstack/react-router' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
declare module '@tanstack/react-start/server' {
|
||||||
|
interface ServerFileRoutesByPath {
|
||||||
|
'/api/tournaments/upload-logo': {
|
||||||
|
id: '/api/tournaments/upload-logo'
|
||||||
|
path: '/api/tournaments/upload-logo'
|
||||||
|
fullPath: '/api/tournaments/upload-logo'
|
||||||
|
preLoaderRoute: typeof ApiTournamentsUploadLogoServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/teams/upload-logo': {
|
||||||
|
id: '/api/teams/upload-logo'
|
||||||
|
path: '/api/teams/upload-logo'
|
||||||
|
fullPath: '/api/teams/upload-logo'
|
||||||
|
preLoaderRoute: typeof ApiTeamsUploadLogoServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/spotify/token': {
|
||||||
|
id: '/api/spotify/token'
|
||||||
|
path: '/api/spotify/token'
|
||||||
|
fullPath: '/api/spotify/token'
|
||||||
|
preLoaderRoute: typeof ApiSpotifyTokenServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/spotify/search': {
|
||||||
|
id: '/api/spotify/search'
|
||||||
|
path: '/api/spotify/search'
|
||||||
|
fullPath: '/api/spotify/search'
|
||||||
|
preLoaderRoute: typeof ApiSpotifySearchServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/spotify/resume': {
|
||||||
|
id: '/api/spotify/resume'
|
||||||
|
path: '/api/spotify/resume'
|
||||||
|
fullPath: '/api/spotify/resume'
|
||||||
|
preLoaderRoute: typeof ApiSpotifyResumeServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/spotify/playback': {
|
||||||
|
id: '/api/spotify/playback'
|
||||||
|
path: '/api/spotify/playback'
|
||||||
|
fullPath: '/api/spotify/playback'
|
||||||
|
preLoaderRoute: typeof ApiSpotifyPlaybackServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/spotify/capture': {
|
||||||
|
id: '/api/spotify/capture'
|
||||||
|
path: '/api/spotify/capture'
|
||||||
|
fullPath: '/api/spotify/capture'
|
||||||
|
preLoaderRoute: typeof ApiSpotifyCaptureServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/spotify/callback': {
|
||||||
|
id: '/api/spotify/callback'
|
||||||
|
path: '/api/spotify/callback'
|
||||||
|
fullPath: '/api/spotify/callback'
|
||||||
|
preLoaderRoute: typeof ApiSpotifyCallbackServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/events/$': {
|
||||||
|
id: '/api/events/$'
|
||||||
|
path: '/api/events/$'
|
||||||
|
fullPath: '/api/events/$'
|
||||||
|
preLoaderRoute: typeof ApiEventsSplatServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/auth/$': {
|
||||||
|
id: '/api/auth/$'
|
||||||
|
path: '/api/auth/$'
|
||||||
|
fullPath: '/api/auth/$'
|
||||||
|
preLoaderRoute: typeof ApiAuthSplatServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
'/api/files/$collection/$recordId/$file': {
|
||||||
|
id: '/api/files/$collection/$recordId/$file'
|
||||||
|
path: '/api/files/$collection/$recordId/$file'
|
||||||
|
fullPath: '/api/files/$collection/$recordId/$file'
|
||||||
|
preLoaderRoute: typeof ApiFilesCollectionRecordIdFileServerRouteImport
|
||||||
|
parentRoute: typeof rootServerRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthedAdminRouteChildren {
|
interface AuthedAdminRouteChildren {
|
||||||
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
||||||
@@ -675,26 +704,24 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
LogoutRoute: LogoutRoute,
|
LogoutRoute: LogoutRoute,
|
||||||
RefreshSessionRoute: RefreshSessionRoute,
|
RefreshSessionRoute: RefreshSessionRoute,
|
||||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
|
||||||
ApiEventsSplatRoute: ApiEventsSplatRoute,
|
|
||||||
ApiSpotifyCallbackRoute: ApiSpotifyCallbackRoute,
|
|
||||||
ApiSpotifyCaptureRoute: ApiSpotifyCaptureRoute,
|
|
||||||
ApiSpotifyPlaybackRoute: ApiSpotifyPlaybackRoute,
|
|
||||||
ApiSpotifyResumeRoute: ApiSpotifyResumeRoute,
|
|
||||||
ApiSpotifySearchRoute: ApiSpotifySearchRoute,
|
|
||||||
ApiSpotifyTokenRoute: ApiSpotifyTokenRoute,
|
|
||||||
ApiTeamsUploadLogoRoute: ApiTeamsUploadLogoRoute,
|
|
||||||
ApiTournamentsUploadLogoRoute: ApiTournamentsUploadLogoRoute,
|
|
||||||
ApiFilesCollectionRecordIdFileRoute: ApiFilesCollectionRecordIdFileRoute,
|
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
._addFileTypes<FileRouteTypes>()
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
const rootServerRouteChildren: RootServerRouteChildren = {
|
||||||
import type { getRouter } from './router.tsx'
|
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
|
||||||
import type { createStart } from '@tanstack/react-start'
|
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute,
|
||||||
declare module '@tanstack/react-start' {
|
ApiSpotifyCallbackServerRoute: ApiSpotifyCallbackServerRoute,
|
||||||
interface Register {
|
ApiSpotifyCaptureServerRoute: ApiSpotifyCaptureServerRoute,
|
||||||
router: Awaited<ReturnType<typeof getRouter>>
|
ApiSpotifyPlaybackServerRoute: ApiSpotifyPlaybackServerRoute,
|
||||||
}
|
ApiSpotifyResumeServerRoute: ApiSpotifyResumeServerRoute,
|
||||||
|
ApiSpotifySearchServerRoute: ApiSpotifySearchServerRoute,
|
||||||
|
ApiSpotifyTokenServerRoute: ApiSpotifyTokenServerRoute,
|
||||||
|
ApiTeamsUploadLogoServerRoute: ApiTeamsUploadLogoServerRoute,
|
||||||
|
ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute,
|
||||||
|
ApiFilesCollectionRecordIdFileServerRoute:
|
||||||
|
ApiFilesCollectionRecordIdFileServerRoute,
|
||||||
}
|
}
|
||||||
|
export const serverRouteTree = rootServerRouteImport
|
||||||
|
._addFileChildren(rootServerRouteChildren)
|
||||||
|
._addFileTypes<FileServerRouteTypes>()
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
||||||
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
|
import { routerWithQueryClient } from "@tanstack/react-router-with-query";
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
||||||
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
||||||
|
|
||||||
export function getRouter() {
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
|
||||||
|
export function createRouter() {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
@@ -18,32 +22,28 @@ export function getRouter() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = createTanStackRouter({
|
return routerWithQueryClient(
|
||||||
routeTree,
|
createTanStackRouter({
|
||||||
context: {
|
routeTree,
|
||||||
queryClient,
|
context: {
|
||||||
auth: undefined!,
|
queryClient,
|
||||||
header: defaultHeaderConfig,
|
auth: undefined!,
|
||||||
refresh: [],
|
header: defaultHeaderConfig,
|
||||||
withPadding: true,
|
refresh: [],
|
||||||
fullWidth: false,
|
withPadding: true,
|
||||||
},
|
fullWidth: false,
|
||||||
defaultPreload: "intent",
|
},
|
||||||
defaultErrorComponent: DefaultCatchBoundary,
|
defaultPreload: "intent",
|
||||||
scrollRestoration: true,
|
defaultErrorComponent: DefaultCatchBoundary,
|
||||||
defaultViewTransition: false,
|
scrollRestoration: true,
|
||||||
});
|
defaultViewTransition: false,
|
||||||
|
}),
|
||||||
setupRouterSsrQueryIntegration({
|
|
||||||
router,
|
|
||||||
queryClient
|
queryClient
|
||||||
})
|
);
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
interface Register {
|
interface Register {
|
||||||
router: ReturnType<typeof getRouter>;
|
router: ReturnType<typeof createRouter>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,20 +83,12 @@ export const Route = createRootRouteWithContext<{
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname === '/login' || location.pathname === '/logout') {
|
// https://github.com/TanStack/router/discussions/3531
|
||||||
return {};
|
const auth = await ensureServerQueryData(
|
||||||
}
|
context.queryClient,
|
||||||
|
playerQueries.auth()
|
||||||
try {
|
);
|
||||||
// https://github.com/TanStack/router/discussions/3531
|
return { auth };
|
||||||
const auth = await ensureServerQueryData(
|
|
||||||
context.queryClient,
|
|
||||||
playerQueries.auth()
|
|
||||||
);
|
|
||||||
return { auth };
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
pendingComponent: () => <Providers><FullScreenLoader /></Providers>,
|
pendingComponent: () => <Providers><FullScreenLoader /></Providers>,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// API file that handles all supertokens auth routes
|
// API file that handles all supertokens auth routes
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createServerFileRoute } from '@tanstack/react-start/server';
|
||||||
import { handleAuthAPIRequest } from 'supertokens-node/custom'
|
import { handleAuthAPIRequest } from 'supertokens-node/custom'
|
||||||
import { ensureSuperTokensBackend } from '@/lib/supertokens/server'
|
import { ensureSuperTokensBackend } from '@/lib/supertokens/server'
|
||||||
|
|
||||||
@@ -12,16 +12,12 @@ const handleRequest = async ({ request }: {request: Request}) => {
|
|||||||
console.log("Handling auth request:", request.method, request.url);
|
console.log("Handling auth request:", request.method, request.url);
|
||||||
return superTokensHandler(request);
|
return superTokensHandler(request);
|
||||||
};
|
};
|
||||||
export const Route = createFileRoute('/api/auth/$')({
|
export const ServerRoute = createServerFileRoute('/api/auth/$').methods({
|
||||||
server: {
|
GET: handleRequest,
|
||||||
handlers: {
|
POST: handleRequest,
|
||||||
GET: handleRequest,
|
PUT: handleRequest,
|
||||||
POST: handleRequest,
|
DELETE: handleRequest,
|
||||||
PUT: handleRequest,
|
PATCH: handleRequest,
|
||||||
DELETE: handleRequest,
|
OPTIONS: handleRequest,
|
||||||
PATCH: handleRequest,
|
HEAD: handleRequest,
|
||||||
OPTIONS: handleRequest,
|
|
||||||
HEAD: handleRequest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,72 +1,66 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from "@tanstack/react-start/server";
|
||||||
import { serverEvents, type ServerEvent } from "@/lib/events/emitter";
|
import { serverEvents, type ServerEvent } from "@/lib/events/emitter";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/events/$")({
|
export const ServerRoute = createServerFileRoute("/api/events/$").middleware([superTokensRequestMiddleware]).methods({
|
||||||
server: {
|
GET: ({ request, context }) => {
|
||||||
middleware: [superTokensRequestMiddleware],
|
logger.info('ServerEvents | New connection', context?.userAuthId);
|
||||||
handlers: {
|
|
||||||
GET: ({ request, context }) => {
|
|
||||||
logger.info("ServerEvents | New connection", context?.userAuthId);
|
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
|
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
|
||||||
controller.enqueue(new TextEncoder().encode(connectMessage));
|
controller.enqueue(new TextEncoder().encode(connectMessage));
|
||||||
|
|
||||||
const handleEvent = (event: ServerEvent) => {
|
const handleEvent = (event: ServerEvent) => {
|
||||||
logger.info("ServerEvents | Event received", event);
|
logger.info('ServerEvents | Event received', event);
|
||||||
const message = `data: ${JSON.stringify(event)}\n\n`;
|
const message = `data: ${JSON.stringify(event)}\n\n`;
|
||||||
try {
|
try {
|
||||||
controller.enqueue(new TextEncoder().encode(message));
|
controller.enqueue(new TextEncoder().encode(message));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("ServerEvents | Error sending SSE message", error);
|
logger.error("ServerEvents | Error sending SSE message", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
serverEvents.on("test", handleEvent);
|
serverEvents.on("test", handleEvent);
|
||||||
serverEvents.on("match", handleEvent);
|
serverEvents.on("match", handleEvent);
|
||||||
serverEvents.on("reaction", handleEvent);
|
serverEvents.on("reaction", handleEvent);
|
||||||
|
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`;
|
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`;
|
||||||
controller.enqueue(new TextEncoder().encode(pingMessage));
|
controller.enqueue(new TextEncoder().encode(pingMessage));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
controller.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
serverEvents.off("test", handleEvent);
|
serverEvents.off("test", handleEvent);
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
try {
|
try {
|
||||||
logger.info(
|
logger.info('ServerEvents | Closing connection', context?.userAuthId);
|
||||||
"ServerEvents | Closing connection",
|
controller.close();
|
||||||
context?.userAuthId
|
} catch (e) {
|
||||||
);
|
logger.error('ServerEvents | Error closing controller', e);
|
||||||
} catch (e) {
|
}
|
||||||
logger.error("ServerEvents | Error closing controller", e);
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.signal?.addEventListener("abort", cleanup);
|
request.signal?.addEventListener("abort", cleanup);
|
||||||
return cleanup;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(stream, {
|
return cleanup;
|
||||||
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1,113 +1,95 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from "@tanstack/react-start/server";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const ServerRoute = createServerFileRoute("/api/files/$collection/$recordId/$file").methods({
|
||||||
"/api/files/$collection/$recordId/$file"
|
GET: async ({ params, request }) => {
|
||||||
)({
|
try {
|
||||||
server: {
|
const { collection, recordId, file } = params;
|
||||||
handlers: {
|
const pocketbaseUrl = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||||
GET: async ({ params, request }) => {
|
const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`;
|
||||||
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", {
|
logger.info('File proxy', {
|
||||||
collection,
|
collection,
|
||||||
recordId,
|
recordId,
|
||||||
file,
|
file,
|
||||||
targetUrl: fileUrl,
|
targetUrl: fileUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(fileUrl, {
|
const response = await fetch(fileUrl, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
...(request.headers.get("range") && {
|
...(request.headers.get('range') && { 'Range': 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')! }),
|
||||||
...(request.headers.get("if-none-match") && {
|
},
|
||||||
"If-None-Match": request.headers.get("if-none-match")!,
|
});
|
||||||
}),
|
|
||||||
...(request.headers.get("if-modified-since") && {
|
|
||||||
"If-Modified-Since": request.headers.get("if-modified-since")!,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.error("PocketBase file request failed", {
|
logger.error('PocketBase file request failed', {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
url: fileUrl,
|
url: fileUrl
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 404) {
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
OPTIONS: () => {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
||||||
"Access-Control-Max-Age": "86400",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
},
|
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 });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
OPTIONS: () => {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||||
|
'Access-Control-Max-Age': '86400',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -1,146 +1,127 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||||
import { SpotifyAuth } from "@/lib/spotify/auth";
|
import { SpotifyAuth } from '@/lib/spotify/auth'
|
||||||
|
|
||||||
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!;
|
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!
|
||||||
const SPOTIFY_CLIENT_SECRET = import.meta.env.SPOTIFY_CLIENT_SECRET!;
|
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!
|
||||||
const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI!;
|
const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI!
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/spotify/callback")({
|
export const ServerRoute = createServerFileRoute('/api/spotify/callback').methods({
|
||||||
server: {
|
GET: async ({ request }: { request: Request }) => {
|
||||||
handlers: {
|
const getReturnPath = (state: string | null): string => {
|
||||||
GET: async ({ request }: { request: Request }) => {
|
if (!state) return '/';
|
||||||
const getReturnPath = (state: string | null): string => {
|
try {
|
||||||
if (!state) return "/";
|
const decodedState = JSON.parse(atob(state));
|
||||||
try {
|
return decodedState.returnPath || '/';
|
||||||
const decodedState = JSON.parse(atob(state));
|
} catch {
|
||||||
return decodedState.returnPath || "/";
|
return '/';
|
||||||
} catch {
|
}
|
||||||
return "/";
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url)
|
||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get('code')
|
||||||
const state = url.searchParams.get("state");
|
const state = url.searchParams.get('state')
|
||||||
const error = url.searchParams.get("error");
|
const error = url.searchParams.get('error')
|
||||||
|
|
||||||
const returnPath = getReturnPath(state);
|
const returnPath = getReturnPath(state);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Spotify OAuth error:", error);
|
console.error('Spotify OAuth error:', error)
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: {
|
headers: {
|
||||||
Location:
|
'Location': returnPath + '?spotify_error=' + encodeURIComponent(error),
|
||||||
returnPath + "?spotify_error=" + encodeURIComponent(error),
|
},
|
||||||
},
|
})
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!code || !state) {
|
if (!code || !state) {
|
||||||
console.error("Missing code or state:", {
|
console.error('Missing code or state:', { code: !!code, state: !!state })
|
||||||
code: !!code,
|
return new Response(null, {
|
||||||
state: !!state,
|
status: 302,
|
||||||
});
|
headers: {
|
||||||
return new Response(null, {
|
'Location': returnPath + '?spotify_error=missing_code_or_state',
|
||||||
status: 302,
|
},
|
||||||
headers: {
|
})
|
||||||
Location: returnPath + "?spotify_error=missing_code_or_state",
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Token exchange attempt:", {
|
console.log('Token exchange attempt:', {
|
||||||
client_id: SPOTIFY_CLIENT_ID,
|
client_id: SPOTIFY_CLIENT_ID,
|
||||||
redirect_uri: SPOTIFY_REDIRECT_URI,
|
redirect_uri: SPOTIFY_REDIRECT_URI,
|
||||||
has_code: !!code,
|
has_code: !!code,
|
||||||
has_state: !!state,
|
has_state: !!state,
|
||||||
});
|
})
|
||||||
|
|
||||||
const tokenResponse = await fetch(
|
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', {
|
||||||
"https://accounts.spotify.com/api/token",
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
method: "POST",
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
headers: {
|
'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`,
|
||||||
"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',
|
||||||
body: new URLSearchParams({
|
code,
|
||||||
grant_type: "authorization_code",
|
redirect_uri: SPOTIFY_REDIRECT_URI,
|
||||||
code,
|
}),
|
||||||
redirect_uri: SPOTIFY_REDIRECT_URI,
|
})
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tokenResponse.ok) {
|
if (!tokenResponse.ok) {
|
||||||
const errorText = await tokenResponse.text();
|
const errorText = await tokenResponse.text()
|
||||||
console.error("Token exchange error:", {
|
console.error('Token exchange error:', {
|
||||||
status: tokenResponse.status,
|
status: tokenResponse.status,
|
||||||
statusText: tokenResponse.statusText,
|
statusText: tokenResponse.statusText,
|
||||||
body: errorText,
|
body: errorText,
|
||||||
redirect_uri: SPOTIFY_REDIRECT_URI,
|
redirect_uri: SPOTIFY_REDIRECT_URI,
|
||||||
});
|
})
|
||||||
|
|
||||||
const errorParam = encodeURIComponent(
|
const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`)
|
||||||
`${tokenResponse.status}: ${errorText}`
|
return new Response(null, {
|
||||||
);
|
status: 302,
|
||||||
return new Response(null, {
|
headers: {
|
||||||
status: 302,
|
'Location': `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`,
|
||||||
headers: {
|
},
|
||||||
Location: `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`,
|
})
|
||||||
},
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = await tokenResponse.json();
|
const tokens = await tokenResponse.json()
|
||||||
|
|
||||||
console.log("Token exchange successful:", {
|
console.log('Token exchange successful:', {
|
||||||
has_access_token: !!tokens.access_token,
|
has_access_token: !!tokens.access_token,
|
||||||
has_refresh_token: !!tokens.refresh_token,
|
has_refresh_token: !!tokens.refresh_token,
|
||||||
expires_in: tokens.expires_in,
|
expires_in: tokens.expires_in,
|
||||||
});
|
})
|
||||||
|
|
||||||
console.log("Decoded return path:", returnPath);
|
console.log('Decoded return path:', returnPath);
|
||||||
|
|
||||||
const response = new Response(null, {
|
const response = new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: {
|
headers: {
|
||||||
Location: returnPath + "?spotify_auth=success",
|
'Location': returnPath + '?spotify_auth=success',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const isSecure = import.meta.env.NODE_ENV === "production";
|
const isSecure = process.env.NODE_ENV === 'production'
|
||||||
const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`;
|
const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`
|
||||||
|
|
||||||
response.headers.append(
|
response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`)
|
||||||
"Set-Cookie",
|
|
||||||
`spotify_access_token=${tokens.access_token}; ${cookieOptions}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tokens.refresh_token) {
|
if (tokens.refresh_token) {
|
||||||
const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}`; // 30 days
|
const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}` // 30 days
|
||||||
response.headers.append(
|
response.headers.append('Set-Cookie', `spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`)
|
||||||
"Set-Cookie",
|
}
|
||||||
`spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Spotify callback error:", error);
|
console.error('Spotify callback error:', error)
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const state = url.searchParams.get("state");
|
const state = url.searchParams.get('state');
|
||||||
const returnPath = getReturnPath(state);
|
const returnPath = getReturnPath(state);
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: {
|
headers: {
|
||||||
Location: returnPath + "?spotify_error=callback_failed",
|
'Location': returnPath + '?spotify_error=callback_failed',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
@@ -1,60 +1,59 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||||
import { SpotifyWebApiClient } from "@/lib/spotify/client";
|
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||||
import type { SpotifyPlaybackSnapshot } from "@/lib/spotify/types";
|
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types'
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/spotify/capture")({
|
export const ServerRoute = createServerFileRoute('/api/spotify/capture').methods({
|
||||||
server: {
|
POST: async ({ request }: { request: Request }) => {
|
||||||
handlers: {
|
try {
|
||||||
POST: async ({ request }: { request: Request }) => {
|
// Get access token from cookies
|
||||||
try {
|
const cookies = request.headers.get('Cookie') || ''
|
||||||
const cookies = request.headers.get("Cookie") || "";
|
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/)
|
||||||
const accessTokenMatch = cookies.match(
|
|
||||||
/spotify_access_token=([^;]+)/
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!accessTokenMatch) {
|
if (!accessTokenMatch) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: "No access token found" }),
|
JSON.stringify({ error: 'No access token found' }),
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = decodeURIComponent(accessTokenMatch[1]);
|
const accessToken = decodeURIComponent(accessTokenMatch[1])
|
||||||
const spotifyClient = new SpotifyWebApiClient(accessToken);
|
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||||
|
|
||||||
const snapshot = await spotifyClient.createPlaybackSnapshot();
|
// Create a snapshot of the current playback state
|
||||||
|
const snapshot = await spotifyClient.createPlaybackSnapshot()
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: "No active playback to capture" }),
|
JSON.stringify({ error: 'No active playback to capture' }),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify({ snapshot }), {
|
return new Response(
|
||||||
status: 200,
|
JSON.stringify({ snapshot }),
|
||||||
headers: { "Content-Type": "application/json" },
|
{
|
||||||
});
|
status: 200,
|
||||||
} catch (error) {
|
headers: { 'Content-Type': 'application/json' }
|
||||||
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' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
@@ -1,203 +1,202 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||||
import { SpotifyWebApiClient } from "@/lib/spotify/client";
|
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||||
|
|
||||||
function getAccessTokenFromCookies(request: Request): string | null {
|
function getAccessTokenFromCookies(request: Request): string | null {
|
||||||
const cookieHeader = request.headers.get("cookie");
|
const cookieHeader = request.headers.get('cookie')
|
||||||
if (!cookieHeader) return null;
|
if (!cookieHeader) return null
|
||||||
|
|
||||||
const cookies = Object.fromEntries(
|
const cookies = Object.fromEntries(
|
||||||
cookieHeader.split("; ").map((c) => c.split("="))
|
cookieHeader.split('; ').map(c => c.split('='))
|
||||||
);
|
)
|
||||||
|
|
||||||
return cookies.spotify_access_token || null;
|
return cookies.spotify_access_token || null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/spotify/playback")({
|
export const ServerRoute = createServerFileRoute('/api/spotify/playback').methods({
|
||||||
server: {
|
POST: async ({ request }: { request: Request }) => {
|
||||||
handlers: {
|
try {
|
||||||
POST: async ({ request }: { request: Request }) => {
|
const accessToken = getAccessTokenFromCookies(request)
|
||||||
try {
|
if (!accessToken) {
|
||||||
const accessToken = getAccessTokenFromCookies(request);
|
return new Response(
|
||||||
if (!accessToken) {
|
JSON.stringify({ error: 'No access token found' }),
|
||||||
return new Response(
|
{
|
||||||
JSON.stringify({ error: "No access token found" }),
|
status: 401,
|
||||||
{
|
headers: { 'Content-Type': 'application/json' },
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json()
|
||||||
const { action, deviceId, volumePercent, trackId, positionMs } = body;
|
const { action, deviceId, volumePercent, trackId, positionMs } = body
|
||||||
|
|
||||||
const spotifyClient = new SpotifyWebApiClient(accessToken);
|
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "play":
|
case 'play':
|
||||||
await spotifyClient.play(deviceId);
|
await spotifyClient.play(deviceId)
|
||||||
break;
|
break
|
||||||
case "playTrack":
|
case 'playTrack':
|
||||||
if (!trackId) {
|
if (!trackId) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({ error: 'trackId is required for playTrack action' }),
|
||||||
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,
|
status: 400,
|
||||||
headers: { "Content-Type": "application/json" },
|
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" },
|
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
await spotifyClient.playTrack(trackId, deviceId, positionMs)
|
||||||
const url = new URL(request.url);
|
break
|
||||||
const type = url.searchParams.get("type");
|
case 'pause':
|
||||||
|
await spotifyClient.pause()
|
||||||
const spotifyClient = new SpotifyWebApiClient(accessToken);
|
break
|
||||||
|
case 'next':
|
||||||
if (type === "devices") {
|
await spotifyClient.skipToNext()
|
||||||
const devices = await spotifyClient.getDevices();
|
break
|
||||||
return new Response(JSON.stringify({ devices }), {
|
case 'previous':
|
||||||
status: 200,
|
await spotifyClient.skipToPrevious()
|
||||||
headers: { "Content-Type": "application/json" },
|
break
|
||||||
});
|
case 'volume':
|
||||||
} else if (type === "state") {
|
if (typeof volumePercent !== 'number') {
|
||||||
const playbackState = await spotifyClient.getPlaybackState();
|
return new Response(
|
||||||
return new Response(JSON.stringify({ playbackState }), {
|
JSON.stringify({ error: 'volumePercent must be a number' }),
|
||||||
status: 200,
|
{
|
||||||
headers: { "Content-Type": "application/json" },
|
status: 400,
|
||||||
});
|
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) {
|
await spotifyClient.setVolume(volumePercent)
|
||||||
console.error("Get playback data error:", error);
|
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(
|
return new Response(
|
||||||
JSON.stringify({ error: "Failed to get playback data" }),
|
JSON.stringify({ error: 'Invalid action' }),
|
||||||
{
|
{
|
||||||
status: 500,
|
status: 400,
|
||||||
headers: { "Content-Type": "application/json" },
|
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' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,71 +1,72 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||||
import { SpotifyWebApiClient } from "@/lib/spotify/client";
|
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||||
import type { SpotifyPlaybackSnapshot } from "@/lib/spotify/types";
|
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types'
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/spotify/resume")({
|
export const ServerRoute = createServerFileRoute('/api/spotify/resume').methods({
|
||||||
server: {
|
POST: async ({ request }: { request: Request }) => {
|
||||||
handlers: {
|
try {
|
||||||
POST: async ({ request }: { request: Request }) => {
|
// Get access token from cookies
|
||||||
try {
|
const cookies = request.headers.get('Cookie') || ''
|
||||||
const cookies = request.headers.get("Cookie") || "";
|
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/)
|
||||||
const accessTokenMatch = cookies.match(
|
|
||||||
/spotify_access_token=([^;]+)/
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!accessTokenMatch) {
|
if (!accessTokenMatch) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: "No access token found" }),
|
JSON.stringify({ error: 'No access token found' }),
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = decodeURIComponent(accessTokenMatch[1]);
|
const accessToken = decodeURIComponent(accessTokenMatch[1])
|
||||||
const spotifyClient = new SpotifyWebApiClient(accessToken);
|
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||||
|
|
||||||
const body = await request.json();
|
// Parse the request body to get the snapshot
|
||||||
const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot };
|
const body = await request.json()
|
||||||
|
const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot }
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: "No snapshot provided" }),
|
JSON.stringify({ error: 'No snapshot provided' }),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await spotifyClient.restorePlaybackSnapshot(snapshot);
|
// Restore the playback state from the snapshot
|
||||||
|
await spotifyClient.restorePlaybackSnapshot(snapshot)
|
||||||
|
|
||||||
return new Response(JSON.stringify({ success: true }), {
|
return new Response(
|
||||||
status: 200,
|
JSON.stringify({ success: true }),
|
||||||
headers: { "Content-Type": "application/json" },
|
{
|
||||||
});
|
status: 200,
|
||||||
} catch (error) {
|
headers: { 'Content-Type': 'application/json' }
|
||||||
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' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
@@ -1,87 +1,81 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||||
|
|
||||||
|
// Function to get Client Credentials access token
|
||||||
async function getClientCredentialsToken(): Promise<string> {
|
async function getClientCredentialsToken(): Promise<string> {
|
||||||
const clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID;
|
const clientId = process.env.VITE_SPOTIFY_CLIENT_ID
|
||||||
const clientSecret = import.meta.env.SPOTIFY_CLIENT_SECRET;
|
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET
|
||||||
|
|
||||||
if (!clientId || !clientSecret) {
|
if (!clientId || !clientSecret) {
|
||||||
throw new Error("Missing Spotify client credentials");
|
throw new Error('Missing Spotify client credentials')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch("https://accounts.spotify.com/api/token", {
|
const response = await fetch('https://accounts.spotify.com/api/token', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
|
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
|
||||||
},
|
},
|
||||||
body: "grant_type=client_credentials",
|
body: 'grant_type=client_credentials',
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to get Spotify access token");
|
throw new Error('Failed to get Spotify access token')
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json()
|
||||||
return data.access_token;
|
return data.access_token
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/spotify/search")({
|
export const ServerRoute = createServerFileRoute('/api/spotify/search').methods({
|
||||||
server: {
|
GET: async ({ request }: { request: Request }) => {
|
||||||
handlers: {
|
try {
|
||||||
GET: async ({ request }: { request: Request }) => {
|
const url = new URL(request.url)
|
||||||
try {
|
const query = url.searchParams.get('q')
|
||||||
const url = new URL(request.url);
|
|
||||||
const query = url.searchParams.get("q");
|
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: "Query parameter q is required" }),
|
JSON.stringify({ error: 'Query parameter q is required' }),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Get client credentials access token
|
// Get client credentials access token
|
||||||
const accessToken = await getClientCredentialsToken();
|
const accessToken = await getClientCredentialsToken()
|
||||||
|
|
||||||
// Search using Spotify API directly
|
// Search using Spotify API directly
|
||||||
const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`;
|
const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`
|
||||||
|
|
||||||
const searchResponse = await fetch(searchUrl, {
|
const searchResponse = await fetch(searchUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!searchResponse.ok) {
|
if (!searchResponse.ok) {
|
||||||
throw new Error("Spotify search request failed");
|
throw new Error('Spotify search request failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResult = await searchResponse.json();
|
const searchResult = await searchResponse.json()
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ tracks: searchResult.tracks.items }),
|
JSON.stringify({ tracks: searchResult.tracks.items }),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
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' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
@@ -1,131 +1,125 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||||
|
|
||||||
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!;
|
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!
|
||||||
const SPOTIFY_CLIENT_SECRET = import.meta.env.SPOTIFY_CLIENT_SECRET!;
|
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/spotify/token")({
|
export const ServerRoute = createServerFileRoute('/api/spotify/token').methods({
|
||||||
server: {
|
POST: async ({ request }: { request: Request }) => {
|
||||||
handlers: {
|
try {
|
||||||
POST: async ({ request }: { request: Request }) => {
|
const body = await request.json()
|
||||||
try {
|
const { refresh_token } = body
|
||||||
const body = await request.json();
|
|
||||||
const { refresh_token } = body;
|
|
||||||
|
|
||||||
if (!refresh_token) {
|
if (!refresh_token) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: "refresh_token is required" }),
|
JSON.stringify({ error: 'refresh_token is required' }),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const tokenResponse = await fetch(
|
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', {
|
||||||
"https://accounts.spotify.com/api/token",
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
method: "POST",
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
headers: {
|
'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`,
|
||||||
"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',
|
||||||
body: new URLSearchParams({
|
refresh_token,
|
||||||
grant_type: "refresh_token",
|
}),
|
||||||
refresh_token,
|
})
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tokenResponse.ok) {
|
if (!tokenResponse.ok) {
|
||||||
const error = await tokenResponse.json();
|
const error = await tokenResponse.json()
|
||||||
console.error("Token refresh error:", error);
|
console.error('Token refresh error:', error)
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({ error: 'Failed to refresh token', details: error }),
|
||||||
error: "Failed to refresh token",
|
{
|
||||||
details: error,
|
status: tokenResponse.status,
|
||||||
}),
|
headers: { 'Content-Type': 'application/json' },
|
||||||
{
|
|
||||||
status: tokenResponse.status,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const tokens = await tokenResponse.json();
|
const tokens = await tokenResponse.json()
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
access_token: tokens.access_token,
|
access_token: tokens.access_token,
|
||||||
expires_in: tokens.expires_in,
|
expires_in: tokens.expires_in,
|
||||||
scope: tokens.scope,
|
scope: tokens.scope,
|
||||||
token_type: tokens.token_type,
|
token_type: tokens.token_type,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
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) {
|
||||||
GET: async ({ request }: { request: Request }) => {
|
console.error('Token refresh endpoint error:', error)
|
||||||
try {
|
return new Response(
|
||||||
const cookieHeader = request.headers.get("cookie");
|
JSON.stringify({ error: 'Internal server error' }),
|
||||||
if (!cookieHeader) {
|
{
|
||||||
return new Response(JSON.stringify({ error: "No cookies found" }), {
|
status: 500,
|
||||||
status: 401,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
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' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,148 +1,117 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server';
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from '@/utils/supertokens';
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from '@/lib/pocketbase/client';
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from '@/lib/logger';
|
||||||
import { z } from "zod";
|
import { z } from 'zod';
|
||||||
|
|
||||||
const uploadSchema = z.object({
|
const uploadSchema = z.object({
|
||||||
teamId: z.string().min(1, "Team ID is required"),
|
teamId: z.string().min(1, 'Team ID is required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/teams/upload-logo")({
|
export const ServerRoute = createServerFileRoute('/api/teams/upload-logo')
|
||||||
server: {
|
.middleware([superTokensRequestMiddleware])
|
||||||
middleware: [superTokensRequestMiddleware],
|
.methods({
|
||||||
handlers: {
|
POST: async ({ request, context }) => {
|
||||||
POST: async ({ request, context }) => {
|
try {
|
||||||
try {
|
const userId = context.userAuthId;
|
||||||
const userId = context.userAuthId;
|
const isAdmin = context.roles.includes("Admin");
|
||||||
const isAdmin = context.roles.includes("Admin");
|
|
||||||
|
|
||||||
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
if (!userId) return new Response('Unauthenticated', { status: 401 });
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const teamId = formData.get("teamId") as string;
|
const teamId = formData.get('teamId') as string;
|
||||||
const logoFile = formData.get("logo") as File;
|
const logoFile = formData.get('logo') as File;
|
||||||
|
|
||||||
const validationResult = uploadSchema.safeParse({ teamId });
|
const validationResult = uploadSchema.safeParse({ teamId });
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
return new Response(
|
return new Response(JSON.stringify({
|
||||||
JSON.stringify({
|
error: 'Invalid input',
|
||||||
error: "Invalid input",
|
details: validationResult.error.issues
|
||||||
details: validationResult.error.issues,
|
}), {
|
||||||
}),
|
status: 400,
|
||||||
{
|
headers: { 'Content-Type': 'application/json' }
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
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(context.userAuthId)
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,145 +1,115 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createServerFileRoute } from '@tanstack/react-start/server';
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from '@/utils/supertokens';
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from '@/lib/pocketbase/client';
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from '@/lib/logger';
|
||||||
import { z } from "zod";
|
import { z } from 'zod';
|
||||||
|
|
||||||
const uploadSchema = z.object({
|
const uploadSchema = z.object({
|
||||||
tournamentId: z.string().min(1, "Tournament ID is required"),
|
tournamentId: z.string().min(1, 'Tournament ID is required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/tournaments/upload-logo")({
|
export const ServerRoute = createServerFileRoute('/api/tournaments/upload-logo')
|
||||||
server: {
|
.middleware([superTokensRequestMiddleware])
|
||||||
middleware: [superTokensRequestMiddleware],
|
.methods({
|
||||||
handlers: {
|
POST: async ({ request, context }) => {
|
||||||
POST: async ({ request, context }) => {
|
try {
|
||||||
try {
|
const userId = context.userAuthId;
|
||||||
const userId = context.userAuthId;
|
const isAdmin = context.roles.includes("Admin");
|
||||||
const isAdmin = context.roles.includes("Admin");
|
|
||||||
|
|
||||||
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
if (!userId) return new Response('Unauthenticated', { status: 401 });
|
||||||
if (!isAdmin) return new Response("Unauthorized", { status: 403 });
|
if (!isAdmin) return new Response('Unauthorized', { status: 403 });
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const tournamentId = formData.get("tournamentId") as string;
|
const tournamentId = formData.get('tournamentId') as string;
|
||||||
const logoFile = formData.get("logo") as File;
|
const logoFile = formData.get('logo') as File;
|
||||||
|
|
||||||
const validationResult = uploadSchema.safeParse({ tournamentId });
|
const validationResult = uploadSchema.safeParse({ tournamentId });
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
return new Response(
|
return new Response(JSON.stringify({
|
||||||
JSON.stringify({
|
error: 'Invalid input',
|
||||||
error: "Invalid input",
|
details: validationResult.error.issues
|
||||||
details: validationResult.error.issues,
|
}), {
|
||||||
}),
|
status: 400,
|
||||||
{
|
headers: { 'Content-Type': 'application/json' }
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,33 +1,38 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect } from 'react'
|
||||||
import FullScreenLoader from '@/components/full-screen-loader'
|
import FullScreenLoader from '@/components/full-screen-loader'
|
||||||
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'
|
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'
|
||||||
import { resetRefreshFlag } from '@/lib/supertokens/client'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/refresh-session')({
|
export const Route = createFileRoute('/refresh-session')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// https://supertokens.com/docs/additional-verification/session-verification/ssr?uiType=custom
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const hasAttemptedRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasAttemptedRef.current) return;
|
|
||||||
hasAttemptedRef.current = true;
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
try {
|
try {
|
||||||
resetRefreshFlag();
|
|
||||||
const refreshed = await attemptRefreshingSession()
|
const refreshed = await attemptRefreshingSession()
|
||||||
|
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const redirect = urlParams.get('redirect')
|
const redirect = urlParams.get('redirect')
|
||||||
|
|
||||||
if (redirect && !redirect.includes('_serverFn') && !redirect.includes('/api/')) {
|
const isServerFunction = redirect && (
|
||||||
|
redirect.startsWith('_serverFn') ||
|
||||||
|
redirect.startsWith('api/') ||
|
||||||
|
redirect.includes('_serverFn')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (redirect && !isServerFunction) {
|
||||||
window.location.href = decodeURIComponent(redirect)
|
window.location.href = decodeURIComponent(redirect)
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '/';
|
const referrer = document.referrer;
|
||||||
|
const referrerUrl = referrer && !referrer.includes('/_serverFn') && !referrer.includes('/api/')
|
||||||
|
? referrer
|
||||||
|
: '/';
|
||||||
|
|
||||||
|
window.location.href = referrerUrl;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
@@ -37,7 +42,8 @@ function RouteComponent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(handleRefresh, 100)
|
const timeout = setTimeout(handleRefresh, 100)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <FullScreenLoader />
|
return <FullScreenLoader />
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
user: data?.user,
|
user: data?.user || defaultAuthData.user,
|
||||||
metadata: data?.metadata || { accentColor: "blue" as MantineColor, colorScheme: "dark" as MantineColorScheme },
|
metadata: data?.metadata || defaultAuthData.metadata,
|
||||||
roles: data?.roles || [],
|
roles: data?.roles || defaultAuthData.roles,
|
||||||
phone: data?.phone || "",
|
phone: data?.phone || "",
|
||||||
set,
|
set,
|
||||||
}),
|
}),
|
||||||
[data, set]
|
[data, defaultAuthData]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <AuthContext value={value}>{children}</AuthContext>;
|
return <AuthContext value={value}>{children}</AuthContext>;
|
||||||
|
|||||||
@@ -438,7 +438,6 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
activeDevice,
|
activeDevice,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
// Capture/Resume state
|
|
||||||
capturedState,
|
capturedState,
|
||||||
isCaptureLoading,
|
isCaptureLoading,
|
||||||
isResumeLoading,
|
isResumeLoading,
|
||||||
@@ -453,11 +452,9 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
getDevices,
|
getDevices,
|
||||||
setActiveDevice,
|
setActiveDevice,
|
||||||
refreshPlaybackState,
|
refreshPlaybackState,
|
||||||
// Capture/Resume methods
|
|
||||||
capturePlaybackState,
|
capturePlaybackState,
|
||||||
resumePlaybackState,
|
resumePlaybackState,
|
||||||
clearCapturedState,
|
clearCapturedState,
|
||||||
// Search
|
|
||||||
searchTracks,
|
searchTracks,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
|||||||
const logger = new Logger("Bracket Generation");
|
const logger = new Logger("Bracket Generation");
|
||||||
|
|
||||||
export const previewBracket = createServerFn()
|
export const previewBracket = createServerFn()
|
||||||
.inputValidator(z.number())
|
.validator(z.number())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: teams }) =>
|
.handler(async ({ data: teams }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const orderedTeamsSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const generateTournamentBracket = createServerFn()
|
export const generateTournamentBracket = createServerFn()
|
||||||
.inputValidator(orderedTeamsSchema)
|
.validator(orderedTeamsSchema)
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
|
.handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -137,7 +137,7 @@ export const generateTournamentBracket = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const startMatch = createServerFn()
|
export const startMatch = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -170,7 +170,7 @@ const endMatchSchema = z.object({
|
|||||||
ot_count: z.number(),
|
ot_count: z.number(),
|
||||||
});
|
});
|
||||||
export const endMatch = createServerFn()
|
export const endMatch = createServerFn()
|
||||||
.inputValidator(endMatchSchema)
|
.validator(endMatchSchema)
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
|
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -252,7 +252,7 @@ const toggleReactionSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const toggleMatchReaction = createServerFn()
|
export const toggleMatchReaction = createServerFn()
|
||||||
.inputValidator(toggleReactionSchema)
|
.validator(toggleReactionSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: { matchId, emoji }, context }) =>
|
.handler(async ({ data: { matchId, emoji }, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -312,7 +312,7 @@ export interface Reaction {
|
|||||||
players: PlayerInfo[];
|
players: PlayerInfo[];
|
||||||
}
|
}
|
||||||
export const getMatchReactions = createServerFn()
|
export const getMatchReactions = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: matchId, context }) =>
|
.handler(async ({ data: matchId, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const useMe = () => {
|
|||||||
const errorData = error?.response?.data;
|
const errorData = error?.response?.data;
|
||||||
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
||||||
const currentUrl = window.location.pathname + window.location.search;
|
const currentUrl = window.location.pathname + window.location.search;
|
||||||
|
console.log('redirecting 3')
|
||||||
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { Match } from "@/features/matches/types";
|
|||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { logger } from ".";
|
import { logger } from ".";
|
||||||
import { getRequest } from "@tanstack/react-start/server";
|
import { getWebRequest } from "@tanstack/react-start/server";
|
||||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||||
|
|
||||||
export const fetchMe = createServerFn()
|
export const fetchMe = createServerFn()
|
||||||
.handler(async () =>
|
.handler(async () =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const request = getRequest();
|
const request = getWebRequest();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = await getSessionContext(request);
|
const context = await getSessionContext(request);
|
||||||
@@ -25,7 +25,7 @@ export const fetchMe = createServerFn()
|
|||||||
phone: context.phone
|
phone: context.phone
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// logger.info("FetchMe: Session error", error)
|
logger.info("FetchMe: Session error", error)
|
||||||
if (error?.response?.status === 401) {
|
if (error?.response?.status === 401) {
|
||||||
const errorData = error?.response?.data;
|
const errorData = error?.response?.data;
|
||||||
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
||||||
@@ -38,14 +38,14 @@ export const fetchMe = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getPlayer = createServerFn()
|
export const getPlayer = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult<Player>(async () => await pbAdmin.getPlayer(data))
|
toServerResult<Player>(async () => await pbAdmin.getPlayer(data))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updatePlayer = createServerFn()
|
export const updatePlayer = createServerFn()
|
||||||
.inputValidator(playerUpdateSchema)
|
.validator(playerUpdateSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -72,7 +72,7 @@ export const updatePlayer = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const createPlayer = createServerFn()
|
export const createPlayer = createServerFn()
|
||||||
.inputValidator(playerInputSchema)
|
.validator(playerInputSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -97,7 +97,7 @@ export const createPlayer = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const associatePlayer = createServerFn()
|
export const associatePlayer = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -129,7 +129,7 @@ export const getUnassociatedPlayers = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getPlayerStats = createServerFn()
|
export const getPlayerStats = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult<PlayerStats>(async () => await pbAdmin.getPlayerStats(data))
|
toServerResult<PlayerStats>(async () => await pbAdmin.getPlayerStats(data))
|
||||||
@@ -142,14 +142,14 @@ export const getAllPlayerStats = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getPlayerMatches = createServerFn()
|
export const getPlayerMatches = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult<Match[]>(async () => await pbAdmin.getPlayerMatches(data))
|
toServerResult<Match[]>(async () => await pbAdmin.getPlayerMatches(data))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getUnenrolledPlayers = createServerFn()
|
export const getUnenrolledPlayers = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: tournamentId }) =>
|
.handler(async ({ data: tournamentId }) =>
|
||||||
toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId))
|
toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId))
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ export const listTeamInfos = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getTeam = createServerFn()
|
export const getTeam = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: teamId }) =>
|
.handler(async ({ data: teamId }) =>
|
||||||
toServerResult(() => pbAdmin.getTeam(teamId))
|
toServerResult(() => pbAdmin.getTeam(teamId))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getTeamInfo = createServerFn()
|
export const getTeamInfo = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: teamId }) =>
|
.handler(async ({ data: teamId }) =>
|
||||||
toServerResult(() => pbAdmin.getTeamInfo(teamId))
|
toServerResult(() => pbAdmin.getTeamInfo(teamId))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const createTeam = createServerFn()
|
export const createTeam = createServerFn()
|
||||||
.inputValidator(teamInputSchema)
|
.validator(teamInputSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data, context }) =>
|
.handler(async ({ data, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -46,7 +46,7 @@ export const createTeam = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const updateTeam = createServerFn()
|
export const updateTeam = createServerFn()
|
||||||
.inputValidator(z.object({
|
.validator(z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
updates: teamUpdateSchema
|
updates: teamUpdateSchema
|
||||||
}))
|
}))
|
||||||
@@ -72,14 +72,14 @@ export const updateTeam = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getTeamStats = createServerFn()
|
export const getTeamStats = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: teamId }) =>
|
.handler(async ({ data: teamId }) =>
|
||||||
toServerResult(() => pbAdmin.getTeamStats(teamId))
|
toServerResult(() => pbAdmin.getTeamStats(teamId))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getTeamMatches = createServerFn()
|
export const getTeamMatches = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult<Match[]>(async () => await pbAdmin.getTeamMatches(data))
|
toServerResult<Match[]>(async () => await pbAdmin.getTeamMatches(data))
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export const listTournaments = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const createTournament = createServerFn()
|
export const createTournament = createServerFn()
|
||||||
.inputValidator(tournamentInputSchema)
|
.validator(tournamentInputSchema)
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(() => pbAdmin.createTournament(data))
|
toServerResult(() => pbAdmin.createTournament(data))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateTournament = createServerFn()
|
export const updateTournament = createServerFn()
|
||||||
.inputValidator(z.object({
|
.validator(z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
updates: tournamentInputSchema.partial()
|
updates: tournamentInputSchema.partial()
|
||||||
}))
|
}))
|
||||||
@@ -30,7 +30,7 @@ export const updateTournament = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getTournament = createServerFn()
|
export const getTournament = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: tournamentId, context }) => {
|
.handler(async ({ data: tournamentId, context }) => {
|
||||||
const isAdmin = context.roles.includes("Admin");
|
const isAdmin = context.roles.includes("Admin");
|
||||||
@@ -44,7 +44,7 @@ export const getCurrentTournament = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const enrollTeam = createServerFn()
|
export const enrollTeam = createServerFn()
|
||||||
.inputValidator(z.object({
|
.validator(z.object({
|
||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
teamId: z.string()
|
teamId: z.string()
|
||||||
}))
|
}))
|
||||||
@@ -70,7 +70,7 @@ export const enrollTeam = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const unenrollTeam = createServerFn()
|
export const unenrollTeam = createServerFn()
|
||||||
.inputValidator(z.object({
|
.validator(z.object({
|
||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
teamId: z.string()
|
teamId: z.string()
|
||||||
}))
|
}))
|
||||||
@@ -80,21 +80,21 @@ export const unenrollTeam = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getUnenrolledTeams = createServerFn()
|
export const getUnenrolledTeams = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async ({ data: tournamentId }) =>
|
.handler(async ({ data: tournamentId }) =>
|
||||||
toServerResult(() => pbAdmin.getUnenrolledTeams(tournamentId))
|
toServerResult(() => pbAdmin.getUnenrolledTeams(tournamentId))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getFreeAgents = createServerFn()
|
export const getFreeAgents = createServerFn()
|
||||||
.inputValidator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async ({ data: tournamentId }) =>
|
.handler(async ({ data: tournamentId }) =>
|
||||||
toServerResult(() => pbAdmin.getFreeAgents(tournamentId))
|
toServerResult(() => pbAdmin.getFreeAgents(tournamentId))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const enrollFreeAgent = createServerFn()
|
export const enrollFreeAgent = createServerFn()
|
||||||
.inputValidator(z.object({ phone: z.string(), tournamentId: z.string() }))
|
.validator(z.object({ phone: z.string(), tournamentId: z.string() }))
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
@@ -108,7 +108,7 @@ export const enrollFreeAgent = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const unenrollFreeAgent = createServerFn()
|
export const unenrollFreeAgent = createServerFn()
|
||||||
.inputValidator(z.object({ tournamentId: z.string() }))
|
.validator(z.object({ tournamentId: z.string() }))
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export function useServerEvents() {
|
|||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
if (!user?.id) return;
|
if (!user?.id) return;
|
||||||
|
|
||||||
shouldConnectRef.current = true;
|
shouldConnectRef.current = true;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class Logger {
|
|||||||
constructor(context?: string, options: LoggerOptions = {}) {
|
constructor(context?: string, options: LoggerOptions = {}) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.options = {
|
this.options = {
|
||||||
enabled: import.meta.env.NODE_ENV !== "production",
|
enabled: process.env.NODE_ENV !== "production",
|
||||||
showTimestamp: true,
|
showTimestamp: true,
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
colors: true,
|
colors: true,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { createTournamentsService } from "./services/tournaments";
|
|||||||
import { createTeamsService } from "./services/teams";
|
import { createTeamsService } from "./services/teams";
|
||||||
import { createMatchesService } from "./services/matches";
|
import { createMatchesService } from "./services/matches";
|
||||||
import { createReactionsService } from "./services/reactions";
|
import { createReactionsService } from "./services/reactions";
|
||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
class PocketBaseAdminClient {
|
class PocketBaseAdminClient {
|
||||||
private pb: PocketBase;
|
private pb: PocketBase;
|
||||||
|
|||||||
@@ -4,31 +4,6 @@ import Passwordless from "supertokens-web-js/recipe/passwordless";
|
|||||||
import { appInfo } from "./config";
|
import { appInfo } from "./config";
|
||||||
import { logger } from "./";
|
import { logger } from "./";
|
||||||
|
|
||||||
let refreshAttemptCount = 0;
|
|
||||||
|
|
||||||
export const resetRefreshFlag = () => {
|
|
||||||
refreshAttemptCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupFetchInterceptor = () => {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
const originalFetch = window.fetch;
|
|
||||||
window.fetch = async (resource: RequestInfo | URL, options?: RequestInit) => {
|
|
||||||
const url = typeof resource === 'string' ? resource :
|
|
||||||
resource instanceof URL ? resource.toString() : resource.url;
|
|
||||||
|
|
||||||
if (url.includes('/api/auth/session/refresh')) {
|
|
||||||
refreshAttemptCount++;
|
|
||||||
if (refreshAttemptCount > 1) {
|
|
||||||
throw new Error('Duplicate refresh attempt blocked');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalFetch.call(window, resource, options);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const frontendConfig = () => {
|
export const frontendConfig = () => {
|
||||||
return {
|
return {
|
||||||
appInfo,
|
appInfo,
|
||||||
@@ -37,6 +12,7 @@ export const frontendConfig = () => {
|
|||||||
Session.init({
|
Session.init({
|
||||||
tokenTransferMethod: "cookie",
|
tokenTransferMethod: "cookie",
|
||||||
sessionTokenBackendDomain: undefined,
|
sessionTokenBackendDomain: undefined,
|
||||||
|
|
||||||
preAPIHook: async (context) => {
|
preAPIHook: async (context) => {
|
||||||
context.requestInit.credentials = "include";
|
context.requestInit.credentials = "include";
|
||||||
return context;
|
return context;
|
||||||
@@ -47,14 +23,16 @@ export const frontendConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
export function ensureSuperTokensFrontend() {
|
export function ensureSuperTokensFrontend() {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
setupFetchInterceptor();
|
|
||||||
SuperTokens.init(frontendConfig());
|
SuperTokens.init(frontendConfig());
|
||||||
initialized = true;
|
initialized = true;
|
||||||
logger.info("SuperTokens initialized");
|
logger.info("Initialized");
|
||||||
|
|
||||||
|
Session.doesSessionExist().then((exists) => {
|
||||||
|
logger.info(`Session does${exists ? "" : "NOT"} exist on load!`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import UserRoles from "supertokens-node/recipe/userroles";
|
|||||||
import { appInfo } from "./config";
|
import { appInfo } from "./config";
|
||||||
import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode";
|
import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode";
|
||||||
import { logger } from "./";
|
import { logger } from "./";
|
||||||
import passwordlessTwilioVerify from "./recipes/passwordless-twilio-verify";
|
|
||||||
|
|
||||||
export const backendConfig = (): TypeInput => {
|
export const backendConfig = (): TypeInput => {
|
||||||
return {
|
return {
|
||||||
@@ -20,10 +19,10 @@ export const backendConfig = (): TypeInput => {
|
|||||||
PasswordlessDevelopmentMode.init(),
|
PasswordlessDevelopmentMode.init(),
|
||||||
Session.init({
|
Session.init({
|
||||||
cookieSameSite: "lax",
|
cookieSameSite: "lax",
|
||||||
cookieSecure: import.meta.env.NODE_ENV === "production",
|
cookieSecure: process.env.NODE_ENV === "production",
|
||||||
cookieDomain:
|
cookieDomain:
|
||||||
import.meta.env.NODE_ENV === "production" ? ".example.com" : undefined,
|
process.env.NODE_ENV === "production" ? ".example.com" : undefined,
|
||||||
antiCsrf: import.meta.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE",
|
antiCsrf: process.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE",
|
||||||
|
|
||||||
// Debug only
|
// Debug only
|
||||||
exposeAccessTokenToFrontendInCookieBasedAuth: true,
|
exposeAccessTokenToFrontendInCookieBasedAuth: true,
|
||||||
@@ -31,13 +30,13 @@ export const backendConfig = (): TypeInput => {
|
|||||||
Dashboard.init(),
|
Dashboard.init(),
|
||||||
UserRoles.init(),
|
UserRoles.init(),
|
||||||
],
|
],
|
||||||
telemetry: import.meta.env.NODE_ENV !== "production",
|
telemetry: process.env.NODE_ENV !== "production",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
export function ensureSuperTokensBackend() {
|
export function ensureSuperTokensBackend() {
|
||||||
if (!initialized && typeof window === 'undefined') {
|
if (!initialized) {
|
||||||
SuperTokens.init(backendConfig());
|
SuperTokens.init(backendConfig());
|
||||||
initialized = true;
|
initialized = true;
|
||||||
logger.simple("Backend initialized");
|
logger.simple("Backend initialized");
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { useMutation, UseMutationOptions } from "@tanstack/react-query";
|
|||||||
import { ServerResult } from "../types";
|
import { ServerResult } from "../types";
|
||||||
import toast from '@/lib/sonner'
|
import toast from '@/lib/sonner'
|
||||||
|
|
||||||
let isMutationRefreshingSession = false;
|
|
||||||
|
|
||||||
export function useServerMutation<TData, TVariables = unknown>(
|
export function useServerMutation<TData, TVariables = unknown>(
|
||||||
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
|
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
|
||||||
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
|
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
|
||||||
@@ -44,14 +42,9 @@ export function useServerMutation<TData, TVariables = unknown>(
|
|||||||
: error.response.data;
|
: error.response.data;
|
||||||
|
|
||||||
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
||||||
if (!isMutationRefreshingSession) {
|
const currentUrl = window.location.pathname + window.location.search;
|
||||||
isMutationRefreshingSession = true;
|
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
||||||
const currentUrl = window.location.pathname + window.location.search;
|
console.log('redirecting 2')
|
||||||
setTimeout(() => {
|
|
||||||
isMutationRefreshingSession = false;
|
|
||||||
}, 1000);
|
|
||||||
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
|
||||||
}
|
|
||||||
throw new Error("SESSION_REFRESH_REQUIRED");
|
throw new Error("SESSION_REFRESH_REQUIRED");
|
||||||
}
|
}
|
||||||
} catch (parseError) {}
|
} catch (parseError) {}
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import type { Twilio } from "twilio";
|
import twilio from "twilio";
|
||||||
|
|
||||||
const accountSid = process.env.TWILIO_ACCOUNT_SID!;
|
const accountSid = process.env.TWILIO_ACCOUNT_SID!;
|
||||||
const authToken = process.env.TWILIO_AUTH_TOKEN!;
|
const authToken = process.env.TWILIO_AUTH_TOKEN!;
|
||||||
const serviceSid = process.env.TWILIO_SERVICE_SID!;
|
const serviceSid = process.env.TWILIO_SERVICE_SID!;
|
||||||
|
|
||||||
let client: Twilio;
|
const client = twilio(accountSid, authToken);
|
||||||
|
|
||||||
function getTwilioClient() {
|
|
||||||
if (!client) {
|
|
||||||
const twilio = require("twilio");
|
|
||||||
client = twilio(accountSid, authToken);
|
|
||||||
}
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendVerifyCode(phoneNumber: string, code: string) {
|
export async function sendVerifyCode(phoneNumber: string, code: string) {
|
||||||
const twilioClient = getTwilioClient();
|
const verification = await client.verify.v2
|
||||||
|
|
||||||
const verification = await twilioClient!.verify.v2
|
|
||||||
.services(serviceSid)
|
.services(serviceSid)
|
||||||
.verifications.create({
|
.verifications.create({
|
||||||
channel: "sms",
|
channel: "sms",
|
||||||
@@ -33,9 +23,7 @@ export async function sendVerifyCode(phoneNumber: string, code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateVerify(sid: string) {
|
export async function updateVerify(sid: string) {
|
||||||
const twilioClient = getTwilioClient();
|
const verification = await client.verify.v2
|
||||||
|
|
||||||
const verification = await twilioClient!.verify.v2
|
|
||||||
.services(serviceSid)
|
.services(serviceSid)
|
||||||
.verifications(sid)
|
.verifications(sid)
|
||||||
.update({ status: "approved" });
|
.update({ status: "approved" });
|
||||||
|
|||||||
@@ -1,25 +1,48 @@
|
|||||||
import {
|
import {
|
||||||
createMiddleware,
|
createMiddleware,
|
||||||
createServerFn,
|
createServerFn,
|
||||||
createServerOnlyFn,
|
ServerFnResponseType,
|
||||||
} from "@tanstack/react-start";
|
} from "@tanstack/react-start";
|
||||||
import { getRequest, setResponseHeader } from "@tanstack/react-start/server";
|
import { getWebRequest } from "@tanstack/react-start/server";
|
||||||
import { redirect as redirect } from "@tanstack/react-router";
|
import { redirect as redirect } from "@tanstack/react-router";
|
||||||
import UserRoles from "supertokens-node/recipe/userroles";
|
import UserRoles from "supertokens-node/recipe/userroles";
|
||||||
import UserMetadata from "supertokens-node/recipe/usermetadata";
|
import UserMetadata from "supertokens-node/recipe/usermetadata";
|
||||||
import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
|
import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
|
||||||
import { Logger } from "@/lib/logger";
|
import { Logger } from "@/lib/logger";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import { refreshSession } from "supertokens-node/recipe/session";
|
||||||
|
|
||||||
const logger = new Logger("Middleware");
|
const logger = new Logger("Middleware");
|
||||||
|
|
||||||
const verifySuperTokensSession = async (
|
export const verifySuperTokensSession = async (
|
||||||
request: Request
|
request: Request,
|
||||||
|
response?: ServerFnResponseType
|
||||||
) => {
|
) => {
|
||||||
let session = await getSessionForStart(request, { sessionRequired: false });
|
let session = await getSessionForStart(request, { sessionRequired: false });
|
||||||
|
|
||||||
if (session?.needsRefresh) {
|
if (session?.needsRefresh) {
|
||||||
logger.info("Session needs refresh - redirecting to client");
|
logger.info("Session needs refresh");
|
||||||
return { context: { session: { tryRefresh: true } } };
|
|
||||||
|
try {
|
||||||
|
if (response) {
|
||||||
|
const refreshedSession = await refreshSession(request, response);
|
||||||
|
if (refreshedSession) {
|
||||||
|
session = await getSessionForStart(request, { sessionRequired: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session?.needsRefresh) {
|
||||||
|
return { context: { session: { tryRefresh: true } } };
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("Session refresh error", error);
|
||||||
|
|
||||||
|
if (error.type === 'UNAUTHORISED' || error.type === 'TOKEN_THEFT_DETECTED') {
|
||||||
|
return { context: { userAuthId: null, roles: [] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { context: { session: { tryRefresh: true } } };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAuthId = session?.userId;
|
const userAuthId = session?.userId;
|
||||||
@@ -46,7 +69,7 @@ const verifySuperTokensSession = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSessionContext = createServerOnlyFn(async (request: Request, options?: { isServerFunction?: boolean }) => {
|
export const getSessionContext = async (request: Request, options?: { isServerFunction?: boolean }) => {
|
||||||
const session = await verifySuperTokensSession(request);
|
const session = await verifySuperTokensSession(request);
|
||||||
|
|
||||||
if (session.context.session?.tryRefresh) {
|
if (session.context.session?.tryRefresh) {
|
||||||
@@ -56,6 +79,7 @@ export const getSessionContext = createServerOnlyFn(async (request: Request, opt
|
|||||||
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const from = encodeURIComponent(url.pathname + url.search);
|
const from = encodeURIComponent(url.pathname + url.search);
|
||||||
|
console.log('redirecting')
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/refresh-session",
|
to: "/refresh-session",
|
||||||
search: { redirect: from }
|
search: { redirect: from }
|
||||||
@@ -75,7 +99,7 @@ export const getSessionContext = createServerOnlyFn(async (request: Request, opt
|
|||||||
};
|
};
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
});
|
};
|
||||||
|
|
||||||
export const superTokensRequestMiddleware = createMiddleware({
|
export const superTokensRequestMiddleware = createMiddleware({
|
||||||
type: "request",
|
type: "request",
|
||||||
@@ -86,8 +110,8 @@ export const superTokensRequestMiddleware = createMiddleware({
|
|||||||
|
|
||||||
export const superTokensFunctionMiddleware = createMiddleware({
|
export const superTokensFunctionMiddleware = createMiddleware({
|
||||||
type: "function",
|
type: "function",
|
||||||
}).server(async ({ next }) => {
|
}).server(async ({ next, response }) => {
|
||||||
const request = getRequest();
|
const request = getWebRequest();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = await getSessionContext(request, { isServerFunction: true });
|
const context = await getSessionContext(request, { isServerFunction: true });
|
||||||
@@ -112,7 +136,7 @@ export const superTokensFunctionMiddleware = createMiddleware({
|
|||||||
export const superTokensAdminFunctionMiddleware = createMiddleware({
|
export const superTokensAdminFunctionMiddleware = createMiddleware({
|
||||||
type: "function",
|
type: "function",
|
||||||
}).server(async ({ next }) => {
|
}).server(async ({ next }) => {
|
||||||
const request = getRequest();
|
const request = getWebRequest();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = await getSessionContext(request, { isServerFunction: true });
|
const context = await getSessionContext(request, { isServerFunction: true });
|
||||||
@@ -146,7 +170,7 @@ export const fetchUserRoles = async (userAuthId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setUserMetadata = createServerFn({ method: "POST" })
|
export const setUserMetadata = createServerFn({ method: "POST" })
|
||||||
.inputValidator(
|
.validator(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
first_name: z
|
first_name: z
|
||||||
@@ -189,7 +213,7 @@ export const setUserMetadata = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const updateUserColorScheme = createServerFn({ method: "POST" })
|
export const updateUserColorScheme = createServerFn({ method: "POST" })
|
||||||
.inputValidator((data: string) => data)
|
.validator((data: string) => data)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ context, data }) => {
|
.handler(async ({ context, data }) => {
|
||||||
const { userAuthId, metadata } = context;
|
const { userAuthId, metadata } = context;
|
||||||
@@ -208,7 +232,7 @@ export const updateUserColorScheme = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const updateUserAccentColor = createServerFn({ method: "POST" })
|
export const updateUserAccentColor = createServerFn({ method: "POST" })
|
||||||
.inputValidator((data: string) => data)
|
.validator((data: string) => data)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ context, data }) => {
|
.handler(async ({ context, data }) => {
|
||||||
const { userAuthId, metadata } = context;
|
const { userAuthId, metadata } = context;
|
||||||
|
|||||||
@@ -6,15 +6,23 @@ import react from '@vitejs/plugin-react';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
allowedHosts: ["dev.flexxon.app"]
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tsConfigPaths({
|
tsConfigPaths({
|
||||||
projects: ['./tsconfig.json'],
|
projects: ['./tsconfig.json'],
|
||||||
}),
|
}),
|
||||||
tanstackStart({
|
tanstackStart({
|
||||||
srcDirectory: 'src/app',
|
customViteReactPlugin: true,
|
||||||
|
tsr: {
|
||||||
|
srcDirectory: 'src/app',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
react()
|
react()
|
||||||
]
|
],
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
ssr: {
|
||||||
|
target: 'node',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user