feat: add tournaments and validation and security

This commit is contained in:
2026-03-26 15:36:22 +01:00
parent d76e8922cc
commit 25d2d1af4a
47 changed files with 5175 additions and 58 deletions

View File

@@ -1,26 +1,27 @@
import { json, error } from '@sveltejs/kit';
import { createInvite, getInvitesForUser } from '$lib/server/match/invites';
import { requireAuth } from '$lib/server/permissions';
import { createInviteSchema, parseBody } from '$lib/server/validation';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) error(401, 'Not logged in');
const invites = await getInvitesForUser(locals.user.id);
const user = requireAuth(locals);
const invites = await getInvitesForUser(user.id);
return json(invites);
};
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) error(401, 'Not logged in');
const user = requireAuth(locals);
const body = await request.json();
const { creatorTeamId, invitedTeamId, config, mappoolId, name, message, scheduledAt } = body;
if (!creatorTeamId || !invitedTeamId || !config || !mappoolId) {
error(400, 'Missing required fields');
}
const { creatorTeamId, invitedTeamId, config, mappoolId, name, message, scheduledAt } = parseBody(
createInviteSchema,
body
);
try {
const invite = await createInvite({
createdBy: locals.user.id,
createdBy: user.id,
creatorTeamId,
invitedTeamId,
config,

View File

@@ -1,11 +1,13 @@
import { json, error } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { mappool } from '$lib/server/db/schema';
import { requireAuth, requireRole } from '$lib/server/permissions';
import { createMappoolSchema, parseBody } from '$lib/server/validation';
import type { RequestHandler } from './$types';
// List all mappools
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) error(401, 'Not logged in');
requireAuth(locals);
const mappools = await db.query.mappool.findMany({
orderBy: (m, { desc }) => [desc(m.createdAt)]
@@ -14,18 +16,18 @@ export const GET: RequestHandler = async ({ locals }) => {
return json(mappools);
};
// Create a mappool
// Create a mappool (admin only)
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) error(401, 'Not logged in');
requireRole(locals, 'admin', 'Only admins can create mappools');
const { name } = await request.json();
if (!name?.trim()) error(400, 'Name is required');
const body = await request.json();
const { name } = parseBody(createMappoolSchema, body);
const [created] = await db
.insert(mappool)
.values({
name: name.trim(),
createdBy: locals.user.id
name,
createdBy: locals.user!.id
})
.returning();

View File

@@ -3,6 +3,7 @@ import { db } from '$lib/server/db';
import { mappool, mappoolSlot } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireOwnerOrAdmin } from '$lib/server/permissions';
import { createMappoolSlotSchema, parseBody } from '$lib/server/validation';
import type { RequestHandler } from './$types';
// Add a slot to a mappool
@@ -15,26 +16,20 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
requireOwnerOrAdmin(locals, pool.createdBy, 'Only the mappool creator or an admin can add slots');
const { beatmapId, category, mods } = await request.json();
if (!beatmapId || !category) {
error(400, 'beatmapId and category are required');
}
const body = await request.json();
const { beatmapId, category, mods } = parseBody(createMappoolSlotSchema, body);
// Count existing slots in this category to auto-increment order
const existing = await db.query.mappoolSlot.findMany({
where: and(
eq(mappoolSlot.mappoolId, params.id),
eq(mappoolSlot.category, category.toUpperCase())
)
where: and(eq(mappoolSlot.mappoolId, params.id), eq(mappoolSlot.category, category))
});
const [created] = await db
.insert(mappoolSlot)
.values({
mappoolId: params.id,
beatmapId: String(beatmapId),
category: category.toUpperCase(),
beatmapId,
category,
orderInCategory: existing.length + 1,
mods: mods ?? []
})

View File

@@ -2,10 +2,12 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { createMatch } from '$lib/server/match/engine';
import { requireAuth, requireRole } from '$lib/server/permissions';
import { createMatchSchema, parseBody } from '$lib/server/validation';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) error(401, 'Not logged in');
requireAuth(locals);
const matches = await db.query.match.findMany({
with: {
@@ -18,14 +20,10 @@ export const GET: RequestHandler = async ({ locals }) => {
};
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) error(401, 'Not logged in');
requireRole(locals, 'referee', 'Only referees or admins can create matches');
const body = await request.json();
const { name, config, mappoolId, teams } = body;
if (!name || !config || !mappoolId || !teams?.length) {
error(400, 'Missing required fields');
}
const { name, config, mappoolId, teams } = parseBody(createMatchSchema, body);
try {
const result = await createMatch({
@@ -33,7 +31,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
config,
mappoolId,
teams,
createdBy: locals.user.id
createdBy: locals.user!.id
});
return json(result, { status: 201 });
} catch (e: any) {

View File

@@ -3,15 +3,14 @@ import { json, error } from '@sveltejs/kit';
import { pickMap } from '$lib/server/match/engine';
import { requireAuth, hasRole } from '$lib/server/permissions';
import { getMatchFull } from '$lib/server/match/helpers';
import { pickMapSchema, parseBody } from '$lib/server/validation';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
const { mappoolSlotId } = await request.json();
if (!mappoolSlotId) {
error(400, 'mappoolSlotId required');
}
const body = await request.json();
const { mappoolSlotId } = parseBody(pickMapSchema, body);
const match = await getMatchFull(params.id);

View File

@@ -3,15 +3,14 @@ import { json, error } from '@sveltejs/kit';
import { submitRoll } from '$lib/server/match/engine';
import { requireAuth, hasRole } from '$lib/server/permissions';
import { getMatchFull } from '$lib/server/match/helpers';
import { submitRollSchema, parseBody } from '$lib/server/validation';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
const { value } = await request.json();
if (value == null) {
error(400, 'value required');
}
const body = await request.json();
const { value } = parseBody(submitRollSchema, body);
const match = await getMatchFull(params.id);

View File

@@ -2,15 +2,14 @@
import { json, error } from '@sveltejs/kit';
import { submitGameScores } from '$lib/server/match/engine';
import { requireRole } from '$lib/server/permissions';
import { submitScoreSchema, parseBody } from '$lib/server/validation';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request, locals }) => {
requireRole(locals, 'referee', 'Only referees or admins can submit scores');
const { matchGameId, scores } = await request.json();
if (!matchGameId || !scores?.length) {
error(400, 'matchGameId and scores required');
}
const body = await request.json();
const { matchGameId, scores } = parseBody(submitScoreSchema, body);
try {
const m = await submitGameScores(matchGameId, scores);

View File

@@ -0,0 +1,76 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournament } from '$lib/server/db/schema';
import { eq, desc } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import { createTournament } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const state = url.searchParams.get('state');
const format = url.searchParams.get('format');
const query = db.query.tournament.findMany({
with: {
registrations: true,
staff: true
},
orderBy: [desc(tournament.createdAt)]
});
const results = await query;
// filter in memory (drizzle doesn't support dynamic where chaining easily)
let filtered = results;
if (state) filtered = filtered.filter((t) => t.state === state);
if (format) filtered = filtered.filter((t) => t.format === format);
return json(filtered);
};
export const POST: RequestHandler = async ({ request, locals }) => {
const user = requireAuth(locals);
const body = await request.json();
const {
name,
description,
format,
config,
maxSlots,
bannerUrl,
registrationOpenAt,
registrationCloseAt,
startAt
} = body;
if (!name || !format || !config || !maxSlots) {
error(400, 'Missing required fields: name, format, config, maxSlots');
}
if (!['single_elim', 'double_elim', 'groups_bracket'].includes(format)) {
error(400, 'Invalid format. Must be single_elim, double_elim, or groups_bracket');
}
if (!config.teamSize || !config.scoringType) {
error(400, 'Config must include teamSize and scoringType');
}
try {
const result = await createTournament({
name,
description,
format,
config,
maxSlots,
bannerUrl,
createdBy: user.id,
registrationOpenAt: registrationOpenAt ? new Date(registrationOpenAt) : undefined,
registrationCloseAt: registrationCloseAt ? new Date(registrationCloseAt) : undefined,
startAt: startAt ? new Date(startAt) : undefined
});
return json(result, { status: 201 });
} catch (e: any) {
error(400, e.message);
}
};

View File

@@ -0,0 +1,83 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournament, tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import { getTournamentFull } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const result = await getTournamentFull(params.id);
if (!result) error(404, 'Tournament not found');
return json(result);
};
export const PATCH: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
await requireTournamentStaff(params.id, user.id);
const t = await db.query.tournament.findFirst({
where: eq(tournament.id, params.id)
});
if (!t) error(404, 'Tournament not found');
if (t.state !== 'DRAFT') error(400, 'Can only edit tournaments in DRAFT state');
const body = await request.json();
const allowed = [
'name',
'description',
'format',
'config',
'maxSlots',
'bannerUrl',
'registrationOpenAt',
'registrationCloseAt',
'startAt'
];
const updates: Record<string, unknown> = {};
for (const key of allowed) {
if (body[key] !== undefined) {
if (key.endsWith('At') && body[key]) {
updates[key] = new Date(body[key]);
} else {
updates[key] = body[key];
}
}
}
if (Object.keys(updates).length === 0) {
error(400, 'No valid fields to update');
}
await db.update(tournament).set(updates).where(eq(tournament.id, params.id));
return json(await getTournamentFull(params.id));
};
export const DELETE: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
await requireTournamentStaff(params.id, user.id);
const t = await db.query.tournament.findFirst({
where: eq(tournament.id, params.id)
});
if (!t) error(404, 'Tournament not found');
if (t.state === 'DRAFT') {
await db.delete(tournament).where(eq(tournament.id, params.id));
return json({ deleted: true });
}
// for non-draft, cancel instead of delete
const { cancelTournament } = await import('$lib/server/tournament/lifecycle');
await cancelTournament(params.id);
return json(await getTournamentFull(params.id));
};
async function requireTournamentStaff(tournamentId: string, userId: string) {
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, tournamentId), eq(tournamentStaff.userId, userId))
});
if (!staff) error(403, 'Not a staff member of this tournament');
return staff;
}

