diff --git a/frontend/src/lib/shared/server/permissions.ts b/frontend/src/lib/shared/server/permissions.ts index 11053859..bdd5d36c 100644 --- a/frontend/src/lib/shared/server/permissions.ts +++ b/frontend/src/lib/shared/server/permissions.ts @@ -1,3 +1,5 @@ +import { error } from '@sveltejs/kit'; + /** * Server-side permission predicates derived from the authenticated user in `locals`. * @@ -12,3 +14,13 @@ type PermissionLocals = { export function hasWriteAll(locals: PermissionLocals): boolean { return locals.user?.groups?.some((group) => group.permissions.includes('WRITE_ALL')) ?? false; } + +/** + * Throws a 403 unless the user holds WRITE_ALL. Anonymous users are rejected too + * — `hasWriteAll` returns false for a null user, so a single check covers both + * the unauthenticated and the under-privileged case. Server-side gate; the + * frontend canWrite flag only hides entry-point buttons. + */ +export function requireWriteAll(locals: PermissionLocals): void { + if (!hasWriteAll(locals)) throw error(403, 'Forbidden'); +} diff --git a/frontend/src/routes/zeitstrahl/events/[id]/edit/+page.server.ts b/frontend/src/routes/zeitstrahl/events/[id]/edit/+page.server.ts index 20bad3b7..fd1f5931 100644 --- a/frontend/src/routes/zeitstrahl/events/[id]/edit/+page.server.ts +++ b/frontend/src/routes/zeitstrahl/events/[id]/edit/+page.server.ts @@ -1,7 +1,7 @@ import { error, fail, redirect } from '@sveltejs/kit'; import { createApiClient, extractErrorCode } from '$lib/shared/api.server'; import { getErrorMessage } from '$lib/shared/errors'; -import { hasWriteAll } from '$lib/shared/server/permissions'; +import { requireWriteAll } from '$lib/shared/server/permissions'; import { parseEventForm, validateEventForm, @@ -20,12 +20,7 @@ export async function load({ url: URL; fetch: typeof globalThis.fetch; }) { - // Null-user guard first — avoids a TypeError on locals.user.groups for an - // unauthenticated request that reaches the route. - if (!locals.user) throw error(403, 'Forbidden'); - // WRITE_ALL check mirrors Permission.WRITE_ALL — server-side gate; frontend - // canWrite flag is for hiding entry-point buttons only. - if (!hasWriteAll(locals)) throw error(403, 'Forbidden'); + requireWriteAll(locals); const api = createApiClient(fetch); const result = await api.GET('/api/timeline/events/{id}', { diff --git a/frontend/src/routes/zeitstrahl/events/new/+page.server.ts b/frontend/src/routes/zeitstrahl/events/new/+page.server.ts index 68d68665..28f14a45 100644 --- a/frontend/src/routes/zeitstrahl/events/new/+page.server.ts +++ b/frontend/src/routes/zeitstrahl/events/new/+page.server.ts @@ -1,7 +1,7 @@ -import { error, fail, redirect } from '@sveltejs/kit'; +import { fail, redirect } from '@sveltejs/kit'; import { createApiClient, extractErrorCode } from '$lib/shared/api.server'; import { getErrorMessage } from '$lib/shared/errors'; -import { hasWriteAll } from '$lib/shared/server/permissions'; +import { requireWriteAll } from '$lib/shared/server/permissions'; import { parseEventForm, validateEventForm, @@ -20,12 +20,7 @@ export async function load({ url: URL; fetch: typeof globalThis.fetch; }) { - // Null-user guard first — avoids a TypeError on locals.user.groups for an - // unauthenticated request that reaches the route. - if (!locals.user) throw error(403, 'Forbidden'); - // WRITE_ALL check mirrors Permission.WRITE_ALL — server-side gate; frontend - // canWrite flag is for hiding entry-point buttons only. - if (!hasWriteAll(locals)) throw error(403, 'Forbidden'); + requireWriteAll(locals); const api = createApiClient(fetch); const personId = url.searchParams.get('personId');