From fa4bfb8e5c41d38ddb401e3a334af48e3c073f2c Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 20 Mar 2026 09:47:52 +0100 Subject: [PATCH] feat(routes): add server-side WRITE_ALL guard on write-only routes Block direct URL navigation to /persons/new, /documents/new, /documents/:id/edit for users without WRITE_ALL permission. E2E tests verify admin user retains access to all write routes. Closes #17 Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/permissions.spec.ts | 31 +++++++++++++++++++ .../documents/[id]/edit/+page.server.ts | 5 ++- .../src/routes/documents/new/+page.server.ts | 7 +++-- .../src/routes/persons/new/+page.server.ts | 7 ++++- 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 frontend/e2e/permissions.spec.ts diff --git a/frontend/e2e/permissions.spec.ts b/frontend/e2e/permissions.spec.ts new file mode 100644 index 00000000..a7416856 --- /dev/null +++ b/frontend/e2e/permissions.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Write permissions — admin user', () => { + test('admin user sees Neues Dokument link on home page', async ({ page }) => { + await page.goto('/'); + await expect(page.getByRole('link', { name: /Neues Dokument/i })).toBeVisible(); + }); + + test('admin user sees Neue Person link on persons page', async ({ page }) => { + await page.goto('/persons'); + await expect(page.getByRole('link', { name: /Neue Person/i })).toBeVisible(); + }); + + test('admin user can navigate to /persons/new', async ({ page }) => { + await page.goto('/persons/new'); + await expect(page).toHaveURL('/persons/new'); + await expect(page.getByLabel('Vorname')).toBeVisible(); + }); + + test('admin user can navigate to /documents/new', async ({ page }) => { + await page.goto('/documents/new'); + await expect(page).toHaveURL('/documents/new'); + }); + + test('admin user sees edit button on person detail page', async ({ page }) => { + await page.goto('/persons'); + const firstPerson = page.locator('a[href^="/persons/"]:not([href="/persons/new"])').first(); + await firstPerson.click(); + await expect(page.getByRole('button', { name: /Bearbeiten/i })).toBeVisible(); + }); +}); diff --git a/frontend/src/routes/documents/[id]/edit/+page.server.ts b/frontend/src/routes/documents/[id]/edit/+page.server.ts index 24f5af7f..01ebac5d 100644 --- a/frontend/src/routes/documents/[id]/edit/+page.server.ts +++ b/frontend/src/routes/documents/[id]/edit/+page.server.ts @@ -3,7 +3,10 @@ import { env } from '$env/dynamic/private'; import { createApiClient } from '$lib/api.server'; import { parseBackendError, getErrorMessage } from '$lib/errors'; -export async function load({ params, fetch }) { +export async function load({ params, fetch, locals }: { params: { id: string }; fetch: typeof globalThis.fetch; locals: App.Locals }) { + const canWrite = locals.user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('WRITE_ALL')) ?? false; + if (!canWrite) throw error(403, 'Forbidden'); + const { id } = params; const api = createApiClient(fetch); diff --git a/frontend/src/routes/documents/new/+page.server.ts b/frontend/src/routes/documents/new/+page.server.ts index 75b36e32..ca8e7498 100644 --- a/frontend/src/routes/documents/new/+page.server.ts +++ b/frontend/src/routes/documents/new/+page.server.ts @@ -1,9 +1,12 @@ -import { fail, redirect } from '@sveltejs/kit'; +import { error, fail, redirect } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; import { createApiClient } from '$lib/api.server'; import { parseBackendError, getErrorMessage } from '$lib/errors'; -export async function load({ fetch }) { +export async function load({ fetch, locals }: { fetch: typeof globalThis.fetch; locals: App.Locals }) { + const canWrite = locals.user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('WRITE_ALL')) ?? false; + if (!canWrite) throw error(403, 'Forbidden'); + const api = createApiClient(fetch); const personsResult = await api.GET('/api/persons'); diff --git a/frontend/src/routes/persons/new/+page.server.ts b/frontend/src/routes/persons/new/+page.server.ts index d58d969c..79f61b5d 100644 --- a/frontend/src/routes/persons/new/+page.server.ts +++ b/frontend/src/routes/persons/new/+page.server.ts @@ -1,6 +1,11 @@ -import { fail, redirect } from '@sveltejs/kit'; +import { error, fail, redirect } from '@sveltejs/kit'; import { createApiClient } from '$lib/api.server'; +export async function load({ locals }: { locals: App.Locals }) { + const canWrite = locals.user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('WRITE_ALL')) ?? false; + if (!canWrite) throw error(403, 'Forbidden'); +} + export const actions = { default: async ({ request, fetch }) => { const formData = await request.formData();