View File

@@ -0,0 +1,28 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentMatch, tournamentRound } from '$lib/server/db/schema';
import { eq, asc } from 'drizzle-orm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const rounds = await db.query.tournamentRound.findMany({
where: eq(tournamentRound.tournamentId, params.id),
orderBy: [asc(tournamentRound.roundOrder)]
});
const matches = await db.query.tournamentMatch.findMany({
where: eq(tournamentMatch.tournamentId, params.id),
with: {
team1: true,
team2: true,
winner: true,
match: {
with: {
participants: { with: { team: true } }
}
}
}
});
return json({ rounds, matches });
};

View File

@@ -0,0 +1,23 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import { cancelTournament, getTournamentFull } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
try {
await cancelTournament(params.id);
return json(await getTournamentFull(params.id));
} catch (e: any) {
error(400, e.message);
}
};

View File

@@ -0,0 +1,23 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import { closeRegistration, getTournamentFull } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
try {
await closeRegistration(params.id);
return json(await getTournamentFull(params.id));
} catch (e: any) {
error(400, e.message);
}
};

View File

@@ -0,0 +1,23 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import { finalizeSeeding, getTournamentFull } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
try {
await finalizeSeeding(params.id);
return json(await getTournamentFull(params.id));
} catch (e: any) {
error(400, e.message);
}
};

