way better auth middleware, enroll team server fn and pb fn
This commit is contained in:
@@ -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 { createServerFn } from "@tanstack/react-start";
|
||||||
import { playerInputSchema, playerUpdateSchema } from "@/features/players/types";
|
import { playerInputSchema, playerUpdateSchema } from "@/features/players/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 ".";
|
||||||
|
|
||||||
export const fetchMe = createServerFn().handler(async () => {
|
export const fetchMe = createServerFn()
|
||||||
const data = await fetchSuperTokensAuth();
|
.middleware([superTokensFunctionMiddleware])
|
||||||
if (!data || !data.userAuthId) return { user: undefined, roles: [], metadata: {} };
|
.handler(async ({ context }) => {
|
||||||
|
if (!context || !context.userAuthId) return { user: undefined, roles: [], metadata: {} };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pbAdmin.authPromise;
|
await pbAdmin.authPromise;
|
||||||
const result = await pbAdmin.getPlayerByAuthId(data.userAuthId);
|
const result = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||||
logger.info('Fetched player', result);
|
logger.info('Fetched player', result);
|
||||||
return {
|
return {
|
||||||
user: result || undefined,
|
user: result || undefined,
|
||||||
roles: data.roles,
|
roles: context.roles,
|
||||||
metadata: data.metadata
|
metadata: context.metadata
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching player:', error);
|
logger.error('Error fetching player:', error);
|
||||||
return { user: undefined, roles: data.roles, metadata: data.metadata };
|
return { user: undefined, roles: context.roles, metadata: context.metadata };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getPlayer = createServerFn()
|
export const getPlayer = createServerFn()
|
||||||
.validator(z.string())
|
.validator(z.string())
|
||||||
|
|||||||
30
src/features/tournaments/hooks/use-enroll-team.ts
Normal file
30
src/features/tournaments/hooks/use-enroll-team.ts
Normal file
@@ -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;
|
||||||
@@ -3,7 +3,7 @@ import { getTournament, listTournaments } from "./server";
|
|||||||
|
|
||||||
const tournamentKeys = {
|
const tournamentKeys = {
|
||||||
list: ['tournaments', 'list'] as const,
|
list: ['tournaments', 'list'] as const,
|
||||||
details: (id: string) => ['tournaments', 'details', id] as const,
|
details: (id: string) => ['tournaments', 'details', id] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tournamentQueries = {
|
export const tournamentQueries = {
|
||||||
|
|||||||
@@ -41,3 +41,32 @@ export const getTournament = createServerFn()
|
|||||||
const tournament = await pbAdmin.getTournament(tournamentId);
|
const tournament = await pbAdmin.getTournament(tournamentId);
|
||||||
return tournament;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { logger } from "@/lib/logger";
|
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 PocketBase from "pocketbase";
|
||||||
import { transformTournament } from "@/lib/pocketbase/util/transform-types";
|
import { transformTournament } from "@/lib/pocketbase/util/transform-types";
|
||||||
|
|
||||||
@@ -7,9 +11,9 @@ export function createTournamentsService(pb: PocketBase) {
|
|||||||
return {
|
return {
|
||||||
async getTournament(id: string): Promise<Tournament | null> {
|
async getTournament(id: string): Promise<Tournament | null> {
|
||||||
try {
|
try {
|
||||||
logger.info('PocketBase | Getting tournament', id);
|
logger.info("PocketBase | Getting tournament", id);
|
||||||
const result = await pb.collection('tournaments').getOne(id, {
|
const result = await pb.collection("tournaments").getOne(id, {
|
||||||
expand: 'teams, teams.players'
|
expand: "teams, teams.players",
|
||||||
});
|
});
|
||||||
return transformTournament(result);
|
return transformTournament(result);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -17,20 +21,40 @@ export function createTournamentsService(pb: PocketBase) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async listTournaments(): Promise<Tournament[]> {
|
async listTournaments(): Promise<Tournament[]> {
|
||||||
const result = await pb.collection('tournaments').getFullList<Tournament>({
|
const result = await pb
|
||||||
fields: 'id,name,start_time,end_time,logo_url,created',
|
.collection("tournaments")
|
||||||
sort: '-created'
|
.getFullList<Tournament>({
|
||||||
|
fields: "id,name,start_time,end_time,logo_url,created",
|
||||||
|
sort: "-created",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(result)
|
console.log(result);
|
||||||
return result.map(transformTournament);
|
return result.map(transformTournament);
|
||||||
},
|
},
|
||||||
async createTournament(data: TournamentInput): Promise<Tournament> {
|
async createTournament(data: TournamentInput): Promise<Tournament> {
|
||||||
const result = await pb.collection('tournaments').create<Tournament>(data);
|
const result = await pb
|
||||||
|
.collection("tournaments")
|
||||||
|
.create<Tournament>(data);
|
||||||
return transformTournament(result);
|
return transformTournament(result);
|
||||||
},
|
},
|
||||||
async updateTournament(id: string, data: TournamentUpdateInput): Promise<Tournament> {
|
async updateTournament(
|
||||||
const result = await pb.collection('tournaments').update<Tournament>(id, data);
|
id: string,
|
||||||
|
data: TournamentUpdateInput
|
||||||
|
): Promise<Tournament> {
|
||||||
|
const result = await pb
|
||||||
|
.collection("tournaments")
|
||||||
|
.update<Tournament>(id, data);
|
||||||
|
return transformTournament(result);
|
||||||
|
},
|
||||||
|
async enrollTeam(
|
||||||
|
tournamentId: string,
|
||||||
|
teamId: string
|
||||||
|
): Promise<Tournament> {
|
||||||
|
const result = await pb.collection("tournaments").update<Tournament>(
|
||||||
|
tournamentId,
|
||||||
|
{ "teams+": teamId },
|
||||||
|
{ expand: "teams, teams.players" }
|
||||||
|
);
|
||||||
return transformTournament(result);
|
return transformTournament(result);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { getWebRequest } from "@tanstack/react-start/server";
|
||||||
import { getSessionForSSR } from "supertokens-node/custom";
|
import { getSessionForSSR } from "supertokens-node/custom";
|
||||||
import UserRoles from "supertokens-node/recipe/userroles";
|
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 { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
|
||||||
import { Logger } from "@/lib/logger";
|
import { Logger } from "@/lib/logger";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import { refreshSession } from "supertokens-node/recipe/session";
|
||||||
|
|
||||||
const logger = new Logger('Middleware');
|
const logger = new Logger('Middleware');
|
||||||
|
|
||||||
const verifySuperTokensSession = async (request: Request) => {
|
const verifySuperTokensSession = async (request: Request, response?: ServerFnResponseType) => {
|
||||||
const session = await getSessionForStart(request, { sessionRequired: false });
|
const session = await getSessionForStart(request, { sessionRequired: false });
|
||||||
|
|
||||||
if (session?.needsRefresh) {
|
if (session?.needsRefresh && response) {
|
||||||
logger.info("Session needs refresh");
|
logger.info("Session refreshing...");
|
||||||
|
refreshSession(request, response);
|
||||||
return { context: { needsRefresh: true } };
|
return { context: { needsRefresh: true } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAuthId = session?.userId;
|
const userAuthId = session?.userId;
|
||||||
|
|
||||||
if (!userAuthId) {
|
if (!userAuthId) {
|
||||||
@@ -34,7 +35,7 @@ const verifySuperTokensSession = async (request: Request) => {
|
|||||||
metadata,
|
metadata,
|
||||||
session: {
|
session: {
|
||||||
accessTokenPayload: session.accessTokenPayload,
|
accessTokenPayload: session.accessTokenPayload,
|
||||||
sessionHandle: session.sessionHandle
|
sessionHandle: session.sessionHandle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -42,35 +43,61 @@ const verifySuperTokensSession = async (request: Request) => {
|
|||||||
|
|
||||||
export const superTokensRequestMiddleware = createMiddleware({ type: 'request' })
|
export const superTokensRequestMiddleware = createMiddleware({ type: 'request' })
|
||||||
.server(async ({ next, request }) => {
|
.server(async ({ next, request }) => {
|
||||||
const context = await verifySuperTokensSession(request);
|
const session = await verifySuperTokensSession(request);
|
||||||
return next(context as any);
|
|
||||||
|
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' })
|
export const superTokensFunctionMiddleware = createMiddleware({ type: 'function' })
|
||||||
.server(async ({ next }) => {
|
.server(async ({ next, response }) => {
|
||||||
const request = getWebRequest();
|
const request = getWebRequest();
|
||||||
|
const session = await verifySuperTokensSession(request, response);
|
||||||
|
|
||||||
const context = await verifySuperTokensSession(request);
|
if (!session.context.userAuthId) {
|
||||||
return next(context as any);
|
logger.error('Unauthenticated user in server function.', session.context)
|
||||||
})
|
throw new Error('Unauthenticated')
|
||||||
|
}
|
||||||
|
|
||||||
export const superTokensRoleFunctionMiddleware = createMiddleware({ type: 'function' })
|
const context = {
|
||||||
.server(async ({ next, context }) => {
|
userAuthId: session.context.userAuthId,
|
||||||
const { roles } = context as any;
|
roles: session.context.roles,
|
||||||
return next(({ context: { roles } }));
|
metadata: session.context.metadata
|
||||||
|
}
|
||||||
|
return next({ context });
|
||||||
})
|
})
|
||||||
|
|
||||||
export const superTokensAdminFunctionMiddleware = createMiddleware({ type: 'function' })
|
export const superTokensAdminFunctionMiddleware = createMiddleware({ type: 'function' })
|
||||||
.server(async ({ next }) => {
|
.server(async ({ next, response }) => {
|
||||||
const request = getWebRequest();
|
const request = getWebRequest();
|
||||||
const session = await verifySuperTokensSession(request);
|
const session = await verifySuperTokensSession(request, response);
|
||||||
|
|
||||||
const { roles } = session?.context as any;
|
if (!session.context.userAuthId) {
|
||||||
if (roles?.includes('Admin')) {
|
logger.error('Unauthenticated user in admin function.', session.context)
|
||||||
return next(({ context: { roles } }));
|
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');
|
throw new Error('Unauthorized');
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,23 +106,6 @@ export const fetchUserRoles = async (userAuthId: string) => {
|
|||||||
return response;
|
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' })
|
export const setUserMetadata = createServerFn({ method: 'POST' })
|
||||||
.validator(z.object({
|
.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"),
|
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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user