refactor(timeline): extract requireWriteAll route guard

Both curator-event loaders repeated the same null-user + hasWriteAll block.
hasWriteAll already returns false for an anonymous user, so a single
requireWriteAll(locals) helper covers both REQ-002 (null user → 403) and
REQ-003 (no WRITE_ALL → 403) without the redundant pre-check.

Addresses PR #832 review (#781).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-13 23:21:37 +02:00
parent 068c2ef256
commit 9f17c4538f
3 changed files with 17 additions and 15 deletions

View File

@@ -1,3 +1,5 @@
import { error } from '@sveltejs/kit';
/** /**
* Server-side permission predicates derived from the authenticated user in `locals`. * Server-side permission predicates derived from the authenticated user in `locals`.
* *
@@ -12,3 +14,13 @@ type PermissionLocals = {
export function hasWriteAll(locals: PermissionLocals): boolean { export function hasWriteAll(locals: PermissionLocals): boolean {
return locals.user?.groups?.some((group) => group.permissions.includes('WRITE_ALL')) ?? false; 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');
}

View File

@@ -1,7 +1,7 @@
import { error, fail, redirect } from '@sveltejs/kit'; import { error, fail, redirect } from '@sveltejs/kit';
import { createApiClient, extractErrorCode } from '$lib/shared/api.server'; import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
import { getErrorMessage } from '$lib/shared/errors'; import { getErrorMessage } from '$lib/shared/errors';
import { hasWriteAll } from '$lib/shared/server/permissions'; import { requireWriteAll } from '$lib/shared/server/permissions';
import { import {
parseEventForm, parseEventForm,
validateEventForm, validateEventForm,
@@ -20,12 +20,7 @@ export async function load({
url: URL; url: URL;
fetch: typeof globalThis.fetch; fetch: typeof globalThis.fetch;
}) { }) {
// Null-user guard first — avoids a TypeError on locals.user.groups for an requireWriteAll(locals);
// 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');
const api = createApiClient(fetch); const api = createApiClient(fetch);
const result = await api.GET('/api/timeline/events/{id}', { const result = await api.GET('/api/timeline/events/{id}', {

View File

@@ -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 { createApiClient, extractErrorCode } from '$lib/shared/api.server';
import { getErrorMessage } from '$lib/shared/errors'; import { getErrorMessage } from '$lib/shared/errors';
import { hasWriteAll } from '$lib/shared/server/permissions'; import { requireWriteAll } from '$lib/shared/server/permissions';
import { import {
parseEventForm, parseEventForm,
validateEventForm, validateEventForm,
@@ -20,12 +20,7 @@ export async function load({
url: URL; url: URL;
fetch: typeof globalThis.fetch; fetch: typeof globalThis.fetch;
}) { }) {
// Null-user guard first — avoids a TypeError on locals.user.groups for an requireWriteAll(locals);
// 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');
const api = createApiClient(fetch); const api = createApiClient(fetch);
const personId = url.searchParams.get('personId'); const personId = url.searchParams.get('personId');