Files
planer/src/server/berechtigung.ts
T
Florian WildeandClaude Opus 5 126ad93301 test: Integrations-, Migrations- und E2E-Tests ergänzen
Der Integrationstest prüft gegen eine echte Datenbank, was Vitest nicht
sieht: Eindeutigkeit, Fremdschlüssel, Kaskaden und dass das Protokoll den
gelöschten Benutzer überlebt. Er läuft bewusst ohne umschließende
Transaktion — er prüft absichtlich Fälle, die scheitern müssen, und ein
gescheiterter Befehl bricht in PostgreSQL die ganze Transaktion ab.
Stattdessen tragen alle Testdaten dieselbe Kennung und werden am Ende
wieder entfernt.

Der Migrationstest legt eine Wegwerf-Datenbank an, spielt alle
Migrationen ein und vergleicht das Ergebnis mit dem Schema. Dass er
Abweichungen wirklich bemerkt, wurde mit einem probeweise ergänzten Feld
nachgewiesen.

Der E2E-Rauchtest deckt den Weg ab, der in M1 trägt: Umleitung ohne
Anmeldung samt gemerktem Ziel, Abweisung falscher Zugangsdaten,
erfolgreiche Anmeldung und die Verwaltungsseite.

Dafür neu: eine Middleware, die nur den angefragten Pfad als Kopfzeile
durchreicht. Ein Layout kennt seinen Pfad nicht, braucht ihn aber für die
Rückkehr nach der Anmeldung. Anmeldelogik steht dort bewusst nicht — die
Middleware läuft im Edge-Umfeld ohne Prisma.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 13:10:54 +02:00

87 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'server-only'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import type { Session } from 'next-auth'
import type { Rolle } from '@/generated/prisma/enums'
import { rolleReichtAus } from '@/lib/rollen'
import { auth } from '@/server/auth'
import { PFAD_KOPFZEILE } from '@/middleware'
/**
* Rollenpruefung.
*
* Jede schreibende Aktion prueft serverseitig – in der Seite und noch einmal
* in der Server Action. Die Oberflaeche blendet Knoepfe nur zusaetzlich aus;
* verlassen wird sich darauf nie.
*/
export type Angemeldet = Session['user']
/** Fehler, den Server Actions dem Aufrufer zurueckgeben. */
export class NichtErlaubt extends Error {
constructor(nachricht = 'Dazu fehlt dir die Berechtigung.') {
super(nachricht)
this.name = 'NichtErlaubt'
}
}
/** Die angemeldete Person, oder null. */
export async function angemeldet(): Promise<Angemeldet | null> {
const sitzung = await auth()
return sitzung?.user?.id ? sitzung.user : null
}
/**
* Riegel fuer Seiten: ohne Anmeldung geht es zur Anmeldeseite zurueck,
* mit Ziel, damit es danach dort weitergeht, wo man hinwollte.
*/
export async function verlangeAnmeldung(zielPfad?: string): Promise<Angemeldet> {
const benutzer = await angemeldet()
if (!benutzer) {
const ziel = zielPfad ? `?weiter=${encodeURIComponent(zielPfad)}` : ''
redirect(`/anmelden${ziel}`)
}
return benutzer
}
/** Riegel fuer Seiten, die eine bestimmte Rolle verlangen. */
export async function verlangeRolle(
verlangt: Rolle,
zielPfad?: string,
): Promise<Angemeldet> {
const benutzer = await verlangeAnmeldung(zielPfad)
if (!rolleReichtAus(benutzer.rolle, verlangt)) {
redirect('/keine-berechtigung')
}
return benutzer
}
/**
* Riegel fuer Server Actions. Hier wird nicht umgeleitet, sondern geworfen –
* der Aufrufer wandelt das in eine Meldung fuer die Oberflaeche.
*/
export async function pruefeRolle(verlangt: Rolle): Promise<Angemeldet> {
const benutzer = await angemeldet()
if (!benutzer) throw new NichtErlaubt('Bitte zuerst anmelden.')
if (!rolleReichtAus(benutzer.rolle, verlangt)) throw new NichtErlaubt()
return benutzer
}
/**
* Riegel fuer Route Handler: gibt null zurueck, statt umzuleiten. Der
* Aufrufer antwortet dann mit 401.
*/
export async function verlangeAnmeldungFuerApi(): Promise<Angemeldet | null> {
return angemeldet()
}
/**
* Der angefragte Pfad, den die Middleware durchgereicht hat. Wird gebraucht,
* um nach der Anmeldung wieder dorthin zurueckzufuehren.
*/
export async function angefragterPfad(): Promise<string | undefined> {
const kopfzeilen = await headers()
const pfad = kopfzeilen.get(PFAD_KOPFZEILE)
return pfad && pfad.startsWith('/') ? pfad : undefined
}