View File

@@ -0,0 +1,25 @@
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentMatch } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const matches = await db.query.tournamentMatch.findMany({
where: eq(tournamentMatch.tournamentId, params.id),
with: {
round: true,
team1: true,
team2: true,
winner: true,
match: {
with: {
participants: { with: { team: true } },
games: { with: { slot: true } }
}
}
}
});
return json(matches);
};

View File

@@ -0,0 +1,23 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import { openRegistration, getTournamentFull } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
try {
await openRegistration(params.id);
return json(await getTournamentFull(params.id));
} catch (e: any) {
error(400, e.message);
}
};

View File

@@ -0,0 +1,50 @@
import { json, error } from '@sveltejs/kit';
import {
getQualifierRankings,
getQualifierMappool,
submitQualifierScore
} from '$lib/server/tournament/qualifiers';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const [rankings, mappool] = await Promise.all([
getQualifierRankings(params.id),
getQualifierMappool(params.id)
]);
return json({ rankings, mappool });
};
export const POST: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
// only staff can submit qualifier scores (referees run qualifier lobbies)
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Only tournament staff can submit qualifier scores');
const body = await request.json();
const { teamId, mappoolSlotId, totalScore, accuracy } = body;
if (!teamId || !mappoolSlotId || totalScore === undefined) {
error(400, 'Missing required fields: teamId, mappoolSlotId, totalScore');
}
try {
const score = await submitQualifierScore({
tournamentId: params.id,
teamId,
mappoolSlotId,
totalScore,
accuracy
});
return json(score, { status: 201 });
} catch (e: any) {
error(400, e.message);
}
};

View File

@@ -0,0 +1,32 @@
import { json, error } from '@sveltejs/kit';
import { requireAuth } from '$lib/server/permissions';
import { registerForTournament, withdrawFromTournament } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
const body = await request.json();
if (!body.teamId) error(400, 'Missing teamId');
try {
const reg = await registerForTournament(params.id, body.teamId, user.id);
return json(reg, { status: 201 });
} catch (e: any) {
error(400, e.message);
}
};
export const DELETE: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
const body = await request.json();
if (!body.teamId) error(400, 'Missing teamId');
try {
await withdrawFromTournament(params.id, body.teamId);
return json({ withdrawn: true });
} catch (e: any) {
error(400, e.message);
}
};

