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>
185 lines
6.5 KiB
TypeScript
185 lines
6.5 KiB
TypeScript
import { Ban, Check, Lock, PlugZap, Plus, RotateCw } from "lucide-react";
|
||
import type { Metadata } from "next";
|
||
import type { ReactNode } from "react";
|
||
import {
|
||
beispielSchluessel as s,
|
||
dryRunDatei,
|
||
dryRunDiff,
|
||
privatAusgerollt,
|
||
publicAusgerollt,
|
||
zusatzaktionen,
|
||
type Ausrollung,
|
||
} from "@/beispiel/schluessel";
|
||
import { BEISPIEL_JETZT } from "@/beispiel/uebersicht";
|
||
import { Badge } from "@/components/ui/Badge";
|
||
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { DiffBox } from "@/components/ui/DiffBox";
|
||
import { InnereBox, Karte } from "@/components/ui/Karte";
|
||
import { MonoKopieren } from "@/components/ui/MonoKopieren";
|
||
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||
import { Statuspunkt } from "@/components/ui/Statuspunkt";
|
||
import { datum, datumZeit, tageBis } from "@/lib/format";
|
||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||
|
||
export const metadata: Metadata = { title: `${s.zweck} (Beispiel)` };
|
||
|
||
/** Prototyp des Schlüsseldetails mit Beispieldaten (#002); echte Daten ab #012/#016/#017. */
|
||
export default async function SchluesselBeispiel() {
|
||
// Jede Seite prueft selbst; die Huelle ist nur das zweite Netz (CLAUDE.md).
|
||
await sitzungErforderlich();
|
||
const tage = tageBis(s.ablauf, BEISPIEL_JETZT);
|
||
return (
|
||
<div className="flex flex-col gap-6 sm:gap-7">
|
||
<Seitenkopf
|
||
oben={<Breadcrumb pfad={[{ titel: "Schlüssel", href: "/schluessel" }, { titel: s.zweck }]} />}
|
||
titel={s.zweck}
|
||
badges={
|
||
<>
|
||
<Badge mono>{s.typ}</Badge>
|
||
<Badge>{s.kategorie}</Badge>
|
||
<Badge>Vorlage: {s.vorlage}</Badge>
|
||
<Badge variante="messing">
|
||
<span>
|
||
läuft ab am <time dateTime={s.ablauf}>{datum(s.ablauf)}</time> · in {tage} Tagen
|
||
</span>
|
||
</Badge>
|
||
<Badge variante="neutral" className="text-muted">
|
||
Beispieldaten
|
||
</Badge>
|
||
</>
|
||
}
|
||
aktionen={
|
||
<>
|
||
<Button icon={<PlugZap className="size-4" />}>Verbindung testen</Button>
|
||
<Button icon={<RotateCw className="size-4" />}>Rotieren</Button>
|
||
<Button variante="gefahr" icon={<Ban className="size-4" />}>
|
||
Widerrufen
|
||
</Button>
|
||
</>
|
||
}
|
||
/>
|
||
|
||
<Karte as="div" innen="ohne">
|
||
<dl className="divide-border grid divide-y md:grid-cols-[minmax(0,1.6fr)_minmax(0,0.8fr)_minmax(0,1.1fr)] md:divide-x md:divide-y-0">
|
||
<InfoFeld titel="Fingerprint">
|
||
<MonoKopieren wert={s.fingerprint} beschriftung="Fingerprint" umbrechen className="-my-[11px]" />
|
||
</InfoFeld>
|
||
<InfoFeld titel="Erstellt">
|
||
<time dateTime={s.erstellt} className="font-mono text-[13px]">
|
||
{datumZeit(s.erstellt)}
|
||
</time>
|
||
<span className="text-muted block text-[13px]">von {s.erstelltVon}</span>
|
||
</InfoFeld>
|
||
<InfoFeld titel="Private Key">
|
||
<span className="inline-flex flex-wrap items-center gap-x-1.5 text-[14px]">
|
||
<Lock className="text-ok size-3.5" aria-hidden />
|
||
verschlüsselt gespeichert ·
|
||
<button
|
||
type="button"
|
||
className="text-accent -my-3 inline-flex min-h-11 items-center font-medium hover:underline"
|
||
>
|
||
anzeigen (Re-Auth)
|
||
</button>
|
||
</span>
|
||
</InfoFeld>
|
||
</dl>
|
||
</Karte>
|
||
|
||
<div className="grid gap-6 lg:grid-cols-2">
|
||
<AusrollKarte
|
||
titel="Private Key ausgerollt auf"
|
||
untertitel="Hosts, die sich mit diesem Schlüssel anmelden"
|
||
eintraege={privatAusgerollt}
|
||
/>
|
||
<AusrollKarte
|
||
titel="Public Key ausgerollt auf"
|
||
untertitel="Konten, die diesen Schlüssel annehmen"
|
||
eintraege={publicAusgerollt}
|
||
/>
|
||
</div>
|
||
|
||
<Karte
|
||
titel="Dry-Run-Vorschau"
|
||
untertitel={
|
||
<>
|
||
Abgleich für <span className="font-mono">backup@srv-pve02</span> – noch nichts geschrieben
|
||
</>
|
||
}
|
||
>
|
||
<DiffBox datei={dryRunDatei} zeilen={dryRunDiff} />
|
||
<fieldset className="mt-5">
|
||
<legend className="text-muted mb-1 text-[13px] font-medium">Zusätzlich ausführen</legend>
|
||
<ul className="flex flex-col">
|
||
{zusatzaktionen.map((z) => (
|
||
<li key={z.id}>
|
||
<label className="flex min-h-11 cursor-pointer items-center gap-3 text-[14.5px]">
|
||
<input
|
||
type="checkbox"
|
||
name={z.id}
|
||
defaultChecked={z.an}
|
||
className="accent-accent size-[18px] shrink-0"
|
||
/>
|
||
<span>{z.text}</span>
|
||
</label>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</fieldset>
|
||
<div className="border-border mt-5 flex flex-wrap gap-2.5 border-t pt-5">
|
||
<Button variante="primaer" icon={<Check className="size-4" strokeWidth={2.4} />}>
|
||
Änderung anwenden
|
||
</Button>
|
||
<Button>Verwerfen</Button>
|
||
</div>
|
||
</Karte>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function InfoFeld({ titel, children }: { titel: string; children: ReactNode }) {
|
||
return (
|
||
<div className="min-w-0 px-5 py-4 sm:px-6">
|
||
<dt className="text-muted mb-1.5 text-[13px] font-medium">{titel}</dt>
|
||
<dd className="min-w-0">{children}</dd>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function AusrollKarte({
|
||
titel,
|
||
untertitel,
|
||
eintraege,
|
||
}: {
|
||
titel: string;
|
||
untertitel: string;
|
||
eintraege: Ausrollung[];
|
||
}) {
|
||
return (
|
||
<Karte
|
||
titel={titel}
|
||
untertitel={untertitel}
|
||
aktionen={
|
||
<Button klein icon={<Plus className="size-4" strokeWidth={2.4} />}>
|
||
Ausrollen
|
||
</Button>
|
||
}
|
||
>
|
||
<ul className="flex flex-col gap-3">
|
||
{eintraege.map((e) => (
|
||
<li key={`${e.ziel}${e.pfad}`}>
|
||
<InnereBox className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||
<div className="min-w-0">
|
||
<p className="text-text font-mono text-[13.5px] font-medium break-all">{e.ziel}</p>
|
||
<p className="text-muted mt-1 font-mono text-[12.5px] break-all">{e.pfad}</p>
|
||
{e.zusatz && <p className="text-muted font-mono text-[12.5px] break-all">{e.zusatz}</p>}
|
||
</div>
|
||
<Statuspunkt status={e.status} text={e.statusText} className="shrink-0 sm:pt-0.5" />
|
||
</InnereBox>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</Karte>
|
||
);
|
||
}
|