diff --git a/src/routes/api/domains/+server.ts b/src/routes/api/domains/+server.ts index 7bd3b7d..7a0bf9b 100644 --- a/src/routes/api/domains/+server.ts +++ b/src/routes/api/domains/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json, error, isHttpError } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { validateBody } from '$lib/server/api-helpers'; import { addDomain, listDomains, setDomainFavicon } from '$lib/server/domains/repository'; import { ensureFavicons, fetchAndStoreFavicon } from '$lib/server/domains/favicons'; @@ -21,16 +22,14 @@ export const GET: RequestHandler = async () => { }; export const POST: RequestHandler = async ({ request }) => { - const body = await request.json().catch(() => null); - const parsed = CreateSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); + const data = validateBody(await request.json().catch(() => null), CreateSchema); try { const db = getDb(); const d = addDomain( db, - parsed.data.domain, - parsed.data.display_name ?? null, - parsed.data.added_by_profile_id ?? null + data.domain, + data.display_name ?? null, + data.added_by_profile_id ?? null ); // Favicon direkt nach dem Insert mitziehen, damit die Antwort schon das // Icon enthält — der POST ist eh ein interaktiver Admin-Vorgang. @@ -41,6 +40,7 @@ export const POST: RequestHandler = async ({ request }) => { } return json(d, { status: 201 }); } catch (e) { + if (isHttpError(e)) throw e; error(409, { message: (e as Error).message }); } }; diff --git a/src/routes/api/domains/[id]/+server.ts b/src/routes/api/domains/[id]/+server.ts index 01da6a3..0c22f9f 100644 --- a/src/routes/api/domains/[id]/+server.ts +++ b/src/routes/api/domains/[id]/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json, error, isHttpError } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { removeDomain, updateDomain, @@ -16,20 +17,12 @@ const UpdateSchema = z.object({ display_name: z.string().max(100).nullable().optional() }); -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const PATCH: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = UpdateSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), UpdateSchema); try { const db = getDb(); - const updated = updateDomain(db, id, parsed.data); + const updated = updateDomain(db, id, data); if (!updated) error(404, { message: 'Not found' }); // Wenn updateDomain favicon_path genullt hat (Domain geändert), frisch laden. if (updated.favicon_path === null) { @@ -41,12 +34,14 @@ export const PATCH: RequestHandler = async ({ params, request }) => { } return json(updated); } catch (e) { + // HTTP-Errors aus error() durchreichen, sonst landet ein 404 als 409. + if (isHttpError(e)) throw e; error(409, { message: (e as Error).message }); } }; export const DELETE: RequestHandler = async ({ params }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); removeDomain(getDb(), id); return json({ ok: true }); }; diff --git a/src/routes/api/profiles/+server.ts b/src/routes/api/profiles/+server.ts index 8ec51da..38c0734 100644 --- a/src/routes/api/profiles/+server.ts +++ b/src/routes/api/profiles/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json, error, isHttpError } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { validateBody } from '$lib/server/api-helpers'; import { createProfile, listProfiles } from '$lib/server/profiles/repository'; const CreateSchema = z.object({ @@ -14,15 +15,12 @@ export const GET: RequestHandler = async () => { }; export const POST: RequestHandler = async ({ request }) => { - const body = await request.json().catch(() => null); - const parsed = CreateSchema.safeParse(body); - if (!parsed.success) { - error(400, { message: 'Invalid body', issues: parsed.error.issues }); - } + const data = validateBody(await request.json().catch(() => null), CreateSchema); try { - const p = createProfile(getDb(), parsed.data.name, parsed.data.avatar_emoji ?? null); + const p = createProfile(getDb(), data.name, data.avatar_emoji ?? null); return json(p, { status: 201 }); } catch (e) { + if (isHttpError(e)) throw e; error(409, { message: (e as Error).message }); } }; diff --git a/src/routes/api/profiles/[id]/+server.ts b/src/routes/api/profiles/[id]/+server.ts index 289ef26..d1d3d73 100644 --- a/src/routes/api/profiles/[id]/+server.ts +++ b/src/routes/api/profiles/[id]/+server.ts @@ -1,28 +1,21 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { deleteProfile, renameProfile } from '$lib/server/profiles/repository'; const RenameSchema = z.object({ name: z.string().min(1).max(50) }); -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const PATCH: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = RenameSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); - renameProfile(getDb(), id, parsed.data.name); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), RenameSchema); + renameProfile(getDb(), id, data.name); return json({ ok: true }); }; export const DELETE: RequestHandler = async ({ params }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); deleteProfile(getDb(), id); return json({ ok: true }); }; diff --git a/src/routes/api/recipes/[id]/+server.ts b/src/routes/api/recipes/[id]/+server.ts index efc43be..5448d9d 100644 --- a/src/routes/api/recipes/[id]/+server.ts +++ b/src/routes/api/recipes/[id]/+server.ts @@ -2,6 +2,7 @@ import type { RequestHandler } from './$types'; import { json, error } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { deleteRecipe, getRecipeById, @@ -48,14 +49,8 @@ const PatchSchema = z }) .refine((v) => Object.keys(v).length > 0, { message: 'Empty patch' }); -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const GET: RequestHandler = async ({ params }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); const db = getDb(); const recipe = getRecipeById(db, id); if (!recipe) error(404, { message: 'Recipe not found' }); @@ -68,12 +63,10 @@ export const GET: RequestHandler = async ({ params }) => { }; export const PATCH: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); const body = await request.json().catch(() => null); - const parsed = PatchSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); + const p = validateBody(body, PatchSchema); const db = getDb(); - const p = parsed.data; // Spezielle Kurz-Updates (bleiben als Sonderfall, weil sie FTS triggern // bzw. andere Tabellen mitpflegen). if (p.title !== undefined && Object.keys(p).length === 1) { @@ -121,7 +114,7 @@ export const PATCH: RequestHandler = async ({ params, request }) => { }; export const DELETE: RequestHandler = async ({ params }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); deleteRecipe(getDb(), id); return json({ ok: true }); }; diff --git a/src/routes/api/recipes/[id]/comments/+server.ts b/src/routes/api/recipes/[id]/comments/+server.ts index 4814240..5a5bfd0 100644 --- a/src/routes/api/recipes/[id]/comments/+server.ts +++ b/src/routes/api/recipes/[id]/comments/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { addComment, deleteComment, listComments } from '$lib/server/recipes/actions'; const Schema = z.object({ @@ -11,30 +12,20 @@ const Schema = z.object({ const DeleteSchema = z.object({ comment_id: z.number().int().positive() }); -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const GET: RequestHandler = async ({ params }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); return json(listComments(getDb(), id)); }; export const POST: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = Schema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); - const cid = addComment(getDb(), id, parsed.data.profile_id, parsed.data.text); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), Schema); + const cid = addComment(getDb(), id, data.profile_id, data.text); return json({ id: cid }, { status: 201 }); }; export const DELETE: RequestHandler = async ({ request }) => { - const body = await request.json().catch(() => null); - const parsed = DeleteSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); - deleteComment(getDb(), parsed.data.comment_id); + const data = validateBody(await request.json().catch(() => null), DeleteSchema); + deleteComment(getDb(), data.comment_id); return json({ ok: true }); }; diff --git a/src/routes/api/recipes/[id]/cooked/+server.ts b/src/routes/api/recipes/[id]/cooked/+server.ts index e1965c9..bf273db 100644 --- a/src/routes/api/recipes/[id]/cooked/+server.ts +++ b/src/routes/api/recipes/[id]/cooked/+server.ts @@ -1,25 +1,18 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { logCooked } from '$lib/server/recipes/actions'; import { removeFromWishlistForAll } from '$lib/server/wishlist/repository'; const Schema = z.object({ profile_id: z.number().int().positive() }); -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const POST: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = Schema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), Schema); const db = getDb(); - const entry = logCooked(db, id, parsed.data.profile_id); + const entry = logCooked(db, id, data.profile_id); // Wenn das Rezept heute gekocht wurde, ist der Wunsch erfüllt — für alle // Profile raus aus der Wunschliste. Client nutzt den removed_from_wishlist- // Flag, um den lokalen State (Badge, Button) ohne Reload zu aktualisieren. diff --git a/src/routes/api/recipes/[id]/favorite/+server.ts b/src/routes/api/recipes/[id]/favorite/+server.ts index 5c82f09..f2bd02c 100644 --- a/src/routes/api/recipes/[id]/favorite/+server.ts +++ b/src/routes/api/recipes/[id]/favorite/+server.ts @@ -1,31 +1,22 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { addFavorite, removeFavorite } from '$lib/server/recipes/actions'; const Schema = z.object({ profile_id: z.number().int().positive() }); -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const PUT: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = Schema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); - addFavorite(getDb(), id, parsed.data.profile_id); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), Schema); + addFavorite(getDb(), id, data.profile_id); return json({ ok: true }); }; export const DELETE: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = Schema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); - removeFavorite(getDb(), id, parsed.data.profile_id); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), Schema); + removeFavorite(getDb(), id, data.profile_id); return json({ ok: true }); }; diff --git a/src/routes/api/recipes/[id]/image/+server.ts b/src/routes/api/recipes/[id]/image/+server.ts index 4927dd9..e19f3b4 100644 --- a/src/routes/api/recipes/[id]/image/+server.ts +++ b/src/routes/api/recipes/[id]/image/+server.ts @@ -5,6 +5,7 @@ import { existsSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam } from '$lib/server/api-helpers'; import { getRecipeById, updateImagePath } from '$lib/server/recipes/repository'; const IMAGE_DIR = process.env.IMAGE_DIR ?? './data/images'; @@ -19,14 +20,8 @@ const EXT_BY_MIME: Record = { 'image/avif': '.avif' }; -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const POST: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); const db = getDb(); if (!getRecipeById(db, id)) error(404, { message: 'Recipe not found' }); @@ -53,7 +48,7 @@ export const POST: RequestHandler = async ({ params, request }) => { }; export const DELETE: RequestHandler = ({ params }) => { - const id = parseId(params.id!); + const id = parsePositiveIntParam(params.id, 'id'); const db = getDb(); if (!getRecipeById(db, id)) error(404, { message: 'Recipe not found' }); updateImagePath(db, id, null); diff --git a/src/routes/api/recipes/[id]/rating/+server.ts b/src/routes/api/recipes/[id]/rating/+server.ts index 0ea926a..1a2cfd3 100644 --- a/src/routes/api/recipes/[id]/rating/+server.ts +++ b/src/routes/api/recipes/[id]/rating/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { clearRating, setRating } from '$lib/server/recipes/actions'; const Schema = z.object({ @@ -11,26 +12,16 @@ const Schema = z.object({ const DeleteSchema = z.object({ profile_id: z.number().int().positive() }); -function parseId(raw: string): number { - const id = Number(raw); - if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); - return id; -} - export const PUT: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = Schema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); - setRating(getDb(), id, parsed.data.profile_id, parsed.data.stars); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), Schema); + setRating(getDb(), id, data.profile_id, data.stars); return json({ ok: true }); }; export const DELETE: RequestHandler = async ({ params, request }) => { - const id = parseId(params.id!); - const body = await request.json().catch(() => null); - const parsed = DeleteSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); - clearRating(getDb(), id, parsed.data.profile_id); + const id = parsePositiveIntParam(params.id, 'id'); + const data = validateBody(await request.json().catch(() => null), DeleteSchema); + clearRating(getDb(), id, data.profile_id); return json({ ok: true }); }; diff --git a/src/routes/api/recipes/import/+server.ts b/src/routes/api/recipes/import/+server.ts index 429e6fd..6b89a2d 100644 --- a/src/routes/api/recipes/import/+server.ts +++ b/src/routes/api/recipes/import/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { validateBody } from '$lib/server/api-helpers'; import { importRecipe } from '$lib/server/recipes/importer'; import { mapImporterError } from '$lib/server/errors'; @@ -10,11 +11,9 @@ const ImportSchema = z.object({ url: z.string().url() }); const IMAGE_DIR = process.env.IMAGE_DIR ?? './data/images'; export const POST: RequestHandler = async ({ request }) => { - const body = await request.json().catch(() => null); - const parsed = ImportSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'Invalid body' }); + const data = validateBody(await request.json().catch(() => null), ImportSchema); try { - const result = await importRecipe(getDb(), IMAGE_DIR, parsed.data.url); + const result = await importRecipe(getDb(), IMAGE_DIR, data.url); return json({ id: result.id, duplicate: result.duplicate }); } catch (e) { mapImporterError(e); diff --git a/src/routes/api/wishlist/+server.ts b/src/routes/api/wishlist/+server.ts index 871cc90..fac3751 100644 --- a/src/routes/api/wishlist/+server.ts +++ b/src/routes/api/wishlist/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; +import { validateBody } from '$lib/server/api-helpers'; import { addToWishlist, listWishlist, @@ -32,9 +33,7 @@ export const GET: RequestHandler = async ({ url }) => { }; export const POST: RequestHandler = async ({ request }) => { - const body = await request.json().catch(() => null); - const parsed = AddSchema.safeParse(body); - if (!parsed.success) error(400, { message: 'recipe_id and profile_id required' }); - addToWishlist(getDb(), parsed.data.recipe_id, parsed.data.profile_id); + const data = validateBody(await request.json().catch(() => null), AddSchema); + addToWishlist(getDb(), data.recipe_id, data.profile_id); return json({ ok: true }, { status: 201 }); }; diff --git a/src/routes/api/wishlist/[recipe_id]/+server.ts b/src/routes/api/wishlist/[recipe_id]/+server.ts index f4c2fba..339b155 100644 --- a/src/routes/api/wishlist/[recipe_id]/+server.ts +++ b/src/routes/api/wishlist/[recipe_id]/+server.ts @@ -1,26 +1,21 @@ import type { RequestHandler } from './$types'; -import { json, error } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; import { getDb } from '$lib/server/db'; +import { parsePositiveIntParam } from '$lib/server/api-helpers'; import { removeFromWishlist, removeFromWishlistForAll } from '$lib/server/wishlist/repository'; -function parsePositiveInt(raw: string | null, field: string): number { - const n = raw === null ? NaN : Number(raw); - if (!Number.isInteger(n) || n <= 0) error(400, { message: `Invalid ${field}` }); - return n; -} - // DELETE /api/wishlist/:id?profile_id=X → entfernt nur den eigenen Wunsch // DELETE /api/wishlist/:id?all=true → entfernt für ALLE Profile export const DELETE: RequestHandler = async ({ params, url }) => { - const id = parsePositiveInt(params.recipe_id!, 'recipe_id'); + const id = parsePositiveIntParam(params.recipe_id, 'recipe_id'); const db = getDb(); if (url.searchParams.get('all') === 'true') { removeFromWishlistForAll(db, id); } else { - const profileId = parsePositiveInt(url.searchParams.get('profile_id'), 'profile_id'); + const profileId = parsePositiveIntParam(url.searchParams.get('profile_id'), 'profile_id'); removeFromWishlist(db, id, profileId); } return json({ ok: true });