Drei serverseitige Lese-Detailseiten (Fahrzeug, Gerät, Wehr), default-deny:
- src/lib/detail/merkmale.ts: formatMerkmal (de-AT Tausenderpunkt + NBSP,
Ja/Nein, enum-Label, „–"), toEckdaten. ICU-unabhängige Zahl-Formatierung
(formatToParts -> Punkt/Komma), da de-AT je nach ICU-Build U+202F gruppiert.
- src/lib/detail/queries.ts: read-only, wehrübergreifend; loadMerkmalRows
(Join merkmal_values↔merkmale↔merkmal_optionen via wert=value_text),
getFahrzeugDetail (+Beladung, +WehrCard), getGeraetDetail (Fahrzeug-Link
oder „im Gerätehaus"), getWehrDetail (Fuhrpark + Geräte im Haus),
getBrigadeCard. UUID-IDs.
- Komponenten: detail/{DetailHeader,EckdatenGrid,BeladungListe,StatusBadge},
kontakt/{KontaktButton (tel:/mailto:, Telefon ohne Leerzeichen, subject;
Empty-State),WehrCard}.
- Seiten (app)/{fahrzeuge,geraete,wehren}/[id]/page.tsx mit requireSession()
als erster Zeile (Default-deny in der Tiefe) + fahrzeuge/[id]/not-found.tsx.
- i18n-Keys (detail/kontakt/wehr) ergänzt; keine hartkodierten Strings.
Tests: merkmale.test.ts (11), queries.test.ts (3, gemockte DB für
„im Gerätehaus" + not-found). Playwright detail-auth.spec.ts geschrieben
(deferred: kein Server/DB in Sandbox); Detailrouten ins Gating-Manifest
aufgenommen.
Offline verifiziert: vitest src/lib/detail grün; tsc --noEmit ok; eslint
ok; next build erfolgreich (alle drei [id]-Routen vorhanden).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
||
|
||
/**
|
||
* Auth-Gating-Garantie (Definition of Done, oberstes Prinzip).
|
||
*
|
||
* NICHT in der Sandbox ausführbar (kein Server/DB) — deferred. Wird über
|
||
* `npm run test:e2e:gating` gegen einen laufenden Server ausgeführt.
|
||
*
|
||
* Kerngarantie (Querschnittsstandard 1–3, default-deny dreifach):
|
||
* - Anonyme Aufrufe von Seiten -> Redirect auf /login (mit callbackUrl).
|
||
* - Anonyme Aufrufe von API-Routen -> 401 OHNE Daten-Leak.
|
||
* - Öffentliche Routen (Login, Health) bleiben erreichbar.
|
||
*
|
||
* Negativ-Probe (manuell/CI): Entfernen von `requireSession()` aus
|
||
* `(app)/layout.tsx` muss diese Suite rot machen.
|
||
*/
|
||
|
||
// Geschützte Seiten (Redirect-Manifest). Neue Seiten hier ergänzen.
|
||
const PROTECTED_PAGES = [
|
||
"/",
|
||
"/start",
|
||
"/fahrzeuge",
|
||
"/fahrzeuge/00000000-0000-0000-0000-000000000001",
|
||
"/geraete/00000000-0000-0000-0000-000000000002",
|
||
"/wehren/00000000-0000-0000-0000-000000000003",
|
||
"/geraete",
|
||
"/wehren",
|
||
"/verwaltung",
|
||
"/admin",
|
||
];
|
||
|
||
// Geschützte API-Routen (401-Manifest). Neue API-Routen hier ergänzen.
|
||
const PROTECTED_API = ["/api/fahrzeuge", "/api/geraete", "/api/verwaltung"];
|
||
|
||
// Öffentliche Routen (Middleware-Allowlist).
|
||
const PUBLIC_ROUTES = ["/login", "/api/health"];
|
||
|
||
test.describe("Default-deny: geschützte Seiten", () => {
|
||
for (const path of PROTECTED_PAGES) {
|
||
test(`anonymer Aufruf von ${path} leitet auf /login um`, async ({
|
||
page,
|
||
}) => {
|
||
await page.goto(path);
|
||
await expect(page).toHaveURL(/\/login/);
|
||
});
|
||
}
|
||
});
|
||
|
||
test.describe("Default-deny: geschützte API-Routen", () => {
|
||
for (const path of PROTECTED_API) {
|
||
test(`anonymer Aufruf von ${path} liefert 401 ohne Daten-Leak`, async ({
|
||
request,
|
||
}) => {
|
||
const res = await request.get(path);
|
||
expect(res.status()).toBe(401);
|
||
const body = await res.text();
|
||
// Kein Daten-Leak: nur eine generische Fehlermeldung.
|
||
expect(body).not.toContain("brigade");
|
||
expect(body).not.toContain("passwort");
|
||
});
|
||
}
|
||
});
|
||
|
||
test.describe("Öffentliche Routen bleiben erreichbar", () => {
|
||
test("Login-Seite ist anonym erreichbar", async ({ page }) => {
|
||
await page.goto("/login");
|
||
await expect(
|
||
page.getByRole("heading", { name: "Anmelden" }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("Health-Check ist anonym 200", async ({ request }) => {
|
||
const res = await request.get("/api/health");
|
||
expect(res.status()).toBe(200);
|
||
expect(await res.json()).toEqual({ status: "ok" });
|
||
});
|
||
});
|
||
|
||
void PUBLIC_ROUTES;
|