security: enforce auth on all API write endpoints, remove mario-kart
Some checks failed
CI / update (push) Has been cancelled
Some checks failed
CI / update (push) Has been cancelled
- 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
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.4.4",
|
"version": "1.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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<AuthenticatedUser> {
|
||||||
|
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.
|
* Optional authentication - returns user if authenticated, null otherwise.
|
||||||
* Useful for routes that have different behavior for authenticated users.
|
* Useful for routes that have different behavior for authenticated users.
|
||||||
|
|||||||
@@ -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<string, number>; // 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<IMarioKartTournament>("MarioKartTournament", MarioKartTournamentSchema);
|
|
||||||
@@ -2,20 +2,12 @@ import type { RequestHandler } from '@sveltejs/kit';
|
|||||||
import { Recipe } from '$models/Recipe';
|
import { Recipe } from '$models/Recipe';
|
||||||
import { dbConnect } from '$utils/db';
|
import { dbConnect } from '$utils/db';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
// header: use for bearer token for now
|
import { requireGroup } from '$lib/server/middleware/auth';
|
||||||
// recipe json in body
|
|
||||||
export const POST: RequestHandler = async ({request, cookies, locals}) => {
|
export const POST: RequestHandler = async ({request, locals}) => {
|
||||||
|
await requireGroup(locals, 'rezepte_users');
|
||||||
let message = await request.json()
|
let message = await request.json()
|
||||||
const recipe_json = message.recipe
|
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();
|
await dbConnect();
|
||||||
try{
|
try{
|
||||||
await Recipe.create(recipe_json);
|
await Recipe.create(recipe_json);
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import { UserFavorites } from '$models/UserFavorites';
|
|||||||
import { dbConnect } from '$utils/db';
|
import { dbConnect } from '$utils/db';
|
||||||
import type {RecipeModelType} from '$types/types';
|
import type {RecipeModelType} from '$types/types';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
// header: use for bearer token for now
|
import { requireGroup } from '$lib/server/middleware/auth';
|
||||||
// recipe json in body
|
|
||||||
export const POST: RequestHandler = async ({request, locals}) => {
|
|
||||||
let message = await request.json()
|
|
||||||
|
|
||||||
const auth = await locals.auth();
|
export const POST: RequestHandler = async ({request, locals}) => {
|
||||||
if(!auth) throw error(401, "Need to be logged in")
|
await requireGroup(locals, 'rezepte_users');
|
||||||
|
let message = await request.json()
|
||||||
|
|
||||||
const short_name = message.old_short_name
|
const short_name = message.old_short_name
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|||||||
@@ -6,50 +6,40 @@ import { error } from '@sveltejs/kit';
|
|||||||
import { rename } from 'fs/promises';
|
import { rename } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { existsSync } from 'fs';
|
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}) => {
|
export const POST: RequestHandler = async ({request, locals}) => {
|
||||||
|
await requireGroup(locals, 'rezepte_users');
|
||||||
let message = await request.json()
|
let message = await request.json()
|
||||||
const recipe_json = message.recipe
|
const recipe_json = message.recipe
|
||||||
const auth = await locals.auth();
|
|
||||||
if(!auth){
|
await dbConnect();
|
||||||
throw error(403, "Not logged in")
|
|
||||||
|
// 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
|
await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json);
|
||||||
const oldShortName = message.old_short_name;
|
|
||||||
const newShortName = recipe_json.short_name;
|
|
||||||
|
|
||||||
if (oldShortName !== newShortName) {
|
return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{
|
||||||
// Rename image files in all three directories
|
status: 200,
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,29 +6,10 @@ import sharp from 'sharp';
|
|||||||
import { generateImageHashFromBuffer, getHashedFilename } from '$utils/imageHash';
|
import { generateImageHashFromBuffer, getHashedFilename } from '$utils/imageHash';
|
||||||
import { validateImageFile } from '$utils/imageValidation';
|
import { validateImageFile } from '$utils/imageValidation';
|
||||||
import { extractDominantColor } from '$utils/imageProcessing';
|
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 }) => {
|
export const POST = (async ({ request, locals }) => {
|
||||||
console.log('[API:ImgAdd] Image upload request received');
|
await requireGroup(locals, 'rezepte_users');
|
||||||
|
|
||||||
// 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');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|||||||
@@ -3,28 +3,35 @@ import type { RequestHandler } from '@sveltejs/kit';
|
|||||||
import { IMAGE_DIR } from '$env/static/private'
|
import { IMAGE_DIR } from '$env/static/private'
|
||||||
import { unlink } from 'node:fs';
|
import { unlink } from 'node:fs';
|
||||||
import { error } from '@sveltejs/kit';
|
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}) => {
|
export const POST = (async ({ request, locals}) => {
|
||||||
|
await requireGroup(locals, 'rezepte_users');
|
||||||
const data = await request.json();
|
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");
|
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 basename = data.name || hashedFilename.replace(/\.[a-f0-9]{8}\.webp$/, '').replace(/\.webp$/, '');
|
||||||
const unhashedFilename = basename + '.webp';
|
const unhashedFilename = basename + '.webp';
|
||||||
|
|
||||||
|
const recipeImgDir = path.join(IMAGE_DIR, "rezepte");
|
||||||
|
|
||||||
[ "full", "thumb"].forEach((folder) => {
|
[ "full", "thumb"].forEach((folder) => {
|
||||||
// Delete hashed version
|
const hashedPath = path.resolve(recipeImgDir, folder, hashedFilename);
|
||||||
unlink(path.join(IMAGE_DIR, "rezepte", folder, hashedFilename), (e) => {
|
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);
|
if(e) console.warn(`Could not delete hashed: ${folder}/${hashedFilename}`, e);
|
||||||
});
|
});
|
||||||
|
unlink(unhashedPath, (e) => {
|
||||||
// Delete unhashed version (for graceful degradation)
|
|
||||||
unlink(path.join(IMAGE_DIR, "rezepte", folder, unhashedFilename), (e) => {
|
|
||||||
if(e) console.warn(`Could not delete unhashed: ${folder}/${unhashedFilename}`, e);
|
if(e) console.warn(`Could not delete unhashed: ${folder}/${unhashedFilename}`, e);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,33 +4,40 @@ import { IMAGE_DIR } from '$env/static/private'
|
|||||||
import { rename } from 'node:fs';
|
import { rename } from 'node:fs';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { extractBasename, getHashedFilename } from '$utils/imageHash';
|
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}) => {
|
export const POST = (async ({ request, locals}) => {
|
||||||
|
await requireGroup(locals, 'rezepte_users');
|
||||||
const data = await request.json();
|
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 oldFilename = data.old_filename || (data.old_name + ".webp");
|
||||||
const hashMatch = oldFilename.match(/\.([a-f0-9]{8})\.webp$/);
|
const hashMatch = oldFilename.match(/\.([a-f0-9]{8})\.webp$/);
|
||||||
|
|
||||||
let newFilename: string;
|
let newFilename: string;
|
||||||
if (hashMatch) {
|
if (hashMatch) {
|
||||||
// Old filename has hash, preserve it
|
|
||||||
const hash = hashMatch[1];
|
const hash = hashMatch[1];
|
||||||
newFilename = getHashedFilename(data.new_name, hash);
|
newFilename = getHashedFilename(data.new_name, hash);
|
||||||
} else {
|
} else {
|
||||||
// Old filename has no hash (legacy), new one won't either
|
|
||||||
newFilename = data.new_name + ".webp";
|
newFilename = data.new_name + ".webp";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipeImgDir = path.join(IMAGE_DIR, "rezepte");
|
||||||
|
|
||||||
[ "full", "thumb"].forEach((folder) => {
|
[ "full", "thumb"].forEach((folder) => {
|
||||||
const old_path = path.join(IMAGE_DIR, "rezepte", folder, oldFilename)
|
const oldPath = path.resolve(recipeImgDir, folder, oldFilename);
|
||||||
rename(old_path, path.join(IMAGE_DIR, "rezepte", folder, newFilename), (e) => {
|
const newPath = path.resolve(recipeImgDir, folder, newFilename);
|
||||||
console.log(e)
|
assertWithinDir(recipeImgDir, oldPath);
|
||||||
if(e) throw error(500, "could not mv: " + old_path)
|
assertWithinDir(recipeImgDir, newPath);
|
||||||
|
|
||||||
|
rename(oldPath, newPath, (e) => {
|
||||||
|
if(e) console.warn(`could not mv: ${oldPath}`, e)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { json, type RequestHandler } from '@sveltejs/kit';
|
|||||||
import { Recipe } from '$models/Recipe';
|
import { Recipe } from '$models/Recipe';
|
||||||
import { dbConnect } from '$utils/db';
|
import { dbConnect } from '$utils/db';
|
||||||
import { generateNutritionMappings } from '$lib/server/nutritionMatcher';
|
import { generateNutritionMappings } from '$lib/server/nutritionMatcher';
|
||||||
|
import { requireGroup } from '$lib/server/middleware/auth';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ locals }) => {
|
export const POST: RequestHandler = async ({ locals }) => {
|
||||||
await locals.auth();
|
await requireGroup(locals, 'rezepte_users');
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
const recipes = await Recipe.find({}).lean();
|
const recipes = await Recipe.find({}).lean();
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { json, error } from '@sveltejs/kit';
|
import { json, error } from '@sveltejs/kit';
|
||||||
import { translationService } from '$lib/../utils/translation';
|
import { translationService } from '$lib/../utils/translation';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
import { requireGroup } from '$lib/server/middleware/auth';
|
||||||
|
|
||||||
/**
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
* POST /api/rezepte/translate
|
await requireGroup(locals, 'rezepte_users');
|
||||||
* 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 }) => {
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { recipe, fields, oldRecipe, existingTranslation } = body;
|
const { recipe, fields, oldRecipe, existingTranslation } = body;
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
// Find all share docs where current user is in the sharedWith list
|
// 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);
|
const owners = shares.map(s => s.owner);
|
||||||
|
|
||||||
if (owners.length === 0) return json({ shared: [] });
|
if (owners.length === 0) return json({ shared: [] });
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import type { RequestHandler } from './$types';
|
|||||||
import { generateAltText, type RecipeContext } from '$lib/server/ai/alttext.js';
|
import { generateAltText, type RecipeContext } from '$lib/server/ai/alttext.js';
|
||||||
import { checkOllamaHealth } from '$lib/server/ai/ollama.js';
|
import { checkOllamaHealth } from '$lib/server/ai/ollama.js';
|
||||||
import { Recipe } from '$models/Recipe.js';
|
import { Recipe } from '$models/Recipe.js';
|
||||||
|
import { requireGroup } from '$lib/server/middleware/auth';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
// Check authentication
|
await requireGroup(locals, 'rezepte_users');
|
||||||
const session = await locals.auth();
|
|
||||||
if (!session?.user) {
|
|
||||||
throw error(401, 'Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import type { RequestHandler } from './$types';
|
|||||||
import { generateAltText, type RecipeContext } from '$lib/server/ai/alttext.js';
|
import { generateAltText, type RecipeContext } from '$lib/server/ai/alttext.js';
|
||||||
import { checkOllamaHealth } from '$lib/server/ai/ollama.js';
|
import { checkOllamaHealth } from '$lib/server/ai/ollama.js';
|
||||||
import { Recipe } from '$models/Recipe.js';
|
import { Recipe } from '$models/Recipe.js';
|
||||||
|
import { requireGroup } from '$lib/server/middleware/auth';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
// Check authentication
|
await requireGroup(locals, 'rezepte_users');
|
||||||
const session = await locals.auth();
|
|
||||||
if (!session?.user) {
|
|
||||||
throw error(401, 'Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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<string, number>();
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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<string, number>();
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -2,10 +2,11 @@ import { json, error, type RequestHandler } from '@sveltejs/kit';
|
|||||||
import { dbConnect } from '$utils/db';
|
import { dbConnect } from '$utils/db';
|
||||||
import { NutritionOverwrite } from '$models/NutritionOverwrite';
|
import { NutritionOverwrite } from '$models/NutritionOverwrite';
|
||||||
import { invalidateOverwriteCache } from '$lib/server/nutritionMatcher';
|
import { invalidateOverwriteCache } from '$lib/server/nutritionMatcher';
|
||||||
|
import { requireAuth } from '$lib/server/middleware/auth';
|
||||||
|
|
||||||
/** GET: List all global nutrition overwrites */
|
/** GET: List all global nutrition overwrites */
|
||||||
export const GET: RequestHandler = async ({ locals }) => {
|
export const GET: RequestHandler = async ({ locals }) => {
|
||||||
await locals.auth();
|
await requireAuth(locals);
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
const overwrites = await NutritionOverwrite.find({}).sort({ ingredientNameDe: 1 }).lean();
|
const overwrites = await NutritionOverwrite.find({}).sort({ ingredientNameDe: 1 }).lean();
|
||||||
return json(overwrites);
|
return json(overwrites);
|
||||||
@@ -13,7 +14,7 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
|
|
||||||
/** POST: Create a new global nutrition overwrite */
|
/** POST: Create a new global nutrition overwrite */
|
||||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
await locals.auth();
|
await requireAuth(locals);
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@@ -43,7 +44,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
|
|
||||||
/** DELETE: Remove a global nutrition overwrite by ingredientNameDe */
|
/** DELETE: Remove a global nutrition overwrite by ingredientNameDe */
|
||||||
export const DELETE: RequestHandler = async ({ request, locals }) => {
|
export const DELETE: RequestHandler = async ({ request, locals }) => {
|
||||||
await locals.auth();
|
await requireAuth(locals);
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import { IMAGE_DIR } from '$env/static/private';
|
|||||||
import { extractDominantColor } from '$utils/imageProcessing';
|
import { extractDominantColor } from '$utils/imageProcessing';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { access, constants } from 'fs/promises';
|
import { access, constants } from 'fs/promises';
|
||||||
|
import { requireGroup } from '$lib/server/middleware/auth';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
const session = await locals.auth();
|
await requireGroup(locals, 'rezepte_users');
|
||||||
if (!session?.user) {
|
|
||||||
throw error(401, 'Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user