Bisher war vorgaengerId eindeutig: nach einer abgebrochenen Rotation passte kein zweiter Nachfolger in die Kette, und der Schlüssel ließ sich nur über Neuanlage, Zuweisen und Widerruf ersetzen. Jetzt ist die Relation 1:n. Ein partieller Index erlaubt je Vorgänger nur einen Nachfolger, der weder widerrufen ist noch gerade widerrufen wird. Maßgeblich ist der jüngste. Erst wenn der Abbruch abgeschlossen ist, beginnt die Rotation wieder bei Schritt 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
import { KeyRound } from "lucide-react";
|
|
import type { Metadata } from "next";
|
|
import { notFound } from "next/navigation";
|
|
import { Rotation } from "@/components/rotation/Rotation";
|
|
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Leerzustand } from "@/components/ui/Leerzustand";
|
|
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
|
import { darf } from "@/server/auth/rechte";
|
|
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
|
import { RotationsFehler, rotationsStand } from "@/server/services/rotation";
|
|
import { offeneEntfernungen } from "@/server/services/widerruf";
|
|
|
|
export const metadata: Metadata = { title: "Schlüssel rotieren" };
|
|
|
|
/** Gefuehrte Rotation (#023): Schrittfolge, wiederaufnehmbar. */
|
|
export default async function RotierenSeite({ params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params;
|
|
const { benutzer } = await sitzungErforderlich("schluesselVerwalten", `/schluessel/${id}/rotieren`);
|
|
if (!darf(benutzer.rolle, "publicKeyAusrollen")) notFound();
|
|
let stand;
|
|
try {
|
|
stand = await rotationsStand(id);
|
|
} catch (fehler) {
|
|
if (fehler instanceof RotationsFehler) notFound();
|
|
throw fehler;
|
|
}
|
|
const { alt } = stand;
|
|
|
|
const kopf = (
|
|
<Seitenkopf
|
|
oben={
|
|
<Breadcrumb
|
|
pfad={[
|
|
{ titel: "Schlüssel", href: "/schluessel" },
|
|
{ titel: alt.name, href: `/schluessel/${alt.id}` },
|
|
{ titel: "Rotieren" },
|
|
]}
|
|
/>
|
|
}
|
|
titel={`„${alt.name}“ rotieren`}
|
|
untertitel="Neuer Schlüssel an allen Stellen, testen, erst dann den alten entfernen."
|
|
/>
|
|
);
|
|
|
|
// Ohne Nachfolger und nicht rotierbar: nur der Hinweis, keine halbe Rotation.
|
|
if (!stand.neu && stand.gesperrt) {
|
|
return (
|
|
<div className="flex flex-col gap-6 sm:gap-7">
|
|
{kopf}
|
|
<Leerzustand
|
|
icon={<KeyRound className="size-5" />}
|
|
titel={stand.gesperrt.neuerPublicKeyNoetig ? "Neuer Public Key nötig" : "Nicht rotierbar"}
|
|
text={stand.gesperrt.grund}
|
|
aktion={<Button href={`/schluessel/${alt.id}`}>Zurück zum Schlüssel</Button>}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const widerruf =
|
|
stand.neu && !stand.abgebrochen && alt.widerrufEingeleitetAm && alt.status !== "ROTIERT"
|
|
? await offeneEntfernungen([alt.id])
|
|
: null;
|
|
// Abbruch (#032): solange der neue Schluessel nicht widerrufen ist, laeuft sein Widerruf hier weiter.
|
|
const abbruch =
|
|
stand.neu && stand.abgebrochen
|
|
? { offen: stand.neu.status === "WIDERRUFEN" ? null : await offeneEntfernungen([stand.neu.id]) }
|
|
: null;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 sm:gap-7">
|
|
{kopf}
|
|
<Rotation
|
|
daten={{
|
|
alt: { id: alt.id, name: alt.name },
|
|
neu: stand.neu ? { id: stand.neu.id, name: stand.neu.name } : null,
|
|
schritt: stand.schritt,
|
|
neuOffen: stand.neu ? stand.neuOffen : 0,
|
|
tests: stand.neu ? stand.tests : [],
|
|
widerruf,
|
|
abbruch,
|
|
letzterAbbruch:
|
|
!stand.neu && stand.alt.nachfolger?.status === "WIDERRUFEN"
|
|
? { id: stand.alt.nachfolger.id, name: stand.alt.nachfolger.name }
|
|
: null,
|
|
darfPrivat: darf(benutzer.rolle, "privateKeyAusrollen"),
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|