Files
sshkeyvault/src/components/huelle/Huelle.tsx
T
florian.wildeandClaude Opus 5.5 d1ba822a4a feat(auth): Anmeldeseiten, TOTP-Einrichtung und Bestaetigung (#004)
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>
2026-10-12 09:28:51 +02:00

104 lines
3.5 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 { 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>
);
}