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>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
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
|
||
}
|