"use client"; import { Ban, CircleCheck, CircleX, KeyRound, PlugZap } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { AusrollenPanel } from "@/components/ausrollen/AusrollenPanel"; import { Button, Formularfehler, Karte } from "@/components/ui"; import { Schrittleiste } from "@/components/ui/Schrittleiste"; import { warten } from "@/components/vorgang/warten"; import { WiderrufAblauf } from "@/components/widerruf/WiderrufAblauf"; import { rotationAltWiderrufenAktion, rotationStartenAktion, rotationUebernehmenAktion, } from "@/server/actions/rotation"; import { verbindungTestenAktion } from "@/server/actions/verwendungen"; import type { OffeneEntfernung } from "@/server/services/widerruf"; /** * Gefuehrte Rotation (#023) als Schrittfolge wie der Assistent (#020). Der Schritt kommt vom * Server (aus der Datenbank errechnet) – nach Neuladen, Abbruch oder Fehlschlag geht es an * derselben Stelle weiter. */ const SCHRITTE = [ "Neuer Schlüssel", "Zuweisungen übernehmen", "Dry-Run & Ausrollen", "Verbindungstests", "Alten widerrufen", ] as const; export type RotationsTest = { id: string; ziel: string; ausgerollt: boolean; status: string | null; fehler: string | null; }; export type RotationsDaten = { alt: { id: string; name: string }; neu: { id: string; name: string } | null; schritt: 0 | 1 | 2 | 3 | 4 | 5; neuOffen: number; tests: RotationsTest[]; /** Offene Entfernungen des alten Schluessels, sobald sein Widerruf laeuft. */ widerruf: OffeneEntfernung[] | null; darfPrivat: boolean; }; export function Rotation({ daten }: { daten: RotationsDaten }) { const router = useRouter(); const [laeuft, setLaeuft] = useState(); const [fehler, setFehler] = useState(); const { alt, neu, schritt } = daten; async function kette(arbeit: () => Promise) { setFehler(undefined); try { const meldung = await arbeit(); if (meldung) setFehler(meldung); } catch (f) { setFehler((f as Error).message); } finally { setLaeuft(undefined); router.refresh(); } } const vorbereiten = () => kette(async () => { if (!neu) { setLaeuft("Neues Schlüsselpaar wird erzeugt …"); const antwort = await rotationStartenAktion(alt.id); if (antwort.fehler || !antwort.vorgangId) return antwort.fehler; const ende = await warten(antwort.vorgangId); if (ende.status !== "ERLEDIGT") return ende.fehler ?? "Erzeugen fehlgeschlagen."; } setLaeuft("Zuweisungen werden übernommen …"); return (await rotationUebernehmenAktion(alt.id)).fehler; }); const testen = () => kette(async () => { for (const test of daten.tests) { setLaeuft(`Teste ${test.ziel} …`); const antwort = await verbindungTestenAktion(test.id); if (antwort.fehler || !antwort.vorgangId) return antwort.fehler; await warten(antwort.vorgangId); } return undefined; }); const altWiderrufen = () => kette(async () => { setLaeuft("Alter Schlüssel wird zum Entfernen vorgemerkt …"); return (await rotationAltWiderrufenAktion(alt.id)).fehler; }); const status = ( <> {laeuft && (

{laeuft}

)} ); return (
{schritt <= 1 && (
  • Neues Schlüsselpaar gleichen Typs, Name mit „(rotiert …)“, verknüpft mit dem alten.
  • Public Key: dieselben Ziele und Einschränkungen wie bisher.
  • Private Key: derselbe Host und Benutzer, neue Datei mit Suffix -rJJJJMMTT; der config-Eintrag behält seinen Alias und zeigt danach auf die neue Datei.
  • Der alte Schlüssel bleibt gültig, bis der neue getestet ist.
{status}
)} {schritt === 2 && neu && ( )} {schritt === 3 && (
    {daten.tests.map((t) => (
  • {t.status === "ERLEDIGT" ? ( ) : t.status === "FEHLGESCHLAGEN" ? ( ) : ( )} {t.ziel} {t.fehler && {t.fehler}}
  • ))}
{status}
)} {schritt === 4 && !daten.widerruf && ( 0 ? "Alle Verbindungstests sind grün." : "Keine Private-Key-Zuweisung mit Alias – es gibt nichts zu testen." } >

Der alte Schlüssel „{alt.name}“ wird wie bei einem Widerruf von allen Hosts entfernt (Dry-Run folgt). Danach gilt er als rotiert.

{status}
)} {schritt === 4 && daten.widerruf && ( )} {schritt === 5 && neu && (

Der alte Schlüssel ist von allen Hosts entfernt. Weiter mit{" "} {neu.name} .

)}
); }