security: enforce auth on all API write endpoints, remove mario-kart
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:
2026-04-07 20:10:48 +02:00
parent 8e4ba896e1
commit 2dce83de55
24 changed files with 119 additions and 1211 deletions

View File

@@ -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": {

View File

@@ -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.

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();

View File

@@ -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,
});
}
}; };

View File

@@ -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();

View File

@@ -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);
}); });
}) })

View File

@@ -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)
}) })
}); });

View File

@@ -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();

View File

@@ -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;

View File

@@ -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: [] });

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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();

View File

@@ -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();