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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
24a799d6ba
commit
126ad93301
@@ -0,0 +1,79 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Rauchtest der Anmeldung.
|
||||
*
|
||||
* Prüft den Weg, der in M1 wirklich trägt: Wer nicht angemeldet ist, kommt
|
||||
* nirgends hin; mit richtigem Passwort schon; und die Verwaltungsseite bleibt
|
||||
* verschlossen, wenn die Rolle nicht reicht.
|
||||
*
|
||||
* Voraussetzung: "npm run db:seed" ist gelaufen und ADMIN_EMAIL sowie
|
||||
* ADMIN_PASSWORT sind gesetzt.
|
||||
*/
|
||||
|
||||
const EMAIL = process.env.ADMIN_EMAIL ?? ''
|
||||
const PASSWORT = process.env.ADMIN_PASSWORT ?? ''
|
||||
|
||||
test.describe('Anmeldung', () => {
|
||||
test.skip(
|
||||
!EMAIL || !PASSWORT,
|
||||
'ADMIN_EMAIL und ADMIN_PASSWORT sind nicht gesetzt – ohne sie gibt es kein Testkonto.',
|
||||
)
|
||||
|
||||
test('leitet nicht Angemeldete zur Anmeldeseite', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page).toHaveURL(/\/anmelden/)
|
||||
await expect(page.getByRole('heading', { name: 'Planer' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('merkt sich das Ziel und kehrt nach der Anmeldung dorthin zurück', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/bereiche')
|
||||
await expect(page).toHaveURL(/\/anmelden\?weiter=%2Fbereiche/)
|
||||
})
|
||||
|
||||
test('weist falsche Zugangsdaten ab, ohne zu verraten warum', async ({ page }) => {
|
||||
await page.goto('/anmelden')
|
||||
await page.getByLabel('E-Mail-Adresse').fill(EMAIL)
|
||||
await page.getByLabel('Passwort').fill('ganz-sicher-falsch-123')
|
||||
await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click()
|
||||
|
||||
await expect(page.getByText('Anmeldung nicht möglich')).toBeVisible()
|
||||
await expect(page).toHaveURL(/\/anmelden/)
|
||||
})
|
||||
|
||||
test('lässt mit richtigem Passwort hinein und wieder hinaus', async ({ page }) => {
|
||||
await page.goto('/anmelden')
|
||||
await page.getByLabel('E-Mail-Adresse').fill(EMAIL)
|
||||
await page.getByLabel('Passwort').fill(PASSWORT)
|
||||
await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click()
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Guten Tag/ })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: 'Bereiche' })).toBeVisible()
|
||||
|
||||
await page.getByTitle('Abmelden').click()
|
||||
await expect(page).toHaveURL(/\/anmelden/)
|
||||
})
|
||||
|
||||
test('zeigt der Verwaltung das Protokoll', async ({ page }) => {
|
||||
await page.goto('/anmelden')
|
||||
await page.getByLabel('E-Mail-Adresse').fill(EMAIL)
|
||||
await page.getByLabel('Passwort').fill(PASSWORT)
|
||||
await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click()
|
||||
await expect(page.getByRole('heading', { name: /Guten Tag/ })).toBeVisible()
|
||||
|
||||
await page.getByRole('link', { name: 'Protokoll' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Protokoll' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test('der Health-Endpunkt antwortet ohne Anmeldung', async ({ request }) => {
|
||||
const antwort = await request.get('/api/health')
|
||||
expect(antwort.ok()).toBe(true)
|
||||
|
||||
const inhalt = (await antwort.json()) as Record<string, unknown>
|
||||
expect(inhalt.status).toBe('ok')
|
||||
expect(inhalt.datenbank).toBe('ok')
|
||||
expect(typeof inhalt.version).toBe('string')
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import 'dotenv/config'
|
||||
|
||||
const PORT = Number(process.env.E2E_PORT ?? 3100)
|
||||
const BASIS = `http://127.0.0.1:${PORT}`
|
||||
|
||||
/**
|
||||
* E2E-Tests gegen eine echte, gebaute Anwendung.
|
||||
*
|
||||
* Der Testserver läuft auf einem eigenen Port, damit er einer laufenden
|
||||
* Entwicklungsinstanz nicht in die Quere kommt. Die Datenbank ist dieselbe wie
|
||||
* im Entwicklungsbetrieb – die Tests legen nur Daten mit eigenem Präfix an und
|
||||
* räumen hinter sich auf.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: false,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: 1,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
timeout: 30_000,
|
||||
|
||||
use: {
|
||||
baseURL: BASIS,
|
||||
locale: 'de-DE',
|
||||
timezoneId: 'Europe/Berlin',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: `${BASIS}/api/health`,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
env: {
|
||||
PORT: String(PORT),
|
||||
APP_URL: BASIS,
|
||||
WEBAUTHN_ORIGIN: BASIS,
|
||||
WEBAUTHN_RP_ID: '127.0.0.1',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Integrationstest gegen eine echte Datenbank.
|
||||
*
|
||||
* Prüft, was Vitest nicht abdeckt: Eindeutigkeit, Fremdschlüssel, Kaskaden
|
||||
* und dass das Audit-Protokoll den Benutzer überlebt.
|
||||
*
|
||||
* Bewusst ohne umschließende Transaktion: Der Test 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 — auch wenn der Test unterwegs abbricht.
|
||||
*
|
||||
* Aufruf: npm run test:integration
|
||||
*/
|
||||
import 'dotenv/config'
|
||||
import { PrismaPg } from '@prisma/adapter-pg'
|
||||
import { PrismaClient } from '../src/generated/prisma/client.js'
|
||||
|
||||
const verbindung = process.env.DATABASE_URL
|
||||
if (!verbindung) {
|
||||
console.error('DATABASE_URL ist nicht gesetzt.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) })
|
||||
|
||||
type Pruefung = { name: string; ok: boolean; hinweis?: string }
|
||||
const ergebnisse: Pruefung[] = []
|
||||
|
||||
function pruefe(name: string, bedingung: boolean, hinweis?: string): void {
|
||||
ergebnisse.push(hinweis ? { name, ok: bedingung, hinweis } : { name, ok: bedingung })
|
||||
}
|
||||
|
||||
/** Erwartet, dass die Aktion scheitert. */
|
||||
async function scheitert(name: string, tun: () => Promise<unknown>): Promise<void> {
|
||||
try {
|
||||
await tun()
|
||||
pruefe(name, false, 'Die Aktion war erlaubt, obwohl sie scheitern sollte.')
|
||||
} catch {
|
||||
pruefe(name, true)
|
||||
}
|
||||
}
|
||||
|
||||
const kennung = `zzz-test-${Date.now()}`
|
||||
const testEmail = `${kennung}@example.invalid`
|
||||
|
||||
async function aufraeumen(): Promise<void> {
|
||||
// Reihenfolge egal, die Kaskaden erledigen den Rest; der Audit-Eintrag
|
||||
// hängt bewusst an keinem Benutzer mehr.
|
||||
await db.auditEintrag.deleteMany({ where: { benutzerEmail: testEmail } })
|
||||
await db.benutzer.deleteMany({ where: { email: testEmail } })
|
||||
await db.bereich.deleteMany({ where: { name: { startsWith: kennung } } })
|
||||
await db.webauthnChallenge.deleteMany({ where: { kennung } })
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// --- Benutzer und Eindeutigkeit der Adresse ------------------------------
|
||||
const benutzer = await db.benutzer.create({
|
||||
data: { email: testEmail, name: 'Testkonto' },
|
||||
})
|
||||
pruefe('Benutzer anlegen', benutzer.id.length > 0)
|
||||
pruefe('Rolle ist im Zweifel BENUTZER', benutzer.rolle === 'BENUTZER')
|
||||
pruefe('Konto ist im Zweifel aktiv', benutzer.aktiv === true)
|
||||
|
||||
await scheitert('E-Mail-Adresse ist eindeutig', () =>
|
||||
db.benutzer.create({ data: { email: testEmail } }),
|
||||
)
|
||||
|
||||
// --- Bereich und Eindeutigkeit des Namens --------------------------------
|
||||
const bereich = await db.bereich.create({
|
||||
data: { name: `${kennung}-bereich`, farbe: '#2563eb', kapazitaetMo: 120 },
|
||||
})
|
||||
pruefe('Bereich anlegen', bereich.name.endsWith('-bereich'))
|
||||
pruefe(
|
||||
'Arbeitsfenster hat brauchbare Standardwerte',
|
||||
bereich.arbeitsfensterVon === 480 && bereich.arbeitsfensterBis === 1020,
|
||||
)
|
||||
|
||||
await scheitert('Bereichsname ist eindeutig', () =>
|
||||
db.bereich.create({ data: { name: bereich.name, farbe: '#000000' } }),
|
||||
)
|
||||
|
||||
// --- Passkey, Eindeutigkeit und Fremdschlüssel ---------------------------
|
||||
const passkey = await db.passkey.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
credentialId: `${kennung}-cred`,
|
||||
publicKey: Buffer.from([1, 2, 3]),
|
||||
bezeichnung: 'Testgerät',
|
||||
},
|
||||
})
|
||||
pruefe('Passkey anlegen', passkey.counter === 0n)
|
||||
pruefe(
|
||||
'Zähler ist ein BigInt und beginnt bei null',
|
||||
typeof passkey.counter === 'bigint',
|
||||
)
|
||||
|
||||
await scheitert('Credential-ID ist eindeutig', () =>
|
||||
db.passkey.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
credentialId: passkey.credentialId,
|
||||
publicKey: Buffer.from([4]),
|
||||
bezeichnung: 'Zweitgerät',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await scheitert('Passkey ohne gültigen Benutzer ist nicht möglich', () =>
|
||||
db.passkey.create({
|
||||
data: {
|
||||
benutzerId: 'gibt-es-nicht',
|
||||
credentialId: `${kennung}-waise`,
|
||||
publicKey: Buffer.from([9]),
|
||||
bezeichnung: 'Waise',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// --- Öffentlicher Schlüssel kommt unverändert zurück ---------------------
|
||||
const gelesen = await db.passkey.findUnique({ where: { id: passkey.id } })
|
||||
pruefe(
|
||||
'Der öffentliche Schlüssel kommt Byte für Byte zurück',
|
||||
Buffer.from(gelesen?.publicKey ?? []).equals(Buffer.from([1, 2, 3])),
|
||||
)
|
||||
|
||||
// --- Konto und Eindeutigkeit je Anbieter ---------------------------------
|
||||
await db.konto.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
type: 'oidc',
|
||||
provider: 'oidc',
|
||||
providerAccountId: kennung,
|
||||
},
|
||||
})
|
||||
await scheitert('Ein Anbieterkonto ist nur einmal verknüpfbar', () =>
|
||||
db.konto.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
type: 'oidc',
|
||||
provider: 'oidc',
|
||||
providerAccountId: kennung,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// --- Audit überlebt den Benutzer ----------------------------------------
|
||||
const audit = await db.auditEintrag.create({
|
||||
data: {
|
||||
benutzerId: benutzer.id,
|
||||
benutzerEmail: benutzer.email,
|
||||
aktion: 'test.durchgefuehrt',
|
||||
objektTyp: 'Bereich',
|
||||
objektId: bereich.id,
|
||||
details: { name: bereich.name },
|
||||
instanz: 'daheim',
|
||||
},
|
||||
})
|
||||
pruefe('Audit-Eintrag anlegen', audit.id.length > 0)
|
||||
|
||||
await db.benutzer.delete({ where: { id: benutzer.id } })
|
||||
|
||||
const nachher = await db.auditEintrag.findUnique({ where: { id: audit.id } })
|
||||
pruefe('Audit-Eintrag überlebt das Löschen des Benutzers', nachher !== null)
|
||||
pruefe(
|
||||
'Die Kennung wird gelöst, die Adresse bleibt als Nachweis stehen',
|
||||
nachher?.benutzerId === null && nachher?.benutzerEmail === testEmail,
|
||||
)
|
||||
|
||||
pruefe(
|
||||
'Passkeys verschwinden mit dem Benutzer',
|
||||
(await db.passkey.count({ where: { credentialId: passkey.credentialId } })) === 0,
|
||||
)
|
||||
pruefe(
|
||||
'Verknüpfte Konten verschwinden mit dem Benutzer',
|
||||
(await db.konto.count({ where: { providerAccountId: kennung } })) === 0,
|
||||
)
|
||||
}
|
||||
|
||||
let abbruch: unknown = null
|
||||
|
||||
main()
|
||||
.catch((fehler: unknown) => {
|
||||
abbruch = fehler
|
||||
})
|
||||
.finally(async () => {
|
||||
await aufraeumen()
|
||||
|
||||
const uebrig =
|
||||
(await db.benutzer.count({ where: { email: testEmail } })) +
|
||||
(await db.bereich.count({ where: { name: { startsWith: kennung } } }))
|
||||
pruefe('Aufräumen hat nichts übrig gelassen', uebrig === 0)
|
||||
|
||||
await db.$disconnect()
|
||||
|
||||
if (abbruch) {
|
||||
console.error('Der Integrationstest ist abgebrochen:', abbruch)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const gefallen = ergebnisse.filter((e) => !e.ok)
|
||||
for (const e of ergebnisse) {
|
||||
console.log(`${e.ok ? '✓' : '✗'} ${e.name}${e.hinweis ? ` – ${e.hinweis}` : ''}`)
|
||||
}
|
||||
console.log(
|
||||
`\n${ergebnisse.length - gefallen.length} von ${ergebnisse.length} Prüfungen bestanden.`,
|
||||
)
|
||||
process.exit(gefallen.length === 0 ? 0 : 1)
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Migrationstest.
|
||||
*
|
||||
* Legt eine Wegwerf-Datenbank an, spielt alle Migrationen der Reihe nach ein
|
||||
* und prüft anschließend, ob das Ergebnis genau dem Schema entspricht. Damit
|
||||
* fällt auf, wenn jemand das Schema ändert, ohne eine Migration zu erzeugen —
|
||||
* beim Aufspielen im Betrieb wäre das sonst erst spät zu merken.
|
||||
*
|
||||
* Aufruf: npm run test:migration
|
||||
*/
|
||||
import 'dotenv/config'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { join } from 'node:path'
|
||||
import { Client } from 'pg'
|
||||
|
||||
const WURZEL = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
||||
|
||||
const verbindung = process.env.DATABASE_URL
|
||||
if (!verbindung) {
|
||||
console.error('DATABASE_URL ist nicht gesetzt.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const testDatenbank = `planer_migrationstest_${Date.now()}`
|
||||
|
||||
/** Dieselbe Verbindung, aber auf eine andere Datenbank gerichtet. */
|
||||
function mitDatenbank(url: string, name: string): string {
|
||||
const zerlegt = new URL(url)
|
||||
zerlegt.pathname = `/${name}`
|
||||
return zerlegt.toString()
|
||||
}
|
||||
|
||||
function prisma(argumente: string[], zusatz: Record<string, string>): number {
|
||||
const ergebnis = spawnSync(
|
||||
process.execPath,
|
||||
[join(WURZEL, 'node_modules', 'prisma', 'build', 'index.js'), ...argumente],
|
||||
{
|
||||
cwd: WURZEL,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, ...zusatz },
|
||||
},
|
||||
)
|
||||
return ergebnis.status ?? 1
|
||||
}
|
||||
|
||||
async function aufVerwaltungsdatenbank<T>(
|
||||
tun: (klient: Client) => Promise<T>,
|
||||
): Promise<T> {
|
||||
// "postgres" ist auf jedem Server vorhanden und wird hier nur benutzt, um
|
||||
// die Wegwerf-Datenbank anzulegen und wieder zu entfernen.
|
||||
const klient = new Client({ connectionString: mitDatenbank(verbindung!, 'postgres') })
|
||||
await klient.connect()
|
||||
try {
|
||||
return await tun(klient)
|
||||
} finally {
|
||||
await klient.end()
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
console.log(`▸ Wegwerf-Datenbank ${testDatenbank} anlegen`)
|
||||
await aufVerwaltungsdatenbank((k) => k.query(`CREATE DATABASE "${testDatenbank}"`))
|
||||
|
||||
const testUrl = mitDatenbank(verbindung!, testDatenbank)
|
||||
|
||||
console.log('▸ Alle Migrationen einspielen')
|
||||
if (prisma(['migrate', 'deploy'], { DATABASE_URL: testUrl }) !== 0) {
|
||||
console.error('✗ Die Migrationen ließen sich nicht einspielen.')
|
||||
return 1
|
||||
}
|
||||
|
||||
console.log('▸ Ergebnis mit dem Schema vergleichen')
|
||||
// --exit-code liefert 2, wenn ein Unterschied bleibt.
|
||||
const abweichung = prisma(
|
||||
[
|
||||
'migrate',
|
||||
'diff',
|
||||
// Die Quelle ist die eben eingespielte Wegwerf-Datenbank, das Ziel das
|
||||
// Schema. So braucht der Vergleich keine Schattendatenbank.
|
||||
'--from-config-datasource',
|
||||
'--to-schema',
|
||||
'prisma/schema.prisma',
|
||||
'--exit-code',
|
||||
],
|
||||
{ DATABASE_URL: testUrl },
|
||||
)
|
||||
|
||||
if (abweichung === 2) {
|
||||
console.error(
|
||||
'\n✗ Nach dem Einspielen weicht die Datenbank vom Schema ab.\n' +
|
||||
' Vermutlich fehlt eine Migration: "npx prisma migrate dev --name <name>".',
|
||||
)
|
||||
return 1
|
||||
}
|
||||
if (abweichung !== 0) {
|
||||
console.error('✗ Der Vergleich ließ sich nicht durchführen.')
|
||||
return 1
|
||||
}
|
||||
|
||||
console.log('\n✓ Die Migrationen ergeben genau das Schema.')
|
||||
return 0
|
||||
}
|
||||
|
||||
let code = 1
|
||||
try {
|
||||
code = await main()
|
||||
} catch (fehler) {
|
||||
console.error('Der Migrationstest ist abgebrochen:', fehler)
|
||||
} finally {
|
||||
await aufVerwaltungsdatenbank(async (k) => {
|
||||
// Offene Verbindungen trennen, sonst lässt sich die Datenbank nicht löschen.
|
||||
await k.query(
|
||||
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1',
|
||||
[testDatenbank],
|
||||
)
|
||||
await k.query(`DROP DATABASE IF EXISTS "${testDatenbank}"`)
|
||||
})
|
||||
console.log(`▸ Wegwerf-Datenbank ${testDatenbank} entfernt`)
|
||||
}
|
||||
|
||||
process.exit(code)
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Kopfleiste } from '@/components/kopfleiste'
|
||||
import { INSTANZ_NAME } from '@/lib/instanz'
|
||||
import { versionAnzeige } from '@/lib/version'
|
||||
import { verlangeAnmeldung } from '@/server/berechtigung'
|
||||
import { angefragterPfad, verlangeAnmeldung } from '@/server/berechtigung'
|
||||
import { aktuelleInstanz } from '@/server/instanz'
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,9 @@ export default async function AngemeldetesLayout({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const benutzer = await verlangeAnmeldung()
|
||||
// Der Pfad kommt aus der Kopfzeile der Middleware, damit es nach der
|
||||
// Anmeldung dort weitergeht, wo man hinwollte.
|
||||
const benutzer = await verlangeAnmeldung(await angefragterPfad())
|
||||
const instanz = aktuelleInstanz()
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export const PFAD_KOPFZEILE = 'x-planer-pfad'
|
||||
|
||||
/**
|
||||
* Reicht den angefragten Pfad als Kopfzeile weiter.
|
||||
*
|
||||
* Ein Layout kennt seinen eigenen Pfad nicht, braucht ihn aber, um nach der
|
||||
* Anmeldung wieder dorthin zurückzuführen. Hier steht bewusst keine
|
||||
* Anmeldelogik: die Middleware läuft im Edge-Umfeld, in dem weder Prisma noch
|
||||
* der Adapter verfügbar sind. Geprüft wird weiterhin im Layout, in der Seite
|
||||
* und in jeder Server Action.
|
||||
*/
|
||||
export function middleware(anfrage: NextRequest) {
|
||||
const kopfzeilen = new Headers(anfrage.headers)
|
||||
kopfzeilen.set(PFAD_KOPFZEILE, `${anfrage.nextUrl.pathname}${anfrage.nextUrl.search}`)
|
||||
return NextResponse.next({ request: { headers: kopfzeilen } })
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Alles außer den Auth-Routen, statischen Dateien und Bildern.
|
||||
'/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
|
||||
],
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
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.
|
||||
@@ -72,3 +74,13 @@ export async function pruefeRolle(verlangt: Rolle): Promise<Angemeldet> {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user