diff --git a/src/features/players/server.ts b/src/features/players/server.ts index 3f2e400..3c92255 100644 --- a/src/features/players/server.ts +++ b/src/features/players/server.ts @@ -1,28 +1,29 @@ -import { fetchSuperTokensAuth, setUserMetadata, superTokensFunctionMiddleware, superTokensRoleFunctionMiddleware } from "@/utils/supertokens"; +import { setUserMetadata, superTokensFunctionMiddleware } from "@/utils/supertokens"; import { createServerFn } from "@tanstack/react-start"; import { playerInputSchema, playerUpdateSchema } from "@/features/players/types"; import { pbAdmin } from "@/lib/pocketbase/client"; import { z } from "zod"; import { logger } from "."; -export const fetchMe = createServerFn().handler(async () => { - const data = await fetchSuperTokensAuth(); - if (!data || !data.userAuthId) return { user: undefined, roles: [], metadata: {} }; +export const fetchMe = createServerFn() + .middleware([superTokensFunctionMiddleware]) + .handler(async ({ context }) => { + if (!context || !context.userAuthId) return { user: undefined, roles: [], metadata: {} }; - try { - await pbAdmin.authPromise; - const result = await pbAdmin.getPlayerByAuthId(data.userAuthId); - logger.info('Fetched player', result); - return { - user: result || undefined, - roles: data.roles, - metadata: data.metadata - }; - } catch (error) { - logger.error('Error fetching player:', error); - return { user: undefined, roles: data.roles, metadata: data.metadata }; - } -}); + try { + await pbAdmin.authPromise; + const result = await pbAdmin.getPlayerByAuthId(context.userAuthId); + logger.info('Fetched player', result); + return { + user: result || undefined, + roles: context.roles, + metadata: context.metadata + }; + } catch (error) { + logger.error('Error fetching player:', error); + return { user: undefined, roles: context.roles, metadata: context.metadata }; + } + }); export const getPlayer = createServerFn() .validator(z.string()) diff --git a/src/features/tournaments/hooks/use-enroll-team.ts b/src/features/tournaments/hooks/use-enroll-team.ts new file mode 100644 index 0000000..28138d3 --- /dev/null +++ b/src/features/tournaments/hooks/use-enroll-team.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { enrollTeam } from "@/features/tournaments/server"; +import toast from '@/lib/sonner'; + +const useEnrollTeam = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: { tournamentId: string, teamId: string }) => { + return enrollTeam({ data }); + }, + onSuccess: (data, { tournamentId }) => { + if (!data) { + toast.error('There was an issue enrolling. Please try again later.'); + } else { + queryClient.invalidateQueries({ queryKey: ['tournaments', 'detail', tournamentId] }); + toast.success('Team enrolled successfully!'); + } + }, + onError: (error: any) => { + if (error.message) { + toast.error(error.message); + } else { + toast.error('An unexpected error occurred when trying to enroll the team. Please try again later.'); + } + }, + }); +}; + +export default useEnrollTeam; diff --git a/src/features/tournaments/queries.ts b/src/features/tournaments/queries.ts index 7de4b59..2a5ada5 100644 --- a/src/features/tournaments/queries.ts +++ b/src/features/tournaments/queries.ts @@ -3,7 +3,7 @@ import { getTournament, listTournaments } from "./server"; const tournamentKeys = { list: ['tournaments', 'list'] as const, - details: (id: string) => ['tournaments', 'details', id] as const, + details: (id: string) => ['tournaments', 'details', id] as const }; export const tournamentQueries = { diff --git a/src/features/tournaments/server.ts b/src/features/tournaments/server.ts index ec24cb0..651b101 100644 --- a/src/features/tournaments/server.ts +++ b/src/features/tournaments/server.ts @@ -41,3 +41,32 @@ export const getTournament = createServerFn() const tournament = await pbAdmin.getTournament(tournamentId); return tournament; }); + +export const enrollTeam = createServerFn() + .validator(z.object({ + tournamentId: z.string(), + teamId: z.string() + })) + .middleware([superTokensFunctionMiddleware]) + .handler(async ({ data: { tournamentId, teamId }, context }) => { + try { + const userId = context.userAuthId; + const isAdmin = context.roles.includes("Admin"); + + const team = await pbAdmin.getTeam(teamId); + if (!team) { throw new Error('Team not found'); } + + const isPlayerOnTeam = team.players?.some(player => player.id === userId); + + if (!isPlayerOnTeam && !isAdmin) { + throw new Error('You do not have permission to enroll this team'); + } + + logger.info('Enrolling team in tournament', { tournamentId, teamId, userId }); + const tournament = await pbAdmin.enrollTeam(tournamentId, teamId); + return tournament; + } catch (error) { + logger.error('Error enrolling team', error); + throw error; + } + }); diff --git a/src/lib/pocketbase/services/tournaments.ts b/src/lib/pocketbase/services/tournaments.ts index 17d8863..d340d92 100644 --- a/src/lib/pocketbase/services/tournaments.ts +++ b/src/lib/pocketbase/services/tournaments.ts @@ -1,5 +1,9 @@ import { logger } from "@/lib/logger"; -import type { Tournament, TournamentInput, TournamentUpdateInput } from "@/features/tournaments/types"; +import type { + Tournament, + TournamentInput, + TournamentUpdateInput, +} from "@/features/tournaments/types"; import PocketBase from "pocketbase"; import { transformTournament } from "@/lib/pocketbase/util/transform-types"; @@ -7,9 +11,9 @@ export function createTournamentsService(pb: PocketBase) { return { async getTournament(id: string): Promise { try { - logger.info('PocketBase | Getting tournament', id); - const result = await pb.collection('tournaments').getOne(id, { - expand: 'teams, teams.players' + logger.info("PocketBase | Getting tournament", id); + const result = await pb.collection("tournaments").getOne(id, { + expand: "teams, teams.players", }); return transformTournament(result); } catch { @@ -17,20 +21,40 @@ export function createTournamentsService(pb: PocketBase) { } }, async listTournaments(): Promise { - const result = await pb.collection('tournaments').getFullList({ - fields: 'id,name,start_time,end_time,logo_url,created', - sort: '-created' - }); + const result = await pb + .collection("tournaments") + .getFullList({ + fields: "id,name,start_time,end_time,logo_url,created", + sort: "-created", + }); - console.log(result) + console.log(result); return result.map(transformTournament); }, async createTournament(data: TournamentInput): Promise { - const result = await pb.collection('tournaments').create(data); + const result = await pb + .collection("tournaments") + .create(data); return transformTournament(result); }, - async updateTournament(id: string, data: TournamentUpdateInput): Promise { - const result = await pb.collection('tournaments').update(id, data); + async updateTournament( + id: string, + data: TournamentUpdateInput + ): Promise { + const result = await pb + .collection("tournaments") + .update(id, data); + return transformTournament(result); + }, + async enrollTeam( + tournamentId: string, + teamId: string + ): Promise { + const result = await pb.collection("tournaments").update( + tournamentId, + { "teams+": teamId }, + { expand: "teams, teams.players" } + ); return transformTournament(result); }, }; diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts index 62d7a3f..d6a4f05 100644 --- a/src/utils/supertokens.ts +++ b/src/utils/supertokens.ts @@ -1,4 +1,4 @@ -import { createMiddleware, createServerFn } from "@tanstack/react-start"; +import { createMiddleware, createServerFn, ServerFnResponseType } from "@tanstack/react-start"; import { getWebRequest } from "@tanstack/react-start/server"; import { getSessionForSSR } from "supertokens-node/custom"; import UserRoles from "supertokens-node/recipe/userroles"; @@ -6,17 +6,18 @@ import UserMetadata from "supertokens-node/recipe/usermetadata"; import { getSessionForStart } from "@/lib/supertokens/recipes/start-session"; import { Logger } from "@/lib/logger"; import z from "zod"; +import { refreshSession } from "supertokens-node/recipe/session"; const logger = new Logger('Middleware'); -const verifySuperTokensSession = async (request: Request) => { +const verifySuperTokensSession = async (request: Request, response?: ServerFnResponseType) => { const session = await getSessionForStart(request, { sessionRequired: false }); - if (session?.needsRefresh) { - logger.info("Session needs refresh"); + if (session?.needsRefresh && response) { + logger.info("Session refreshing..."); + refreshSession(request, response); return { context: { needsRefresh: true } }; } - const userAuthId = session?.userId; if (!userAuthId) { @@ -34,7 +35,7 @@ const verifySuperTokensSession = async (request: Request) => { metadata, session: { accessTokenPayload: session.accessTokenPayload, - sessionHandle: session.sessionHandle + sessionHandle: session.sessionHandle, } } }; @@ -42,35 +43,61 @@ const verifySuperTokensSession = async (request: Request) => { export const superTokensRequestMiddleware = createMiddleware({ type: 'request' }) .server(async ({ next, request }) => { - const context = await verifySuperTokensSession(request); - return next(context as any); + const session = await verifySuperTokensSession(request); + + if (!session.context.userAuthId) { + logger.error('Unauthenticated user in API call.', session.context) + throw new Error('Unauthenticated') + } + + const context = { + userAuthId: session.context.userAuthId, + roles: session.context.roles, + metadata: session.context.metadata + } + + return next({ context }) }); export const superTokensFunctionMiddleware = createMiddleware({ type: 'function' }) - .server(async ({ next }) => { + .server(async ({ next, response }) => { const request = getWebRequest(); + const session = await verifySuperTokensSession(request, response); - const context = await verifySuperTokensSession(request); - return next(context as any); - }) + if (!session.context.userAuthId) { + logger.error('Unauthenticated user in server function.', session.context) + throw new Error('Unauthenticated') + } -export const superTokensRoleFunctionMiddleware = createMiddleware({ type: 'function' }) - .server(async ({ next, context }) => { - const { roles } = context as any; - return next(({ context: { roles } })); + const context = { + userAuthId: session.context.userAuthId, + roles: session.context.roles, + metadata: session.context.metadata + } + return next({ context }); }) export const superTokensAdminFunctionMiddleware = createMiddleware({ type: 'function' }) - .server(async ({ next }) => { + .server(async ({ next, response }) => { const request = getWebRequest(); - const session = await verifySuperTokensSession(request); + const session = await verifySuperTokensSession(request, response); - const { roles } = session?.context as any; - if (roles?.includes('Admin')) { - return next(({ context: { roles } })); + if (!session.context.userAuthId) { + logger.error('Unauthenticated user in admin function.', session.context) + throw new Error('Unauthenticated') } - logger.error('User reached admin function without admin role', next); + const context = { + userAuthId: session.context.userAuthId, + roles: session.context.roles, + metadata: session.context.metadata + } + + if (context.roles?.includes('Admin')) { + return next(({ context })); + } + + logger.error('Unauthorized user in admin function.', context); throw new Error('Unauthorized'); }) @@ -79,23 +106,6 @@ export const fetchUserRoles = async (userAuthId: string) => { return response; } -export const fetchSuperTokensAuth = createServerFn({ method: 'GET' }).handler(async () => { - const request = getWebRequest(); - const session = await getSessionForSSR(request); - const userAuthId = session?.accessTokenPayload?.sub; - - if (!userAuthId) return; - const { roles } = await fetchUserRoles(userAuthId); - const { metadata } = await UserMetadata.getUserMetadata(userAuthId); - - return { - userAuthId, - hasToken: session.hasToken, - roles, - metadata - } -}) - export const setUserMetadata = createServerFn({ method: 'POST' }) .validator(z.object({ first_name: z.string().min(3).max(20).regex(/^[a-zA-Z0-9\s]+$/, "First name must be 3-20 characters long and contain only letters and spaces"),