feat: add tournaments and validation and security
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 ?? []
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
76
src/routes/api/tournaments/+server.ts
Normal file
76
src/routes/api/tournaments/+server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
83
src/routes/api/tournaments/[id]/+server.ts
Normal file
83
src/routes/api/tournaments/[id]/+server.ts
Normal 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;
|
||||
}
|
||||
28
src/routes/api/tournaments/[id]/bracket/+server.ts
Normal file
28
src/routes/api/tournaments/[id]/bracket/+server.ts
Normal 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 });
|
||||
};
|
||||
23
src/routes/api/tournaments/[id]/cancel/+server.ts
Normal file
23
src/routes/api/tournaments/[id]/cancel/+server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
23
src/routes/api/tournaments/[id]/finalize-seeding/+server.ts
Normal file
23
src/routes/api/tournaments/[id]/finalize-seeding/+server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
25
src/routes/api/tournaments/[id]/matches/+server.ts
Normal file
25
src/routes/api/tournaments/[id]/matches/+server.ts
Normal 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);
|
||||
};
|
||||
23
src/routes/api/tournaments/[id]/open-registration/+server.ts
Normal file
23
src/routes/api/tournaments/[id]/open-registration/+server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
50
src/routes/api/tournaments/[id]/qualifiers/+server.ts
Normal file
50
src/routes/api/tournaments/[id]/qualifiers/+server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
32
src/routes/api/tournaments/[id]/register/+server.ts
Normal file
32
src/routes/api/tournaments/[id]/register/+server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
50
src/routes/api/tournaments/[id]/rounds/+server.ts
Normal file
50
src/routes/api/tournaments/[id]/rounds/+server.ts
Normal 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 });
|
||||
};
|
||||
50
src/routes/api/tournaments/[id]/rounds/[roundId]/+server.ts
Normal file
50
src/routes/api/tournaments/[id]/rounds/[roundId]/+server.ts
Normal 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 });
|
||||
};
|
||||
47
src/routes/api/tournaments/[id]/staff/+server.ts
Normal file
47
src/routes/api/tournaments/[id]/staff/+server.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
20
src/routes/api/tournaments/[id]/staff/[staffId]/+server.ts
Normal file
20
src/routes/api/tournaments/[id]/staff/[staffId]/+server.ts
Normal 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 });
|
||||
};
|
||||
27
src/routes/api/tournaments/[id]/start-qualifiers/+server.ts
Normal file
27
src/routes/api/tournaments/[id]/start-qualifiers/+server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user