Compare commits

2 Commits

Author SHA1 Message Date
yohlo
e4164cbc71 attempted upgrade 2025-09-24 00:13:41 -05:00
yohlo
94ea44c66e drawer fixes 2025-09-23 15:04:29 -05:00
30 changed files with 1421 additions and 1281 deletions

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { routeTree } from "./routeTree.gen";
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary"; import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config"; import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
export function createRouter() { export function getRouter() {
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
@@ -40,6 +40,6 @@ export function createRouter() {
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: ReturnType<typeof createRouter>; router: ReturnType<typeof getRouter>;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import { Box, Container, Flex, Loader, useComputedColorScheme } from "@mantine/core"; import { Box, Container, Flex, Loader, useComputedColorScheme } from "@mantine/core";
import { PropsWithChildren, Suspense, useEffect } from "react"; import { PropsWithChildren, Suspense, useEffect, useRef } from "react";
import { Drawer as VaulDrawer } from "vaul"; import { Drawer as VaulDrawer } from "vaul";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import FullScreenLoader from "../full-screen-loader";
interface DrawerProps extends PropsWithChildren { interface DrawerProps extends PropsWithChildren {
title?: string; title?: string;
@@ -17,6 +16,7 @@ const Drawer: React.FC<DrawerProps> = ({
onChange, onChange,
}) => { }) => {
const colorScheme = useComputedColorScheme("light"); const colorScheme = useComputedColorScheme("light");
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const appElement = document.querySelector(".app") as HTMLElement; const appElement = document.querySelector(".app") as HTMLElement;
@@ -59,11 +59,31 @@ const Drawer: React.FC<DrawerProps> = ({
}; };
}, [opened, colorScheme]); }, [opened, colorScheme]);
useEffect(() => {
if (!opened || !contentRef.current) return;
const resizeObserver = new ResizeObserver(() => {
if (contentRef.current) {
const drawerContent = contentRef.current.closest('[data-vaul-drawer-wrapper]');
if (drawerContent) {
(drawerContent as HTMLElement).style.height = 'auto';
(drawerContent as HTMLElement).offsetHeight;
}
}
});
resizeObserver.observe(contentRef.current);
return () => {
resizeObserver.disconnect();
};
}, [opened, children]);
return ( return (
<VaulDrawer.Root open={opened} onOpenChange={onChange}> <VaulDrawer.Root open={opened} onOpenChange={onChange}>
<VaulDrawer.Portal> <VaulDrawer.Portal>
<VaulDrawer.Overlay className={styles.drawerOverlay} /> <VaulDrawer.Overlay className={styles.drawerOverlay} />
<VaulDrawer.Content className={styles.drawerContent} aria-describedby="drawer"> <VaulDrawer.Content className={styles.drawerContent} aria-describedby="drawer" ref={contentRef}>
<Container flex={1} p="md"> <Container flex={1} p="md">
<Box <Box
mb="sm" mb="sm"
@@ -74,7 +94,7 @@ const Drawer: React.FC<DrawerProps> = ({
mr="auto" mr="auto"
style={{ borderRadius: "9999px" }} style={{ borderRadius: "9999px" }}
/> />
<Container mah="fit-content" mx="auto" maw="28rem" px={0}> <Container mx="auto" maw="28rem" px={0}>
<VaulDrawer.Title>{title}</VaulDrawer.Title> <VaulDrawer.Title>{title}</VaulDrawer.Title>
<Suspense fallback={ <Suspense fallback={
<Flex justify='center' align='center' w='100%' h={400}> <Flex justify='center' align='center' w='100%' h={400}>

View File

@@ -2,7 +2,7 @@ import { PropsWithChildren, useCallback } from "react";
import { useIsMobile } from "@/hooks/use-is-mobile"; import { useIsMobile } from "@/hooks/use-is-mobile";
import Drawer from "./drawer"; import Drawer from "./drawer";
import Modal from "./modal"; import Modal from "./modal";
import { Box, ScrollArea } from "@mantine/core"; import { ScrollArea } from "@mantine/core";
interface SheetProps extends PropsWithChildren { interface SheetProps extends PropsWithChildren {
title?: string; title?: string;
@@ -29,7 +29,7 @@ const Sheet: React.FC<SheetProps> = ({ title, children, opened, onChange }) => {
scrollbars="y" scrollbars="y"
type="scroll" type="scroll"
> >
<Box mah="70vh">{children}</Box> {children}
</ScrollArea> </ScrollArea>
</SheetComponent> </SheetComponent>
); );

View File

@@ -11,10 +11,13 @@
border-top-left-radius: 20px; border-top-left-radius: 20px;
border-top-right-radius: 20px; border-top-right-radius: 20px;
margin-top: 24px; margin-top: 24px;
height: fit-content; height: auto !important;
min-height: fit-content;
max-height: 100dvh;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
outline: none; outline: none;
transition: height 0.2s ease-out;
} }

View File

@@ -107,7 +107,7 @@ function SwipeableTabs({
const activeSlideRef = slideRefs.current[activeTab]; const activeSlideRef = slideRefs.current[activeTab];
if (!activeSlideRef) return; if (!activeSlideRef) return;
let timeoutId: number; let timeoutId: any;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(updateHeight, 16); timeoutId = setTimeout(updateHeight, 16);

View File

@@ -19,7 +19,7 @@ const AdminPage = () => {
label="Open Pocketbase" label="Open Pocketbase"
Icon={DatabaseIcon} Icon={DatabaseIcon}
onClick={() => onClick={() =>
window.location.replace(process.env.POCKETBASE_URL! + "/_/") window.location.replace(import.meta.env.POCKETBASE_URL! + "/_/")
} }
/> />
<ListLink <ListLink

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,23 +6,24 @@ 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 {
framework: "custom", framework: "custom",
supertokens: { supertokens: {
connectionURI: connectionURI:
process.env.SUPERTOKENS_URI || "https://try.supertokens.io", import.meta.env.SUPERTOKENS_URI || "https://try.supertokens.io",
}, },
appInfo, appInfo,
recipeList: [ recipeList: [
PasswordlessDevelopmentMode.init(), PasswordlessDevelopmentMode.init(),
Session.init({ Session.init({
cookieSameSite: "lax", cookieSameSite: "lax",
cookieSecure: process.env.NODE_ENV === "production", cookieSecure: import.meta.env.NODE_ENV === "production",
cookieDomain: cookieDomain:
process.env.NODE_ENV === "production" ? ".example.com" : undefined, import.meta.env.NODE_ENV === "production" ? ".example.com" : undefined,
antiCsrf: process.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE", antiCsrf: import.meta.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE",
// Debug only // Debug only
exposeAccessTokenToFrontendInCookieBasedAuth: true, exposeAccessTokenToFrontendInCookieBasedAuth: true,
@@ -30,13 +31,13 @@ export const backendConfig = (): TypeInput => {
Dashboard.init(), Dashboard.init(),
UserRoles.init(), UserRoles.init(),
], ],
telemetry: process.env.NODE_ENV !== "production", telemetry: import.meta.env.NODE_ENV !== "production",
}; };
}; };
let initialized = false; let initialized = false;
export function ensureSuperTokensBackend() { export function ensureSuperTokensBackend() {
if (!initialized) { if (!initialized && typeof window === 'undefined') {
SuperTokens.init(backendConfig()); SuperTokens.init(backendConfig());
initialized = true; initialized = true;
logger.simple("Backend initialized"); logger.simple("Backend initialized");

View File

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

View File

@@ -1,9 +1,8 @@
import { import {
createMiddleware, createMiddleware,
createServerFn, createServerFn,
ServerFnResponseType,
} from "@tanstack/react-start"; } from "@tanstack/react-start";
import { getWebRequest } from "@tanstack/react-start/server"; import { getRequest, setResponseHeader } from "@tanstack/react-start/server";
import { redirect as redirect } from "@tanstack/react-router"; import { redirect as redirect } from "@tanstack/react-router";
import UserRoles from "supertokens-node/recipe/userroles"; import UserRoles from "supertokens-node/recipe/userroles";
import UserMetadata from "supertokens-node/recipe/usermetadata"; import UserMetadata from "supertokens-node/recipe/usermetadata";
@@ -15,8 +14,7 @@ import { refreshSession } from "supertokens-node/recipe/session";
const logger = new Logger("Middleware"); const logger = new Logger("Middleware");
export 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 });
@@ -24,13 +22,17 @@ export const verifySuperTokensSession = async (
logger.info("Session needs refresh"); logger.info("Session needs refresh");
try { try {
if (response) { const refreshedSession = await refreshSession(request, {
const refreshedSession = await refreshSession(request, response); setHeader: (key: string, value: string) => {
setResponseHeader(key, value);
},
setCookie: (cookie: string) => {
setResponseHeader('Set-Cookie', cookie);
}
});
if (refreshedSession) { if (refreshedSession) {
session = await getSessionForStart(request, { sessionRequired: false }); session = await getSessionForStart(request, { sessionRequired: false });
} }
}
if (session?.needsRefresh) { if (session?.needsRefresh) {
return { context: { session: { tryRefresh: true } } }; return { context: { session: { tryRefresh: true } } };
} }
@@ -109,8 +111,8 @@ export const superTokensRequestMiddleware = createMiddleware({
export const superTokensFunctionMiddleware = createMiddleware({ export const superTokensFunctionMiddleware = createMiddleware({
type: "function", type: "function",
}).server(async ({ next, response }) => { }).server(async ({ next }) => {
const request = getWebRequest(); const request = getRequest();
try { try {
const context = await getSessionContext(request, { isServerFunction: true }); const context = await getSessionContext(request, { isServerFunction: true });
@@ -135,7 +137,7 @@ export const superTokensFunctionMiddleware = createMiddleware({
export const superTokensAdminFunctionMiddleware = createMiddleware({ export const superTokensAdminFunctionMiddleware = createMiddleware({
type: "function", type: "function",
}).server(async ({ next }) => { }).server(async ({ next }) => {
const request = getWebRequest(); const request = getRequest();
try { try {
const context = await getSessionContext(request, { isServerFunction: true }); const context = await getSessionContext(request, { isServerFunction: true });
@@ -169,7 +171,7 @@ export const fetchUserRoles = async (userAuthId: string) => {
}; };
export const setUserMetadata = createServerFn({ method: "POST" }) export const setUserMetadata = createServerFn({ method: "POST" })
.validator( .inputValidator(
z z
.object({ .object({
first_name: z first_name: z
@@ -212,7 +214,7 @@ export const setUserMetadata = createServerFn({ method: "POST" })
}); });
export const updateUserColorScheme = createServerFn({ method: "POST" }) export const updateUserColorScheme = createServerFn({ method: "POST" })
.validator((data: string) => data) .inputValidator((data: string) => data)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => { .handler(async ({ context, data }) => {
const { userAuthId, metadata } = context; const { userAuthId, metadata } = context;
@@ -231,7 +233,7 @@ export const updateUserColorScheme = createServerFn({ method: "POST" })
}); });
export const updateUserAccentColor = createServerFn({ method: "POST" }) export const updateUserAccentColor = createServerFn({ method: "POST" })
.validator((data: string) => data) .inputValidator((data: string) => data)
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => { .handler(async ({ context, data }) => {
const { userAuthId, metadata } = context; const { userAuthId, metadata } = context;

View File

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