Anmeldung, Einrichtung des zweiten Faktors fuer den Notfall-Admin, erneute Bestaetigung vor kritischen Aktionen und die Seite fuer fehlende Rechte. Die Huelle zeigt jetzt den echten Benutzer, und jede Seite prueft die Sitzung selbst; ein struktureller Test haelt das fest. Die Ablaeufe sind im gebauten Stand gegen den Mock-OIDC geprueft (pnpm test:e2e): Admin, Konto ohne Gruppe, Notfall-Admin mit TOTP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
import { LogOut } from "lucide-react";
|
||
import Link from "next/link";
|
||
import type { ReactNode } from "react";
|
||
import { Bildmarke, Kurzname, Marke, Wortmarke } from "@/components/marke";
|
||
import { VERSION } from "@/lib/version";
|
||
import { Navigation } from "./Navigation";
|
||
import { SeitenleistenRahmen } from "./SeitenleistenRahmen";
|
||
import { ROLLEN_TEXT, type HuellenBenutzer } from "./typen";
|
||
|
||
/**
|
||
* Grundlayout (#002): feste Seitenleiste links (248 px), Hauptbereich rechts.
|
||
* Benutzer und aktiver Pfad kommen als Props – die Hülle kennt weder Sitzung noch
|
||
* Datenbank, damit sie in Tests und Prototypen ohne Anmeldung rendert.
|
||
*/
|
||
export function Huelle({
|
||
benutzer,
|
||
aktiverPfad,
|
||
children,
|
||
}: {
|
||
benutzer: HuellenBenutzer;
|
||
/** Ohne Angabe ermittelt die Navigation den Pfad im Browser. */
|
||
aktiverPfad?: string;
|
||
children: ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="bg-bg min-h-dvh lg:flex">
|
||
<a
|
||
href="#inhalt"
|
||
className="bg-accent text-bg rounded-box sr-only z-[60] px-4 py-3 font-semibold focus:not-sr-only focus:fixed focus:top-3 focus:left-3"
|
||
>
|
||
Zum Inhalt springen
|
||
</a>
|
||
|
||
<SeitenleistenRahmen
|
||
kopf={
|
||
<Link href="/" className="rounded-box flex min-h-11 items-center gap-2 px-1">
|
||
<Bildmarke groesse={28} />
|
||
<Wortmarke groesse={19} />
|
||
</Link>
|
||
}
|
||
>
|
||
<div className="flex h-full flex-col overflow-y-auto px-4 pt-6 pb-4">
|
||
<Link
|
||
href="/"
|
||
className="rounded-box mb-8 flex items-center px-2 py-1"
|
||
aria-label="SSHKeyVault – Übersicht"
|
||
>
|
||
<Marke bildGroesse={34} textGroesse={22} />
|
||
</Link>
|
||
|
||
<Navigation aktiverPfad={aktiverPfad} />
|
||
|
||
<div className="mt-auto pt-6">
|
||
<BenutzerKasten benutzer={benutzer} />
|
||
<p className="text-muted mt-3 px-2 font-mono text-[11.5px]">
|
||
<Kurzname /> v{VERSION}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</SeitenleistenRahmen>
|
||
|
||
<main id="inhalt" tabIndex={-1} className="min-w-0 flex-1 outline-none">
|
||
<div className="mx-auto max-w-[1440px] px-4 py-6 sm:px-8 sm:py-8 xl:px-10">{children}</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function BenutzerKasten({ benutzer }: { benutzer: HuellenBenutzer }) {
|
||
const kuerzel = benutzer.name
|
||
.split(/\s+/)
|
||
.filter(Boolean)
|
||
.slice(0, 2)
|
||
.map((t) => t[0]?.toUpperCase())
|
||
.join("");
|
||
return (
|
||
<div className="border-border bg-surface rounded-box flex items-center gap-3 border p-3">
|
||
<span
|
||
aria-hidden
|
||
className="bg-raised text-accent font-display flex size-9 shrink-0 items-center justify-center rounded-full text-[13px] font-semibold"
|
||
>
|
||
{kuerzel}
|
||
</span>
|
||
<div className="min-w-0 text-[13px] leading-snug">
|
||
<p className="text-text truncate text-[14px] font-medium">{benutzer.name}</p>
|
||
<p className="text-muted truncate">
|
||
{ROLLEN_TEXT[benutzer.rolle]} · über {benutzer.herkunft}
|
||
</p>
|
||
</div>
|
||
{/* Abmelden nur per POST, damit ein fremder Link niemanden abmelden kann (#004). */}
|
||
<form action="/api/auth/abmelden" method="post" className="ml-auto">
|
||
<button
|
||
type="submit"
|
||
title="Abmelden"
|
||
aria-label="Abmelden"
|
||
className="text-muted hover:text-text hover:bg-raised rounded-feld flex size-11 items-center justify-center"
|
||
>
|
||
<LogOut size={17} aria-hidden />
|
||
</button>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|