Files
sshkeyvault/src/app/(app)/page.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

191 lines
7.1 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 { ArrowRight, Plus, RefreshCw, Route } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import {
abweichungen,
BEISPIEL_JETZT,
baldAblaufend,
kennzahlen,
letzteAktionen,
letzterAbgleich,
type AktionsArt,
} from "@/beispiel/uebersicht";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { Karte } from "@/components/ui/Karte";
import { Kennzahl } from "@/components/ui/Kennzahl";
import { Seitenkopf } from "@/components/ui/Seitenkopf";
import { Statuspunkt } from "@/components/ui/Statuspunkt";
import { Tabelle, TabellenZeile, TabellenZelle } from "@/components/ui/Tabelle";
import { datum, datumZeit, relativ, tageBis, uhrzeit } from "@/lib/format";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
export const metadata: Metadata = { title: "Übersicht" };
const AKTION: Record<AktionsArt, { text: string; farbe: string }> = {
ausgerollt: { text: "Ausgerollt", farbe: "text-ok" },
widerrufen: { text: "Widerrufen", farbe: "text-danger" },
erstellt: { text: "Erstellt", farbe: "text-text" },
abgleich: { text: "Abgleich", farbe: "text-info" },
rotiert: { text: "Rotiert", farbe: "text-accent" },
};
/**
* Prototyp der Übersicht mit Beispieldaten (#002). Die echte Übersicht mit Lesemodellen
* folgt in #026; Aufbau und Komponenten bleiben.
*/
export default async function Uebersicht() {
// Jede Seite prueft selbst; die Huelle ist nur das zweite Netz (CLAUDE.md).
await sitzungErforderlich();
return (
<div className="flex flex-col gap-6 sm:gap-7">
<Seitenkopf
titel="Übersicht"
badges={<Badge variante="messing">Prototyp · Beispieldaten</Badge>}
untertitel={
<>
Letzter Abgleich {relativ(letzterAbgleich, BEISPIEL_JETZT)} ·{" "}
<time dateTime={letzterAbgleich} className="font-mono text-[13px]">
{datumZeit(letzterAbgleich)}
</time>
</>
}
aktionen={
<>
<Button icon={<RefreshCw className="size-4" />}>Jetzt abgleichen</Button>
<Button href="/zuweisungen" icon={<Route className="size-4" />}>
Host-zu-Host-Assistent
</Button>
<Button
href="/schluessel"
variante="primaer"
icon={<Plus className="size-4" strokeWidth={2.4} />}
>
Neuer Schlüssel
</Button>
</>
}
/>
<div className="grid grid-cols-2 gap-3 sm:gap-5 xl:grid-cols-4">
<Kennzahl
titel="Hosts"
wert={kennzahlen.hosts.wert}
hinweis={kennzahlen.hosts.hinweis}
href="/hosts"
/>
<Kennzahl
titel="Schlüssel"
wert={kennzahlen.schluessel.wert}
hinweis={kennzahlen.schluessel.hinweis}
href="/schluessel"
/>
<Kennzahl
titel="Abweichungen"
variante="gefahr"
wert={kennzahlen.abweichungen.wert}
hinweis={kennzahlen.abweichungen.hinweis}
href="/hosts"
/>
<Kennzahl
titel="Läuft in 30 Tagen ab"
variante="messing"
wert={kennzahlen.ablaufend.wert}
hinweis={kennzahlen.ablaufend.hinweis}
href="/schluessel"
/>
</div>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.55fr)_minmax(0,1fr)]">
<Karte titel="Hosts mit Abweichung" untertitel="Ergebnis des letzten Abgleichs">
<Tabelle
beschriftung="Hosts mit Abweichung"
spalten={[
{ titel: "Host" },
{ titel: "Benutzer" },
{ titel: "Befund" },
{ titel: "Aktion", rechts: true, versteckt: true },
]}
>
{abweichungen.map((a) => (
<TabellenZeile key={`${a.host}-${a.benutzer}`}>
<TabellenZelle mono className="whitespace-nowrap">
{a.host}
</TabellenZelle>
<TabellenZelle mono>{a.benutzer}</TabellenZelle>
<TabellenZelle className="whitespace-nowrap">
<Statuspunkt status={a.status} text={a.befund} />
</TabellenZelle>
<TabellenZelle rechts>
<Link
href="/hosts"
className="text-accent -my-2 inline-flex min-h-11 items-center px-1 font-medium hover:underline"
>
Prüfen<span className="sr-only"> – {a.host}</span>
</Link>
</TabellenZelle>
</TabellenZeile>
))}
</Tabelle>
</Karte>
<Karte titel="Bald ablaufend" untertitel="Schlüssel mit Ablaufdatum in den nächsten 30 Tagen">
<ul className="flex flex-col">
{baldAblaufend.map((s) => {
const tage = tageBis(s.ablauf, BEISPIEL_JETZT);
return (
<li key={s.zweck} className="border-border border-b last:border-b-0">
<Link
href="/schluessel/beispiel"
className="hover:bg-raised/50 rounded-box -mx-2 flex min-h-11 items-center justify-between gap-3 px-2 py-2.5 transition-colors"
>
<span className="min-w-0">
<span className="text-text block truncate text-[14.5px] font-medium">{s.zweck}</span>
<span className="text-muted block truncate text-[13px]">
{s.person} · <span className="font-mono">{s.typ}</span>
</span>
</span>
<Badge variante="messing" className="shrink-0">
<time dateTime={s.ablauf}>{tage === 1 ? "morgen" : `in ${tage} Tagen`}</time>
</Badge>
</Link>
</li>
);
})}
</ul>
</Karte>
</div>
<Karte
titel="Letzte Aktionen"
aktionen={
<Link
href="/protokoll"
className="text-accent inline-flex min-h-11 items-center gap-1.5 text-[14px] font-medium hover:underline"
>
Zum Audit-Log <ArrowRight className="size-4" aria-hidden />
</Link>
}
>
<ol className="flex flex-col">
{letzteAktionen.map((a) => (
<li
key={a.zeit}
className="border-border grid grid-cols-[auto_1fr] gap-x-4 gap-y-0.5 border-b py-3 text-[14px] last:border-b-0 md:grid-cols-[8.5rem_6.5rem_1fr_auto] md:items-baseline"
>
<time dateTime={a.zeit} className="text-muted font-mono text-[12.5px] whitespace-nowrap">
{datum(a.zeit)} {uhrzeit(a.zeit)}
</time>
<span className={`${AKTION[a.art].farbe} font-medium md:order-none`}>{AKTION[a.art].text}</span>
<span className="text-text col-span-2 min-w-0 md:col-span-1">{a.beschreibung}</span>
<span className="text-muted col-span-2 text-[13px] md:col-span-1 md:text-right">
{a.benutzer}
</span>
</li>
))}
</ol>
</Karte>
</div>
);
}