feat(assistent): Host-zu-Host-Assistent mit Vorlagen (#018, #020)

Ein Durchgang fuer den Kern-Anwendungsfall: Vorlage und Zweck, Host A,
Host B mit Zielbenutzer, Zusammenfassung, dann Schluessel erzeugen,
Benutzer anlegen, zuweisen, Dry-Run, ausrollen und Verbindung testen.
Die Vorlage Deployment-Zugang fuellt deploy, Ablaufdatum, from= und
Alias vor; bei root als Ziel warnt der Assistent ausdruecklich.

Die Ausfuehrung ist wiederaufnehmbar: wird mittendrin eine frische
Bestaetigung fuer das Zielkonto noetig, geschieht sie in einem neuen
Tab, und nichts wird doppelt angelegt. Zielkonten laufen als eigene
Vorgaenge (neue Migration) und merken sich, ob sshkv den Benutzer
angelegt hat - nur dann loescht ein Widerruf ihn spaeter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 11:19:28 +02:00
co-authored by Claude Opus 5.5
parent bdf12b05e7
commit b80f14c5d7
16 changed files with 1144 additions and 2 deletions
@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import { Leerzustand } from "@/components/ui/Leerzustand";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
export const metadata: Metadata = { title: "Bestätigt" };
/** Ziel der Bestaetigung aus dem Assistenten (#020): der Assistent wartet im anderen Tab. */
export default async function BestaetigtSeite() {
await sitzungErforderlich();
return (
<Leerzustand
titel="Anmeldung bestätigt"
text="Die Bestätigung gilt fünf Minuten. Diesen Tab schließen und im Assistenten „Fortsetzen“ wählen."
/>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from "next";
import { Assistent } from "@/components/assistent/Assistent";
import { Breadcrumb } from "@/components/ui/Breadcrumb";
import { Seitenkopf } from "@/components/ui/Seitenkopf";
import { assistentDaten } from "@/server/queries/assistent";
import { bestaetigungFrisch, sitzungErforderlich } from "@/server/auth/sitzungspflicht";
export const metadata: Metadata = { title: "Host-zu-Host-Assistent" };
/**
* Host-zu-Host-Assistent (#020): ein Schluesselpaar fuer genau einen Zweck, Private Key auf
* Host A, Public Key auf Host B, Verbindungstest. Nur Admins – es wird ein Private Key ausgerollt.
*/
export default async function AssistentSeite() {
const { benutzer } = await sitzungErforderlich("privateKeyAusrollen", "/assistent");
const daten = await assistentDaten();
return (
<div className="flex flex-col gap-6 sm:gap-7">
<Seitenkopf
oben={<Breadcrumb pfad={[{ titel: "Übersicht", href: "/" }, { titel: "Host-zu-Host-Assistent" }]} />}
titel="Host-zu-Host-Assistent"
untertitel="Host A soll sich per SSH an Host B anmelden – Schlüssel, Ausrollen und Test in einem Durchgang."
/>
<Assistent daten={daten} bestaetigt={bestaetigungFrisch(benutzer.bestaetigtAm)} />
</div>
);
}
+1 -1
View File
@@ -53,7 +53,7 @@ export default async function Uebersicht() {
aktionen={
<>
<Button icon={<RefreshCw className="size-4" />}>Jetzt abgleichen</Button>
<Button href="/zuweisungen" icon={<Route className="size-4" />}>
<Button href="/assistent" icon={<Route className="size-4" />}>
Host-zu-Host-Assistent
</Button>
<Button