View File

@@ -0,0 +1,50 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentRound, tournamentStaff } from '$lib/server/db/schema';
import { eq, and, asc } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const rounds = await db.query.tournamentRound.findMany({
where: eq(tournamentRound.tournamentId, params.id),
with: { mappool: { with: { slots: true } } },
orderBy: [asc(tournamentRound.roundOrder)]
});
return json(rounds);
};
export const POST: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
const body = await request.json();
const { name, abbreviation, bestOf, mappoolId, bracketType, scheduledAt } = body;
if (!name || !bestOf) error(400, 'Missing required fields: name, bestOf');
// get next round order
const existing = await db.query.tournamentRound.findMany({
where: eq(tournamentRound.tournamentId, params.id)
});
const [created] = await db
.insert(tournamentRound)
.values({
tournamentId: params.id,
name,
abbreviation,
roundOrder: existing.length,
bestOf,
mappoolId,
bracketType,
scheduledAt: scheduledAt ? new Date(scheduledAt) : undefined
})
.returning();
return json(created, { status: 201 });
};

View File

@@ -0,0 +1,50 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentRound, tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import type { RequestHandler } from './$types';
export const PATCH: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
const body = await request.json();
const updates: Record<string, unknown> = {};
if (body.name !== undefined) updates.name = body.name;
if (body.abbreviation !== undefined) updates.abbreviation = body.abbreviation;
if (body.bestOf !== undefined) updates.bestOf = body.bestOf;
if (body.mappoolId !== undefined) updates.mappoolId = body.mappoolId;
if (body.scheduledAt !== undefined)
updates.scheduledAt = body.scheduledAt ? new Date(body.scheduledAt) : null;
if (Object.keys(updates).length === 0) {
error(400, 'No valid fields to update');
}
await db.update(tournamentRound).set(updates).where(eq(tournamentRound.id, params.roundId));
const updated = await db.query.tournamentRound.findFirst({
where: eq(tournamentRound.id, params.roundId),
with: { mappool: { with: { slots: true } } }
});
return json(updated);
};
export const DELETE: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
await db.delete(tournamentRound).where(eq(tournamentRound.id, params.roundId));
return json({ deleted: true });
};

View File

@@ -0,0 +1,47 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const staff = await db.query.tournamentStaff.findMany({
where: eq(tournamentStaff.tournamentId, params.id)
});
return json(staff);
};
export const POST: RequestHandler = async ({ params, request, locals }) => {
const user = requireAuth(locals);
// only organizers can add staff
const actor = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!actor || (actor.role !== 'organizer' && actor.role !== 'admin')) {
error(403, 'Only organizers can manage staff');
}
const body = await request.json();
if (!body.userId || !body.role) error(400, 'Missing userId or role');
const validRoles = ['organizer', 'admin', 'referee', 'pooler', 'streamer'];
if (!validRoles.includes(body.role)) {
error(400, `Invalid role. Must be one of: ${validRoles.join(', ')}`);
}
try {
const [created] = await db
.insert(tournamentStaff)
.values({
tournamentId: params.id,
userId: body.userId,
role: body.role
})
.returning();
return json(created, { status: 201 });
} catch (e: any) {
error(400, 'User is already a staff member');
}
};

View File

@@ -0,0 +1,20 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import type { RequestHandler } from './$types';
export const DELETE: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
const actor = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!actor || (actor.role !== 'organizer' && actor.role !== 'admin')) {
error(403, 'Only organizers can manage staff');
}
await db.delete(tournamentStaff).where(eq(tournamentStaff.id, params.staffId));
return json({ deleted: true });
};

View File

@@ -0,0 +1,27 @@
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tournamentStaff } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import { requireAuth } from '$lib/server/permissions';
import { getTournamentFull } from '$lib/server/tournament/lifecycle';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ params, locals }) => {
const user = requireAuth(locals);
const staff = await db.query.tournamentStaff.findFirst({
where: and(eq(tournamentStaff.tournamentId, params.id), eq(tournamentStaff.userId, user.id))
});
if (!staff) error(403, 'Not a staff member of this tournament');
// qualifiers are started via closeRegistration when qualifiers are enabled
// this endpoint is for manually triggering qualifier finalization
const { finalizeQualifiers } = await import('$lib/server/tournament/qualifiers');
try {
await finalizeQualifiers(params.id);
return json(await getTournamentFull(params.id));
} catch (e: any) {
error(400, e.message);
}
};