From 2dce83de55e2dcff6074f0bd9e0d1c16ee3f22cd Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Tue, 7 Apr 2026 20:10:48 +0200 Subject: [PATCH] security: enforce auth on all API write endpoints, remove mario-kart - Remove all mario-kart routes and model (zero auth, unused) - Add requireGroup() helper to auth middleware - Recipe write APIs (add/edit/delete/img/*): require rezepte_users group - Translate endpoint: require rezepte_users (was fully unauthenticated) - Nutrition overwrites: require auth (was no-op) - Nutrition generate-all: require rezepte_users (was no-op) - Alt-text/color endpoints: require rezepte_users group - Image delete/mv: add path traversal protection - Period shared endpoint: normalize username for consistent lookup --- package.json | 2 +- src/lib/server/middleware/auth.ts | 26 ++ src/models/MarioKartTournament.ts | 228 ----------------- .../[recipeLang=recipeLang]/add/+server.ts | 16 +- .../[recipeLang=recipeLang]/delete/+server.ts | 10 +- .../[recipeLang=recipeLang]/edit/+server.ts | 68 +++-- .../img/add/+server.ts | 23 +- .../img/delete/+server.ts | 29 ++- .../[recipeLang=recipeLang]/img/mv/+server.ts | 29 ++- .../nutrition/generate-all/+server.ts | 3 +- .../translate/+server.ts | 15 +- .../api/fitness/period/shared/+server.ts | 3 +- .../api/generate-alt-text-bulk/+server.ts | 7 +- src/routes/api/generate-alt-text/+server.ts | 7 +- .../api/mario-kart/tournaments/+server.ts | 48 ---- .../mario-kart/tournaments/[id]/+server.ts | 67 ----- .../tournaments/[id]/bracket/+server.ts | 240 ------------------ .../matches/[matchId]/scores/+server.ts | 173 ------------- .../tournaments/[id]/contestants/+server.ts | 107 -------- .../contestants/[contestantId]/dnf/+server.ts | 43 ---- .../tournaments/[id]/groups/+server.ts | 97 ------- .../[id]/groups/[groupId]/scores/+server.ts | 76 ------ .../api/nutrition/overwrites/+server.ts | 7 +- .../api/recalculate-image-colors/+server.ts | 6 +- 24 files changed, 119 insertions(+), 1211 deletions(-) delete mode 100644 src/models/MarioKartTournament.ts delete mode 100644 src/routes/api/mario-kart/tournaments/+server.ts delete mode 100644 src/routes/api/mario-kart/tournaments/[id]/+server.ts delete mode 100644 src/routes/api/mario-kart/tournaments/[id]/bracket/+server.ts delete mode 100644 src/routes/api/mario-kart/tournaments/[id]/bracket/matches/[matchId]/scores/+server.ts delete mode 100644 src/routes/api/mario-kart/tournaments/[id]/contestants/+server.ts delete mode 100644 src/routes/api/mario-kart/tournaments/[id]/contestants/[contestantId]/dnf/+server.ts delete mode 100644 src/routes/api/mario-kart/tournaments/[id]/groups/+server.ts delete mode 100644 src/routes/api/mario-kart/tournaments/[id]/groups/[groupId]/scores/+server.ts diff --git a/package.json b/package.json index 077a514..b2fc59b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.4.4", + "version": "1.5.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/server/middleware/auth.ts b/src/lib/server/middleware/auth.ts index b70eed9..547c219 100644 --- a/src/lib/server/middleware/auth.ts +++ b/src/lib/server/middleware/auth.ts @@ -45,6 +45,32 @@ export async function requireAuth( }; } +/** + * Require authentication AND membership in a specific group. + * Throws 401 if not authenticated, 403 if not in the group. + */ +export async function requireGroup( + locals: RequestEvent['locals'], + group: string +): Promise { + const session = await locals.auth(); + + if (!session || !session.user?.nickname) { + throw json({ error: 'Unauthorized' }, { status: 401 }); + } + + if (!session.user.groups?.includes(group)) { + throw json({ error: 'Forbidden' }, { status: 403 }); + } + + return { + nickname: session.user.nickname, + name: session.user.name ?? undefined, + email: session.user.email ?? undefined, + image: session.user.image ?? undefined + }; +} + /** * Optional authentication - returns user if authenticated, null otherwise. * Useful for routes that have different behavior for authenticated users. diff --git a/src/models/MarioKartTournament.ts b/src/models/MarioKartTournament.ts deleted file mode 100644 index 1bf4337..0000000 --- a/src/models/MarioKartTournament.ts +++ /dev/null @@ -1,228 +0,0 @@ -import mongoose from 'mongoose'; - -export interface IContestant { - _id?: string; - name: string; - seed?: number; // For bracket seeding - dnf?: boolean; // Did Not Finish - marked as inactive mid-tournament -} - -export interface IRound { - roundNumber: number; - scores: Map; // contestantId -> score - completedAt?: Date; -} - -export interface IGroupMatch { - _id?: string; - contestantIds: string[]; // All contestants in this match - rounds: IRound[]; - completed: boolean; -} - -export interface IGroup { - _id?: string; - name: string; - contestantIds: string[]; // References to contestants - matches: IGroupMatch[]; - standings?: { contestantId: string; totalScore: number; position: number }[]; -} - -export interface IBracketMatch { - _id?: string; - contestantIds: string[]; // Array of contestant IDs competing in this match - rounds: IRound[]; - winnerId?: string; - completed: boolean; -} - -export interface IBracketRound { - roundNumber: number; // 1 = finals, 2 = semis, 3 = quarters, etc. - name: string; // "Finals", "Semi-Finals", etc. - matches: IBracketMatch[]; -} - -export interface IBracket { - rounds: IBracketRound[]; -} - -export interface IMarioKartTournament { - _id?: string; - name: string; - status: 'setup' | 'group_stage' | 'bracket' | 'completed'; - contestants: IContestant[]; - groups: IGroup[]; - bracket?: IBracket; - runnersUpBracket?: IBracket; - roundsPerMatch: number; // How many rounds in each match - matchSize: number; // How many contestants compete simultaneously (default 2 for 1v1) - createdBy: string; - createdAt?: Date; - updatedAt?: Date; -} - -const RoundSchema = new mongoose.Schema({ - roundNumber: { - type: Number, - required: true, - min: 1 - }, - scores: { - type: Map, - of: Number, - required: true - }, - completedAt: { - type: Date - } -}); - -const GroupMatchSchema = new mongoose.Schema({ - contestantIds: { - type: [String], - required: true - }, - rounds: { - type: [RoundSchema], - default: [] - }, - completed: { - type: Boolean, - default: false - } -}); - -const GroupSchema = new mongoose.Schema({ - name: { - type: String, - required: true, - trim: true - }, - contestantIds: { - type: [String], - required: true - }, - matches: { - type: [GroupMatchSchema], - default: [] - }, - standings: [{ - contestantId: String, - totalScore: Number, - position: Number - }] -}); - -const BracketMatchSchema = new mongoose.Schema({ - contestantIds: { - type: [String], - default: [], - required: false - }, - rounds: { - type: [RoundSchema], - default: [] - }, - winnerId: { - type: String, - required: false - }, - completed: { - type: Boolean, - default: false - } -}, { _id: true, minimize: false }); - -const BracketRoundSchema = new mongoose.Schema({ - roundNumber: { - type: Number, - required: true - }, - name: { - type: String, - required: true - }, - matches: { - type: [BracketMatchSchema], - required: true - } -}, { _id: true, minimize: false }); - -const BracketSchema = new mongoose.Schema({ - rounds: { - type: [BracketRoundSchema], - default: [] - } -}, { _id: true, minimize: false }); - -const ContestantSchema = new mongoose.Schema({ - name: { - type: String, - required: true, - trim: true - }, - seed: { - type: Number - }, - dnf: { - type: Boolean, - default: false - } -}); - -const MarioKartTournamentSchema = new mongoose.Schema( - { - name: { - type: String, - required: true, - trim: true, - maxlength: 200 - }, - status: { - type: String, - enum: ['setup', 'group_stage', 'bracket', 'completed'], - default: 'setup' - }, - contestants: { - type: [ContestantSchema], - default: [] - }, - groups: { - type: [GroupSchema], - default: [] - }, - bracket: { - type: BracketSchema - }, - runnersUpBracket: { - type: BracketSchema - }, - roundsPerMatch: { - type: Number, - default: 3, - min: 1, - max: 10 - }, - matchSize: { - type: Number, - default: 2, - min: 2, - max: 12 - }, - createdBy: { - type: String, - required: true, - trim: true - } - }, - { - timestamps: true, - toJSON: { virtuals: true }, - toObject: { virtuals: true } - } -); - -MarioKartTournamentSchema.index({ createdBy: 1, createdAt: -1 }); - -export const MarioKartTournament = mongoose.models.MarioKartTournament || - mongoose.model("MarioKartTournament", MarioKartTournamentSchema); diff --git a/src/routes/api/[recipeLang=recipeLang]/add/+server.ts b/src/routes/api/[recipeLang=recipeLang]/add/+server.ts index aa0f083..f9c6a7d 100644 --- a/src/routes/api/[recipeLang=recipeLang]/add/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/add/+server.ts @@ -2,20 +2,12 @@ import type { RequestHandler } from '@sveltejs/kit'; import { Recipe } from '$models/Recipe'; import { dbConnect } from '$utils/db'; import { error } from '@sveltejs/kit'; -// header: use for bearer token for now -// recipe json in body -export const POST: RequestHandler = async ({request, cookies, locals}) => { +import { requireGroup } from '$lib/server/middleware/auth'; + +export const POST: RequestHandler = async ({request, locals}) => { + await requireGroup(locals, 'rezepte_users'); let message = await request.json() const recipe_json = message.recipe - let auth = await locals.auth(); - /*const user = session.user;*/ - console.log(auth) - if(!auth){ - throw error(401, "Not logged in") - } - /*if(!user.access.includes("rezepte")){ - throw error(401, "This user does not have permissions to add recipes") - }*/ await dbConnect(); try{ await Recipe.create(recipe_json); diff --git a/src/routes/api/[recipeLang=recipeLang]/delete/+server.ts b/src/routes/api/[recipeLang=recipeLang]/delete/+server.ts index 03f6dbc..ce9dd9b 100644 --- a/src/routes/api/[recipeLang=recipeLang]/delete/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/delete/+server.ts @@ -4,13 +4,11 @@ import { UserFavorites } from '$models/UserFavorites'; import { dbConnect } from '$utils/db'; import type {RecipeModelType} from '$types/types'; import { error } from '@sveltejs/kit'; -// header: use for bearer token for now -// recipe json in body -export const POST: RequestHandler = async ({request, locals}) => { - let message = await request.json() +import { requireGroup } from '$lib/server/middleware/auth'; - const auth = await locals.auth(); - if(!auth) throw error(401, "Need to be logged in") +export const POST: RequestHandler = async ({request, locals}) => { + await requireGroup(locals, 'rezepte_users'); + let message = await request.json() const short_name = message.old_short_name await dbConnect(); diff --git a/src/routes/api/[recipeLang=recipeLang]/edit/+server.ts b/src/routes/api/[recipeLang=recipeLang]/edit/+server.ts index f1b6e2d..dd7c45c 100644 --- a/src/routes/api/[recipeLang=recipeLang]/edit/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/edit/+server.ts @@ -6,50 +6,40 @@ import { error } from '@sveltejs/kit'; import { rename } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; +import { requireGroup } from '$lib/server/middleware/auth'; -// header: use for bearer token for now -// recipe json in body export const POST: RequestHandler = async ({request, locals}) => { + await requireGroup(locals, 'rezepte_users'); let message = await request.json() const recipe_json = message.recipe - const auth = await locals.auth(); - if(!auth){ - throw error(403, "Not logged in") + + await dbConnect(); + + // Check if short_name has changed + const oldShortName = message.old_short_name; + const newShortName = recipe_json.short_name; + + if (oldShortName !== newShortName) { + const imageDirectories = ['full', 'thumb']; + const staticPath = join(process.cwd(), 'static', 'rezepte'); + + for (const dir of imageDirectories) { + const oldPath = join(staticPath, dir, `${oldShortName}.webp`); + const newPath = join(staticPath, dir, `${newShortName}.webp`); + + if (existsSync(oldPath)) { + try { + await rename(oldPath, newPath); + } catch (err) { + console.error(`Failed to rename ${dir}/${oldShortName}.webp:`, err); + } + } + } } - else{ - await dbConnect(); - // Check if short_name has changed - const oldShortName = message.old_short_name; - const newShortName = recipe_json.short_name; + await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json); - if (oldShortName !== newShortName) { - // Rename image files in all three directories - const imageDirectories = ['full', 'thumb']; - const staticPath = join(process.cwd(), 'static', 'rezepte'); - - for (const dir of imageDirectories) { - const oldPath = join(staticPath, dir, `${oldShortName}.webp`); - const newPath = join(staticPath, dir, `${newShortName}.webp`); - - // Only rename if the old file exists - if (existsSync(oldPath)) { - try { - await rename(oldPath, newPath); - console.log(`Renamed ${dir}/${oldShortName}.webp -> ${dir}/${newShortName}.webp`); - } catch (err) { - console.error(`Failed to rename ${dir}/${oldShortName}.webp:`, err); - // Continue with other files even if one fails - } - } - } - } - - await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json); - - return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{ - status: 200, - }); - - } + return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{ + status: 200, + }); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts b/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts index 9fb91dc..f46ae37 100644 --- a/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts @@ -6,29 +6,10 @@ import sharp from 'sharp'; import { generateImageHashFromBuffer, getHashedFilename } from '$utils/imageHash'; import { validateImageFile } from '$utils/imageValidation'; import { extractDominantColor } from '$utils/imageProcessing'; +import { requireGroup } from '$lib/server/middleware/auth'; -/** - * Secure image upload endpoint for recipe images - * - * SECURITY: - * - Requires authentication - * - 5-layer validation (size, magic bytes, MIME, extension, Sharp) - * - Uses FormData instead of base64 JSON (more efficient, more secure) - * - Generates full/thumb versions + dominant color extraction - * - Content hash for cache busting - * - * @route POST /api/rezepte/img/add - */ export const POST = (async ({ request, locals }) => { - console.log('[API:ImgAdd] Image upload request received'); - - // Check authentication - const auth = await locals.auth(); - if (!auth) { - console.error('[API:ImgAdd] Authentication required'); - throw error(401, 'Authentication required to upload images'); - } - console.log('[API:ImgAdd] Authentication passed'); + await requireGroup(locals, 'rezepte_users'); try { const formData = await request.formData(); diff --git a/src/routes/api/[recipeLang=recipeLang]/img/delete/+server.ts b/src/routes/api/[recipeLang=recipeLang]/img/delete/+server.ts index 326d247..cf82f60 100644 --- a/src/routes/api/[recipeLang=recipeLang]/img/delete/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/img/delete/+server.ts @@ -3,28 +3,35 @@ import type { RequestHandler } from '@sveltejs/kit'; import { IMAGE_DIR } from '$env/static/private' import { unlink } from 'node:fs'; import { error } from '@sveltejs/kit'; +import { requireGroup } from '$lib/server/middleware/auth'; + +/** Ensure a resolved path stays within the allowed base directory */ +function assertWithinDir(base: string, resolved: string) { + if (!resolved.startsWith(path.resolve(base) + path.sep)) { + throw error(400, 'Invalid filename'); + } +} export const POST = (async ({ request, locals}) => { + await requireGroup(locals, 'rezepte_users'); const data = await request.json(); - const auth = await locals.auth() - if(!auth) throw error(401, "You need to be logged in") - // data.filename should be the full filename with hash (e.g., "maccaroni.a1b2c3d4.webp") - // For backward compatibility, also support data.name (will construct filename) const hashedFilename = data.filename || (data.name + ".webp"); - - // Also extract basename to delete unhashed version const basename = data.name || hashedFilename.replace(/\.[a-f0-9]{8}\.webp$/, '').replace(/\.webp$/, ''); const unhashedFilename = basename + '.webp'; + const recipeImgDir = path.join(IMAGE_DIR, "rezepte"); + [ "full", "thumb"].forEach((folder) => { - // Delete hashed version - unlink(path.join(IMAGE_DIR, "rezepte", folder, hashedFilename), (e) => { + const hashedPath = path.resolve(recipeImgDir, folder, hashedFilename); + const unhashedPath = path.resolve(recipeImgDir, folder, unhashedFilename); + assertWithinDir(recipeImgDir, hashedPath); + assertWithinDir(recipeImgDir, unhashedPath); + + unlink(hashedPath, (e) => { if(e) console.warn(`Could not delete hashed: ${folder}/${hashedFilename}`, e); }); - - // Delete unhashed version (for graceful degradation) - unlink(path.join(IMAGE_DIR, "rezepte", folder, unhashedFilename), (e) => { + unlink(unhashedPath, (e) => { if(e) console.warn(`Could not delete unhashed: ${folder}/${unhashedFilename}`, e); }); }) diff --git a/src/routes/api/[recipeLang=recipeLang]/img/mv/+server.ts b/src/routes/api/[recipeLang=recipeLang]/img/mv/+server.ts index acb602a..489f0c2 100644 --- a/src/routes/api/[recipeLang=recipeLang]/img/mv/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/img/mv/+server.ts @@ -4,33 +4,40 @@ import { IMAGE_DIR } from '$env/static/private' import { rename } from 'node:fs'; import { error } from '@sveltejs/kit'; import { extractBasename, getHashedFilename } from '$utils/imageHash'; +import { requireGroup } from '$lib/server/middleware/auth'; + +/** Ensure a resolved path stays within the allowed base directory */ +function assertWithinDir(base: string, resolved: string) { + if (!resolved.startsWith(path.resolve(base) + path.sep)) { + throw error(400, 'Invalid filename'); + } +} export const POST = (async ({ request, locals}) => { + await requireGroup(locals, 'rezepte_users'); const data = await request.json(); - const auth = await locals.auth(); - if(!auth ) throw error(401, "need to be logged in") - // data.old_filename should be the full filename with hash (e.g., "maccaroni.a1b2c3d4.webp") - // data.new_name should be the new basename (e.g., "pasta") - // Extract hash from old filename and apply to new basename const oldFilename = data.old_filename || (data.old_name + ".webp"); const hashMatch = oldFilename.match(/\.([a-f0-9]{8})\.webp$/); let newFilename: string; if (hashMatch) { - // Old filename has hash, preserve it const hash = hashMatch[1]; newFilename = getHashedFilename(data.new_name, hash); } else { - // Old filename has no hash (legacy), new one won't either newFilename = data.new_name + ".webp"; } + const recipeImgDir = path.join(IMAGE_DIR, "rezepte"); + [ "full", "thumb"].forEach((folder) => { - const old_path = path.join(IMAGE_DIR, "rezepte", folder, oldFilename) - rename(old_path, path.join(IMAGE_DIR, "rezepte", folder, newFilename), (e) => { - console.log(e) - if(e) throw error(500, "could not mv: " + old_path) + const oldPath = path.resolve(recipeImgDir, folder, oldFilename); + const newPath = path.resolve(recipeImgDir, folder, newFilename); + assertWithinDir(recipeImgDir, oldPath); + assertWithinDir(recipeImgDir, newPath); + + rename(oldPath, newPath, (e) => { + if(e) console.warn(`could not mv: ${oldPath}`, e) }) }); diff --git a/src/routes/api/[recipeLang=recipeLang]/nutrition/generate-all/+server.ts b/src/routes/api/[recipeLang=recipeLang]/nutrition/generate-all/+server.ts index 5970f08..57fa59c 100644 --- a/src/routes/api/[recipeLang=recipeLang]/nutrition/generate-all/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/nutrition/generate-all/+server.ts @@ -2,9 +2,10 @@ import { json, type RequestHandler } from '@sveltejs/kit'; import { Recipe } from '$models/Recipe'; import { dbConnect } from '$utils/db'; import { generateNutritionMappings } from '$lib/server/nutritionMatcher'; +import { requireGroup } from '$lib/server/middleware/auth'; export const POST: RequestHandler = async ({ locals }) => { - await locals.auth(); + await requireGroup(locals, 'rezepte_users'); await dbConnect(); const recipes = await Recipe.find({}).lean(); diff --git a/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts b/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts index 25e4129..89d5b04 100644 --- a/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts @@ -1,19 +1,10 @@ import { json, error } from '@sveltejs/kit'; import { translationService } from '$lib/../utils/translation'; import type { RequestHandler } from './$types'; +import { requireGroup } from '$lib/server/middleware/auth'; -/** - * POST /api/rezepte/translate - * Translates recipe data from German to English using DeepL API - * - * Request body: - * - recipe: Recipe object with German content - * - fields?: Optional array of specific fields to translate (for partial updates) - * - * Response: - * - translatedRecipe: Translated recipe data - */ -export const POST: RequestHandler = async ({ request }) => { +export const POST: RequestHandler = async ({ request, locals }) => { + await requireGroup(locals, 'rezepte_users'); try { const body = await request.json(); const { recipe, fields, oldRecipe, existingTranslation } = body; diff --git a/src/routes/api/fitness/period/shared/+server.ts b/src/routes/api/fitness/period/shared/+server.ts index 62a70ad..e62732b 100644 --- a/src/routes/api/fitness/period/shared/+server.ts +++ b/src/routes/api/fitness/period/shared/+server.ts @@ -11,7 +11,8 @@ export const GET: RequestHandler = async ({ locals }) => { await dbConnect(); // Find all share docs where current user is in the sharedWith list - const shares = await PeriodShare.find({ sharedWith: user.nickname }).lean(); + // sharedWith stores lowercase usernames, so normalize for lookup + const shares = await PeriodShare.find({ sharedWith: user.nickname.toLowerCase() }).lean(); const owners = shares.map(s => s.owner); if (owners.length === 0) return json({ shared: [] }); diff --git a/src/routes/api/generate-alt-text-bulk/+server.ts b/src/routes/api/generate-alt-text-bulk/+server.ts index 250a075..56676d9 100644 --- a/src/routes/api/generate-alt-text-bulk/+server.ts +++ b/src/routes/api/generate-alt-text-bulk/+server.ts @@ -3,13 +3,10 @@ import type { RequestHandler } from './$types'; import { generateAltText, type RecipeContext } from '$lib/server/ai/alttext.js'; import { checkOllamaHealth } from '$lib/server/ai/ollama.js'; import { Recipe } from '$models/Recipe.js'; +import { requireGroup } from '$lib/server/middleware/auth'; export const POST: RequestHandler = async ({ request, locals }) => { - // Check authentication - const session = await locals.auth(); - if (!session?.user) { - throw error(401, 'Unauthorized'); - } + await requireGroup(locals, 'rezepte_users'); try { const body = await request.json(); diff --git a/src/routes/api/generate-alt-text/+server.ts b/src/routes/api/generate-alt-text/+server.ts index 35ccf87..afb2163 100644 --- a/src/routes/api/generate-alt-text/+server.ts +++ b/src/routes/api/generate-alt-text/+server.ts @@ -3,13 +3,10 @@ import type { RequestHandler } from './$types'; import { generateAltText, type RecipeContext } from '$lib/server/ai/alttext.js'; import { checkOllamaHealth } from '$lib/server/ai/ollama.js'; import { Recipe } from '$models/Recipe.js'; +import { requireGroup } from '$lib/server/middleware/auth'; export const POST: RequestHandler = async ({ request, locals }) => { - // Check authentication - const session = await locals.auth(); - if (!session?.user) { - throw error(401, 'Unauthorized'); - } + await requireGroup(locals, 'rezepte_users'); try { const body = await request.json(); diff --git a/src/routes/api/mario-kart/tournaments/+server.ts b/src/routes/api/mario-kart/tournaments/+server.ts deleted file mode 100644 index 6ba65a8..0000000 --- a/src/routes/api/mario-kart/tournaments/+server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; - -// GET /api/mario-kart/tournaments - Get all tournaments -export const GET: RequestHandler = async () => { - try { - await dbConnect(); - - const tournaments = await MarioKartTournament.find() - .sort({ createdAt: -1 }); - - return json({ tournaments }); - } catch (error) { - console.error('Error fetching tournaments:', error); - return json({ error: 'Failed to fetch tournaments' }, { status: 500 }); - } -}; - -// POST /api/mario-kart/tournaments - Create a new tournament -export const POST: RequestHandler = async ({ request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { name, roundsPerMatch = 3, matchSize = 2 } = data; - - if (!name) { - return json({ error: 'Tournament name is required' }, { status: 400 }); - } - - const tournament = new MarioKartTournament({ - name, - roundsPerMatch, - matchSize, - status: 'setup', - createdBy: 'anonymous' - }); - - await tournament.save(); - - return json({ tournament }, { status: 201 }); - } catch (error) { - console.error('Error creating tournament:', error); - return json({ error: 'Failed to create tournament' }, { status: 500 }); - } -}; diff --git a/src/routes/api/mario-kart/tournaments/[id]/+server.ts b/src/routes/api/mario-kart/tournaments/[id]/+server.ts deleted file mode 100644 index 0bfbfd6..0000000 --- a/src/routes/api/mario-kart/tournaments/[id]/+server.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; - -// GET /api/mario-kart/tournaments/[id] - Get a specific tournament -export const GET: RequestHandler = async ({ params }) => { - try { - await dbConnect(); - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - return json({ tournament }); - } catch (error) { - console.error('Error fetching tournament:', error); - return json({ error: 'Failed to fetch tournament' }, { status: 500 }); - } -}; - -// PUT /api/mario-kart/tournaments/[id] - Update tournament -export const PUT: RequestHandler = async ({ params, request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { name, roundsPerMatch, status } = data; - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - if (name) tournament.name = name; - if (roundsPerMatch) tournament.roundsPerMatch = roundsPerMatch; - if (status) tournament.status = status; - - await tournament.save(); - - return json({ tournament }); - } catch (error) { - console.error('Error updating tournament:', error); - return json({ error: 'Failed to update tournament' }, { status: 500 }); - } -}; - -// DELETE /api/mario-kart/tournaments/[id] - Delete tournament -export const DELETE: RequestHandler = async ({ params }) => { - try { - await dbConnect(); - - const result = await MarioKartTournament.deleteOne({ _id: params.id }); - - if (result.deletedCount === 0) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - return json({ success: true }); - } catch (error) { - console.error('Error deleting tournament:', error); - return json({ error: 'Failed to delete tournament' }, { status: 500 }); - } -}; diff --git a/src/routes/api/mario-kart/tournaments/[id]/bracket/+server.ts b/src/routes/api/mario-kart/tournaments/[id]/bracket/+server.ts deleted file mode 100644 index 3de8083..0000000 --- a/src/routes/api/mario-kart/tournaments/[id]/bracket/+server.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; -import mongoose from 'mongoose'; - -interface BracketMatch { - _id: string; - contestantIds: string[]; - rounds: any[]; - completed: boolean; - winnerId?: string; -} - -interface BracketRound { - roundNumber: number; - name: string; - matches: BracketMatch[]; -} - -// POST /api/mario-kart/tournaments/[id]/bracket - Generate tournament bracket -export const POST: RequestHandler = async ({ params, request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { topNFromEachGroup = 2 } = data; - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - if (tournament.status !== 'group_stage') { - return json({ error: 'Can only generate bracket from group stage' }, { status: 400 }); - } - - // Collect top contestants from each group for main bracket - const qualifiedContestants: string[] = []; - const nonQualifiedContestants: string[] = []; - - for (const group of tournament.groups) { - if (!group.standings || group.standings.length === 0) { - return json({ error: `Group ${group.name} has no standings yet` }, { status: 400 }); - } - - const sortedStandings = group.standings.sort((a: any, b: any) => a.position - b.position); - - // Top N qualify for main bracket - const topContestants = sortedStandings - .slice(0, topNFromEachGroup) - .map((s: any) => s.contestantId); - qualifiedContestants.push(...topContestants); - - // Remaining contestants go to consolation bracket - const remainingContestants = sortedStandings - .slice(topNFromEachGroup) - .map((s: any) => s.contestantId); - nonQualifiedContestants.push(...remainingContestants); - } - - const matchSize = tournament.matchSize || 2; - - if (qualifiedContestants.length < matchSize) { - return json({ error: `Need at least ${matchSize} qualified contestants for bracket` }, { status: 400 }); - } - - // Calculate bracket size based on matchSize - // We need enough slots so that contestants can be evenly divided by matchSize at each round - const bracketSize = Math.pow(matchSize, Math.ceil(Math.log(qualifiedContestants.length) / Math.log(matchSize))); - - // Generate bracket rounds - const rounds: BracketRound[] = []; - let currentContestants = bracketSize; - let roundNumber = 1; - - // Calculate total number of rounds - while (currentContestants > 1) { - currentContestants = currentContestants / matchSize; - roundNumber++; - } - - // Build rounds from smallest (finals) to largest (first round) - currentContestants = bracketSize; - roundNumber = Math.ceil(Math.log(bracketSize) / Math.log(matchSize)); - const totalRounds = roundNumber; - - // Build from finals (roundNumber 1) to first round (highest roundNumber) - for (let rn = 1; rn <= totalRounds; rn++) { - const roundName = rn === 1 ? 'Finals' : - rn === 2 ? 'Semi-Finals' : - rn === 3 ? 'Quarter-Finals' : - rn === 4 ? 'Round of 16' : - rn === 5 ? 'Round of 32' : - `Round ${rn}`; - - const matchesInRound = Math.pow(matchSize, rn - 1); - - rounds.push({ - roundNumber: rn, - name: roundName, - matches: [] - }); - } - - // Populate last round (highest roundNumber, most matches) with contestants - const firstRound = rounds[rounds.length - 1]; - const matchesInFirstRound = bracketSize / matchSize; - - for (let i = 0; i < matchesInFirstRound; i++) { - const contestantIds: string[] = []; - - for (let j = 0; j < matchSize; j++) { - const contestantIndex = i * matchSize + j; - if (contestantIndex < qualifiedContestants.length) { - contestantIds.push(qualifiedContestants[contestantIndex]); - } - } - - firstRound.matches.push({ - _id: new mongoose.Types.ObjectId().toString(), - contestantIds, - rounds: [], - completed: false - }); - } - - // Create empty matches for other rounds (finals to second-to-last round) - for (let i = 0; i < rounds.length - 1; i++) { - const matchesInRound = Math.pow(matchSize, rounds[i].roundNumber - 1); - for (let j = 0; j < matchesInRound; j++) { - rounds[i].matches.push({ - _id: new mongoose.Types.ObjectId().toString(), - contestantIds: [], - rounds: [], - completed: false - }); - } - } - - // Explicitly cast to ensure Mongoose properly saves the structure - tournament.bracket = { - rounds: rounds.map(round => ({ - roundNumber: round.roundNumber, - name: round.name, - matches: round.matches.map(match => ({ - _id: match._id, - contestantIds: match.contestantIds || [], - rounds: match.rounds || [], - winnerId: match.winnerId, - completed: match.completed || false - })) - })) - }; - - // Create consolation bracket for non-qualifiers - const runnersUpRounds: BracketRound[] = []; - if (nonQualifiedContestants.length >= matchSize) { - // Calculate consolation bracket size - const consolationBracketSize = Math.pow(matchSize, Math.ceil(Math.log(nonQualifiedContestants.length) / Math.log(matchSize))); - const consolationTotalRounds = Math.ceil(Math.log(consolationBracketSize) / Math.log(matchSize)); - - // Build consolation rounds from finals to first round - for (let rn = 1; rn <= consolationTotalRounds; rn++) { - const roundName = rn === 1 ? '3rd Place Match' : - rn === 2 ? 'Consolation Semi-Finals' : - rn === 3 ? 'Consolation Quarter-Finals' : - `Consolation Round ${rn}`; - - runnersUpRounds.push({ - roundNumber: rn, - name: roundName, - matches: [] - }); - } - - // Populate last round (first round of competition) with non-qualified contestants - const consolationFirstRound = runnersUpRounds[runnersUpRounds.length - 1]; - const consolationMatchesInFirstRound = consolationBracketSize / matchSize; - - for (let i = 0; i < consolationMatchesInFirstRound; i++) { - const contestantIds: string[] = []; - for (let j = 0; j < matchSize; j++) { - const contestantIndex = i * matchSize + j; - if (contestantIndex < nonQualifiedContestants.length) { - contestantIds.push(nonQualifiedContestants[contestantIndex]); - } - } - - consolationFirstRound.matches.push({ - _id: new mongoose.Types.ObjectId().toString(), - contestantIds, - rounds: [], - completed: false - }); - } - - // Create empty matches for other consolation rounds - for (let i = 0; i < runnersUpRounds.length - 1; i++) { - const matchesInRound = Math.pow(matchSize, runnersUpRounds[i].roundNumber - 1); - for (let j = 0; j < matchesInRound; j++) { - runnersUpRounds[i].matches.push({ - _id: new mongoose.Types.ObjectId().toString(), - contestantIds: [], - rounds: [], - completed: false - }); - } - } - } - - tournament.runnersUpBracket = { - rounds: runnersUpRounds.map(round => ({ - roundNumber: round.roundNumber, - name: round.name, - matches: round.matches.map(match => ({ - _id: match._id, - contestantIds: match.contestantIds || [], - rounds: match.rounds || [], - winnerId: match.winnerId, - completed: match.completed || false - })) - })) - }; - - tournament.status = 'bracket'; - - // Mark as modified to ensure Mongoose saves nested objects - tournament.markModified('bracket'); - tournament.markModified('runnersUpBracket'); - - await tournament.save(); - - return json({ tournament }); - } catch (error) { - console.error('Error generating bracket:', error); - return json({ error: 'Failed to generate bracket' }, { status: 500 }); - } -}; diff --git a/src/routes/api/mario-kart/tournaments/[id]/bracket/matches/[matchId]/scores/+server.ts b/src/routes/api/mario-kart/tournaments/[id]/bracket/matches/[matchId]/scores/+server.ts deleted file mode 100644 index db4584f..0000000 --- a/src/routes/api/mario-kart/tournaments/[id]/bracket/matches/[matchId]/scores/+server.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; - -// POST /api/mario-kart/tournaments/[id]/bracket/matches/[matchId]/scores - Update bracket match scores -export const POST: RequestHandler = async ({ params, request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { roundNumber, scores } = data; - - if (!roundNumber || !scores) { - return json({ error: 'roundNumber and scores are required' }, { status: 400 }); - } - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - if (!tournament.bracket) { - return json({ error: 'Tournament has no bracket' }, { status: 404 }); - } - - // Find the match in either main or runners-up bracket - let match: any = null; - let matchRound: any = null; - let matchRoundIndex = -1; - let isRunnersUp = false; - let bracket = tournament.bracket; - - console.log('Bracket structure:', JSON.stringify(bracket, null, 2)); - - if (!bracket.rounds || !Array.isArray(bracket.rounds)) { - return json({ error: 'Bracket has no rounds array' }, { status: 500 }); - } - - for (let i = 0; i < bracket.rounds.length; i++) { - const round = bracket.rounds[i]; - if (!round.matches || !Array.isArray(round.matches)) { - console.error(`Round ${i} has no matches array:`, round); - continue; - } - const foundMatch = round.matches.find((m: any) => m._id?.toString() === params.matchId); - if (foundMatch) { - match = foundMatch; - matchRound = round; - matchRoundIndex = i; - break; - } - } - - // If not found in main bracket, check runners-up bracket - if (!match && tournament.runnersUpBracket) { - bracket = tournament.runnersUpBracket; - isRunnersUp = true; - for (let i = 0; i < bracket.rounds.length; i++) { - const round = bracket.rounds[i]; - const foundMatch = round.matches.find((m: any) => m._id?.toString() === params.matchId); - if (foundMatch) { - match = foundMatch; - matchRound = round; - matchRoundIndex = i; - break; - } - } - } - - if (!match) { - return json({ error: 'Match not found' }, { status: 404 }); - } - - // Add or update round - const existingRoundIndex = match.rounds.findIndex((r: any) => r.roundNumber === roundNumber); - const scoresMap = new Map(Object.entries(scores)); - - if (existingRoundIndex >= 0) { - match.rounds[existingRoundIndex].scores = scoresMap; - match.rounds[existingRoundIndex].completedAt = new Date(); - } else { - match.rounds.push({ - roundNumber, - scores: scoresMap, - completedAt: new Date() - }); - } - - // Check if all rounds are complete for this match - if (match.rounds.length >= tournament.roundsPerMatch) { - match.completed = true; - - // Calculate winner (highest total score) - const totalScores = new Map(); - for (const round of match.rounds) { - for (const [contestantId, score] of round.scores) { - totalScores.set(contestantId, (totalScores.get(contestantId) || 0) + score); - } - } - - const sortedScores = Array.from(totalScores.entries()) - .sort((a, b) => b[1] - a[1]); - - if (sortedScores.length > 0) { - match.winnerId = sortedScores[0][0]; - const matchSize = tournament.matchSize || 2; - - // Collect all non-winners for runners-up bracket (2nd place and below) - const nonWinners = sortedScores.slice(1).map(([contestantId]) => contestantId); - const secondPlace = sortedScores.length > 1 ? sortedScores[1][0] : null; - - // Advance winner to next round if not finals - if (matchRoundIndex > 0) { - console.log('Advancing winner to next round', { matchRoundIndex, bracketRoundsLength: bracket.rounds.length }); - const nextRound = bracket.rounds[matchRoundIndex - 1]; - console.log('Next round:', nextRound); - const matchIndexInRound = matchRound.matches.findIndex((m: any) => m._id?.toString() === params.matchId); - const nextMatchIndex = Math.floor(matchIndexInRound / matchSize); - - if (nextRound && nextMatchIndex < nextRound.matches.length) { - const nextMatch = nextRound.matches[nextMatchIndex]; - - // Add winner to the next match's contestant list - if (!nextMatch.contestantIds.includes(match.winnerId)) { - nextMatch.contestantIds.push(match.winnerId); - } - } - } - - // Move second place to runners-up bracket (only from main bracket, not from runners-up) - // Note: For matchSize > 2, we only send 2nd place to consolation bracket - if (!isRunnersUp && secondPlace && tournament.runnersUpBracket) { - const matchIndexInRound = matchRound.matches.findIndex((m: any) => m._id?.toString() === params.matchId); - - // For the first round of losers, they go to the last round of runners-up bracket - if (matchRoundIndex === bracket.rounds.length - 1) { - const runnersUpLastRound = tournament.runnersUpBracket.rounds[tournament.runnersUpBracket.rounds.length - 1]; - const targetMatchIndex = Math.floor(matchIndexInRound / matchSize); - - if (targetMatchIndex < runnersUpLastRound.matches.length) { - const targetMatch = runnersUpLastRound.matches[targetMatchIndex]; - - // Add second place to runners-up bracket - if (!targetMatch.contestantIds.includes(secondPlace)) { - targetMatch.contestantIds.push(secondPlace); - } - } - } - } - } - } - - // Check if tournament is completed (both finals and 3rd place match completed) - const finals = tournament.bracket.rounds[0]; - const thirdPlaceMatch = tournament.runnersUpBracket?.rounds?.[0]; - - const mainBracketComplete = finals?.matches?.length > 0 && finals.matches[0].completed; - const runnersUpComplete = !thirdPlaceMatch || (thirdPlaceMatch?.matches?.length > 0 && thirdPlaceMatch.matches[0].completed); - - if (mainBracketComplete && runnersUpComplete) { - tournament.status = 'completed'; - } - - await tournament.save(); - - return json({ tournament }); - } catch (error) { - console.error('Error updating bracket scores:', error); - return json({ error: 'Failed to update bracket scores' }, { status: 500 }); - } -}; diff --git a/src/routes/api/mario-kart/tournaments/[id]/contestants/+server.ts b/src/routes/api/mario-kart/tournaments/[id]/contestants/+server.ts deleted file mode 100644 index 17ea8e1..0000000 --- a/src/routes/api/mario-kart/tournaments/[id]/contestants/+server.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; -import mongoose from 'mongoose'; - -// POST /api/mario-kart/tournaments/[id]/contestants - Add a contestant -export const POST: RequestHandler = async ({ params, request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { name } = data; - - if (!name) { - return json({ error: 'Contestant name is required' }, { status: 400 }); - } - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - // Check for duplicate names - if (tournament.contestants.some((c: any) => c.name === name)) { - return json({ error: 'Contestant with this name already exists' }, { status: 400 }); - } - - const newContestantId = new mongoose.Types.ObjectId().toString(); - - tournament.contestants.push({ - _id: newContestantId, - name - }); - - // If tournament is in group stage, add contestant to all group matches with 0 scores - if (tournament.status === 'group_stage' && tournament.groups.length > 0) { - for (const group of tournament.groups) { - // Add contestant to group's contestant list - group.contestantIds.push(newContestantId); - - // Add contestant to all matches in this group with 0 scores for completed rounds - for (const match of group.matches) { - match.contestantIds.push(newContestantId); - - // Add 0 score for all completed rounds - for (const round of match.rounds) { - if (!round.scores) { - round.scores = new Map(); - } - round.scores.set(newContestantId, 0); - } - } - - // Update group standings to include new contestant with 0 score - if (group.standings) { - group.standings.push({ - contestantId: newContestantId, - totalScore: 0, - position: group.standings.length + 1 - }); - } - } - } - - await tournament.save(); - - return json({ tournament }, { status: 201 }); - } catch (error) { - console.error('Error adding contestant:', error); - return json({ error: 'Failed to add contestant' }, { status: 500 }); - } -}; - -// DELETE /api/mario-kart/tournaments/[id]/contestants - Remove a contestant -export const DELETE: RequestHandler = async ({ params, url }) => { - try { - await dbConnect(); - - const contestantId = url.searchParams.get('contestantId'); - if (!contestantId) { - return json({ error: 'Contestant ID is required' }, { status: 400 }); - } - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - if (tournament.status !== 'setup') { - return json({ error: 'Cannot remove contestants after setup phase' }, { status: 400 }); - } - - tournament.contestants = tournament.contestants.filter( - (c: any) => c._id?.toString() !== contestantId - ); - - await tournament.save(); - - return json({ tournament }); - } catch (error) { - console.error('Error removing contestant:', error); - return json({ error: 'Failed to remove contestant' }, { status: 500 }); - } -}; diff --git a/src/routes/api/mario-kart/tournaments/[id]/contestants/[contestantId]/dnf/+server.ts b/src/routes/api/mario-kart/tournaments/[id]/contestants/[contestantId]/dnf/+server.ts deleted file mode 100644 index 004332c..0000000 --- a/src/routes/api/mario-kart/tournaments/[id]/contestants/[contestantId]/dnf/+server.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; - -// PATCH /api/mario-kart/tournaments/[id]/contestants/[contestantId]/dnf - Toggle DNF status -export const PATCH: RequestHandler = async ({ params, request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { dnf } = data; - - if (typeof dnf !== 'boolean') { - return json({ error: 'DNF status must be a boolean' }, { status: 400 }); - } - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - // Find the contestant in the contestants array - const contestant = tournament.contestants.find( - (c: any) => c._id?.toString() === params.contestantId - ); - - if (!contestant) { - return json({ error: 'Contestant not found' }, { status: 404 }); - } - - // Update the DNF status - contestant.dnf = dnf; - - await tournament.save(); - - return json({ tournament }); - } catch (error) { - console.error('Error updating contestant DNF status:', error); - return json({ error: 'Failed to update contestant status' }, { status: 500 }); - } -}; diff --git a/src/routes/api/mario-kart/tournaments/[id]/groups/+server.ts b/src/routes/api/mario-kart/tournaments/[id]/groups/+server.ts deleted file mode 100644 index 40ce04c..0000000 --- a/src/routes/api/mario-kart/tournaments/[id]/groups/+server.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; -import mongoose from 'mongoose'; - -// POST /api/mario-kart/tournaments/[id]/groups - Create groups for tournament -export const POST: RequestHandler = async ({ params, request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { numberOfGroups, maxUsersPerGroup, groupConfigs } = data; - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - if (tournament.contestants.length < 2) { - return json({ error: 'Need at least 2 contestants to create groups' }, { status: 400 }); - } - - // If groupConfigs are provided, use them. Otherwise, auto-assign - if (groupConfigs && Array.isArray(groupConfigs)) { - tournament.groups = groupConfigs.map((config: any) => ({ - _id: new mongoose.Types.ObjectId().toString(), - name: config.name, - contestantIds: config.contestantIds, - matches: [], - standings: [] - })); - } else if (numberOfGroups) { - // Auto-assign contestants to groups based on number of groups - // Shuffle contestants for random assignment - const contestants = [...tournament.contestants].sort(() => Math.random() - 0.5); - const groupSize = Math.ceil(contestants.length / numberOfGroups); - - tournament.groups = []; - for (let i = 0; i < numberOfGroups; i++) { - const groupContestants = contestants.slice(i * groupSize, (i + 1) * groupSize); - if (groupContestants.length > 0) { - tournament.groups.push({ - _id: new mongoose.Types.ObjectId().toString(), - name: `Group ${String.fromCharCode(65 + i)}`, // A, B, C, etc. - contestantIds: groupContestants.map(c => c._id!.toString()), - matches: [], - standings: [] - }); - } - } - } else if (maxUsersPerGroup) { - // Auto-assign contestants to groups based on max users per group - // Shuffle contestants for random assignment - const contestants = [...tournament.contestants].sort(() => Math.random() - 0.5); - const numberOfGroupsNeeded = Math.ceil(contestants.length / maxUsersPerGroup); - - tournament.groups = []; - for (let i = 0; i < numberOfGroupsNeeded; i++) { - const groupContestants = contestants.slice(i * maxUsersPerGroup, (i + 1) * maxUsersPerGroup); - if (groupContestants.length > 0) { - tournament.groups.push({ - _id: new mongoose.Types.ObjectId().toString(), - name: `Group ${String.fromCharCode(65 + i)}`, // A, B, C, etc. - contestantIds: groupContestants.map(c => c._id!.toString()), - matches: [], - standings: [] - }); - } - } - } else { - return json({ error: 'Either numberOfGroups, maxUsersPerGroup, or groupConfigs is required' }, { status: 400 }); - } - - // Create matches for each group (round-robin style where everyone plays together) - for (const group of tournament.groups) { - if (group.contestantIds.length >= 2) { - // Create one match with all contestants - group.matches.push({ - _id: new mongoose.Types.ObjectId().toString(), - contestantIds: group.contestantIds, - rounds: [], - completed: false - }); - } - } - - tournament.status = 'group_stage'; - await tournament.save(); - - return json({ tournament }); - } catch (error) { - console.error('Error creating groups:', error); - return json({ error: 'Failed to create groups' }, { status: 500 }); - } -}; diff --git a/src/routes/api/mario-kart/tournaments/[id]/groups/[groupId]/scores/+server.ts b/src/routes/api/mario-kart/tournaments/[id]/groups/[groupId]/scores/+server.ts deleted file mode 100644 index 25178b0..0000000 --- a/src/routes/api/mario-kart/tournaments/[id]/groups/[groupId]/scores/+server.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { dbConnect } from '$utils/db'; -import { MarioKartTournament } from '$models/MarioKartTournament'; - -// POST /api/mario-kart/tournaments/[id]/groups/[groupId]/scores - Add/update scores for a round -export const POST: RequestHandler = async ({ params, request }) => { - try { - await dbConnect(); - - const data = await request.json(); - const { matchId, roundNumber, scores } = data; - - if (!matchId || !roundNumber || !scores) { - return json({ error: 'matchId, roundNumber, and scores are required' }, { status: 400 }); - } - - const tournament = await MarioKartTournament.findById(params.id); - - if (!tournament) { - return json({ error: 'Tournament not found' }, { status: 404 }); - } - - const group = tournament.groups.find((g: any) => g._id?.toString() === params.groupId); - if (!group) { - return json({ error: 'Group not found' }, { status: 404 }); - } - - const match = group.matches.find((m: any) => m._id?.toString() === matchId); - if (!match) { - return json({ error: 'Match not found' }, { status: 404 }); - } - - // Add or update round - const existingRoundIndex = match.rounds.findIndex((r: any) => r.roundNumber === roundNumber); - const scoresMap = new Map(Object.entries(scores)); - - if (existingRoundIndex >= 0) { - match.rounds[existingRoundIndex].scores = scoresMap; - match.rounds[existingRoundIndex].completedAt = new Date(); - } else { - match.rounds.push({ - roundNumber, - scores: scoresMap, - completedAt: new Date() - }); - } - - // Check if all rounds are complete for this match - match.completed = match.rounds.length >= tournament.roundsPerMatch; - - // Calculate group standings - const standings = new Map(); - - for (const m of group.matches) { - for (const round of m.rounds) { - for (const [contestantId, score] of round.scores) { - standings.set(contestantId, (standings.get(contestantId) || 0) + score); - } - } - } - - // Convert to sorted array - group.standings = Array.from(standings.entries()) - .map(([contestantId, totalScore]) => ({ contestantId, totalScore, position: 0 })) - .sort((a, b) => b.totalScore - a.totalScore) - .map((entry, index) => ({ ...entry, position: index + 1 })); - - await tournament.save(); - - return json({ tournament }); - } catch (error) { - console.error('Error updating scores:', error); - return json({ error: 'Failed to update scores' }, { status: 500 }); - } -}; diff --git a/src/routes/api/nutrition/overwrites/+server.ts b/src/routes/api/nutrition/overwrites/+server.ts index 3ac2f91..75004a9 100644 --- a/src/routes/api/nutrition/overwrites/+server.ts +++ b/src/routes/api/nutrition/overwrites/+server.ts @@ -2,10 +2,11 @@ import { json, error, type RequestHandler } from '@sveltejs/kit'; import { dbConnect } from '$utils/db'; import { NutritionOverwrite } from '$models/NutritionOverwrite'; import { invalidateOverwriteCache } from '$lib/server/nutritionMatcher'; +import { requireAuth } from '$lib/server/middleware/auth'; /** GET: List all global nutrition overwrites */ export const GET: RequestHandler = async ({ locals }) => { - await locals.auth(); + await requireAuth(locals); await dbConnect(); const overwrites = await NutritionOverwrite.find({}).sort({ ingredientNameDe: 1 }).lean(); return json(overwrites); @@ -13,7 +14,7 @@ export const GET: RequestHandler = async ({ locals }) => { /** POST: Create a new global nutrition overwrite */ export const POST: RequestHandler = async ({ request, locals }) => { - await locals.auth(); + await requireAuth(locals); await dbConnect(); const body = await request.json(); @@ -43,7 +44,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { /** DELETE: Remove a global nutrition overwrite by ingredientNameDe */ export const DELETE: RequestHandler = async ({ request, locals }) => { - await locals.auth(); + await requireAuth(locals); await dbConnect(); const body = await request.json(); diff --git a/src/routes/api/recalculate-image-colors/+server.ts b/src/routes/api/recalculate-image-colors/+server.ts index e1cb8e7..6a32138 100644 --- a/src/routes/api/recalculate-image-colors/+server.ts +++ b/src/routes/api/recalculate-image-colors/+server.ts @@ -5,12 +5,10 @@ import { IMAGE_DIR } from '$env/static/private'; import { extractDominantColor } from '$utils/imageProcessing'; import { join } from 'path'; import { access, constants } from 'fs/promises'; +import { requireGroup } from '$lib/server/middleware/auth'; export const POST: RequestHandler = async ({ request, locals }) => { - const session = await locals.auth(); - if (!session?.user) { - throw error(401, 'Unauthorized'); - } + await requireGroup(locals, 'rezepte_users'); try { const body = await request.json();