Merge branch 'phase/032-nacharbeiten'
This commit is contained in:
@@ -23,7 +23,8 @@ import { widerrufUebersicht } from "@/server/queries/widerruf";
|
||||
import { WiderrufAblauf } from "@/components/widerruf/WiderrufAblauf";
|
||||
import { WiderrufKnopf } from "@/components/widerruf/WiderrufKnopf";
|
||||
import { ablaufZustand } from "@/lib/schluessel";
|
||||
import { rotationGesperrt } from "@/server/services/rotation";
|
||||
import { RotationAbbrechen } from "@/components/rotation/RotationAbbrechen";
|
||||
import { rotationAbgebrochen, rotationGesperrt } from "@/server/services/rotation";
|
||||
import { PrivateAusrollenAktion } from "./PrivateAusrollenAktion";
|
||||
import { PrivateKeyAnzeigen, PrivateKeyLoeschen } from "./PrivateKeyAktionen";
|
||||
import { PublicAusrollenAktion } from "./PublicAusrollenAktion";
|
||||
@@ -216,9 +217,17 @@ export default async function SchluesselSeite({
|
||||
const jetzt = new Date();
|
||||
const abgelaufen = s.status === "ABGELAUFEN" || ablaufZustand(s.ablaufAm, jetzt).art === "abgelaufen";
|
||||
const verwaltenRecht = darf(benutzer.rolle, "schluesselVerwalten");
|
||||
// Auf dem Nachfolger: die laufende Rotation gehoert zum Vorgaenger.
|
||||
const rotationDesVorgaengers =
|
||||
s.vorgaenger && (s.vorgaenger.status === "AKTIV" || s.vorgaenger.status === "ABGELAUFEN")
|
||||
? s.vorgaenger
|
||||
: null;
|
||||
const widerrufbar =
|
||||
!imWiderruf &&
|
||||
!(s.nachfolger?.status === "AKTIV") &&
|
||||
// Ein Nachfolger mitten in der Rotation wird ueber „Rotation abbrechen“ widerrufen (#032) –
|
||||
// so steht der Abbruch im Protokoll und die Rotationsseite kennt den Stand.
|
||||
!rotationDesVorgaengers &&
|
||||
s.kategorie !== "DEPLOYMENT" &&
|
||||
(s.status === "AKTIV" || s.status === "ABGELAUFEN") &&
|
||||
rechte.public &&
|
||||
@@ -235,13 +244,19 @@ export default async function SchluesselSeite({
|
||||
privatZuweisungen: aktivePrivat,
|
||||
});
|
||||
const rotationLaeuft = s.nachfolger?.status === "AKTIV" && s.status !== "ROTIERT";
|
||||
const abgebrocheneRotation = s.status !== "ROTIERT" && rotationAbgebrochen(s.nachfolger);
|
||||
const darfRotieren = verwaltenRecht && rechte.public && (aktivePrivat === 0 || rechte.privat);
|
||||
const rotierbar = darfRotieren && (rotationLaeuft || (!rotationsSperre && !imWiderruf));
|
||||
// Auf dem Nachfolger: die laufende Rotation gehoert zum Vorgaenger.
|
||||
const rotationDesVorgaengers =
|
||||
s.vorgaenger && (s.vorgaenger.status === "AKTIV" || s.vorgaenger.status === "ABGELAUFEN")
|
||||
? s.vorgaenger
|
||||
: null;
|
||||
// Nach einem abgeschlossenen Abbruch (#032) bleibt der Nachfolger in der Kette; ein zweiter
|
||||
// passt nicht hinein (vorgaengerId ist eindeutig) – deshalb kein „Rotieren“ mehr.
|
||||
const rotierbar = darfRotieren && (rotationLaeuft || (!rotationsSperre && !imWiderruf && !s.nachfolger));
|
||||
// Abbrechen auf dem Nachfolger: gleiche Rechte wie die Rotation, Private Keys nur mit Recht.
|
||||
const darfAbbrechen =
|
||||
Boolean(rotationDesVorgaengers && !rotationDesVorgaengers.widerrufEingeleitetAm) &&
|
||||
!imWiderruf &&
|
||||
s.status === "AKTIV" &&
|
||||
verwaltenRecht &&
|
||||
rechte.public &&
|
||||
(s.privateVerwendungen.length === 0 || rechte.privat);
|
||||
const vorauswahl = (
|
||||
Array.isArray(zielkontenParameter) ? zielkontenParameter.join(",") : (zielkontenParameter ?? "")
|
||||
)
|
||||
@@ -308,20 +323,41 @@ export default async function SchluesselSeite({
|
||||
}
|
||||
/>
|
||||
|
||||
{rotationDesVorgaengers && (
|
||||
<Karte titel="Rotation läuft" className="border-accent">
|
||||
<p className="text-muted text-[14px]">
|
||||
Dieser Schlüssel löst „{rotationDesVorgaengers.name}“ ab. Die Rotation ist erst fertig, wenn der
|
||||
neue Schlüssel ausgerollt und getestet und der alte entfernt ist.{" "}
|
||||
<Link
|
||||
href={`/schluessel/${rotationDesVorgaengers.id}/rotieren`}
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
Rotation fortsetzen
|
||||
</Link>
|
||||
</p>
|
||||
</Karte>
|
||||
)}
|
||||
{rotationDesVorgaengers &&
|
||||
(imWiderruf || s.status === "WIDERRUFEN" ? (
|
||||
<Karte titel="Rotation abgebrochen">
|
||||
<p className="text-muted text-[14px]">
|
||||
Dieser Schlüssel sollte „{rotationDesVorgaengers.name}“ ablösen; die Rotation wurde abgebrochen.
|
||||
{imWiderruf ? " Er wird gerade entfernt; " : " Er ist von allen Hosts entfernt; "}
|
||||
<Link href={`/schluessel/${rotationDesVorgaengers.id}`} className="text-accent hover:underline">
|
||||
{rotationDesVorgaengers.name}
|
||||
</Link>{" "}
|
||||
bleibt aktiv.
|
||||
</p>
|
||||
</Karte>
|
||||
) : (
|
||||
<Karte titel="Rotation läuft" className="border-accent">
|
||||
<p className="text-muted text-[14px]">
|
||||
Dieser Schlüssel löst „{rotationDesVorgaengers.name}“ ab. Die Rotation ist erst fertig, wenn der
|
||||
neue Schlüssel ausgerollt und getestet und der alte entfernt ist.{" "}
|
||||
<Link
|
||||
href={`/schluessel/${rotationDesVorgaengers.id}/rotieren`}
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
Rotation fortsetzen
|
||||
</Link>
|
||||
</p>
|
||||
{darfAbbrechen && (
|
||||
<div className="mt-4">
|
||||
<RotationAbbrechen
|
||||
altId={rotationDesVorgaengers.id}
|
||||
altName={rotationDesVorgaengers.name}
|
||||
neuName={s.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Karte>
|
||||
))}
|
||||
|
||||
{imWiderruf && widerruf && (
|
||||
<Karte
|
||||
@@ -336,7 +372,7 @@ export default async function SchluesselSeite({
|
||||
vorauswahl={vorauswahl}
|
||||
ruecksprung={`/schluessel/${s.id}`}
|
||||
darfPrivat={rechte.privat}
|
||||
ohneZielkonten={rotationLaeuft}
|
||||
ohneZielkonten={rotationLaeuft || Boolean(rotationDesVorgaengers)}
|
||||
/>
|
||||
</Karte>
|
||||
)}
|
||||
@@ -367,7 +403,8 @@ export default async function SchluesselSeite({
|
||||
<Karte titel={s.status === "WIDERRUFEN" ? "Widerrufen" : "Rotiert"}>
|
||||
<p className="text-muted text-[14px]">
|
||||
Von allen Hosts entfernt; der Schlüssel lässt sich nicht mehr zuweisen.
|
||||
{s.nachfolger && (
|
||||
{/* Ein widerrufener Nachfolger (abgebrochene Rotation, #032) ersetzt nichts. */}
|
||||
{s.nachfolger && s.status === "ROTIERT" && (
|
||||
<>
|
||||
{" "}
|
||||
Ersetzt durch{" "}
|
||||
@@ -457,6 +494,10 @@ export default async function SchluesselSeite({
|
||||
!imWiderruf &&
|
||||
(s.publicVerwendungen.length > 0 || (rechte.privat && s.privateVerwendungen.length > 0)) && (
|
||||
<Karte
|
||||
// Sprungziel fuer „Zum Dry-Run“ aus der Zuweisungsmatrix; scroll-mt haelt die Karte
|
||||
// unter der festen Kopfleiste der schmalen Ansicht (#032).
|
||||
id="dry-run"
|
||||
className="scroll-mt-20"
|
||||
titel="Dry-Run-Vorschau"
|
||||
untertitel="Erst prüfen, dann anwenden – auf den Hosts ändert sich vorher nichts"
|
||||
>
|
||||
@@ -498,6 +539,14 @@ export default async function SchluesselSeite({
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
{abgebrocheneRotation && s.nachfolger && (
|
||||
<p className="text-muted mt-3 text-[14px]">
|
||||
Die Rotation zu „{s.nachfolger.name}“ wurde abgebrochen
|
||||
{s.nachfolger.status === "WIDERRUFEN" ? "" : " – der neue Schlüssel wird gerade entfernt"};
|
||||
dieser Schlüssel bleibt aktiv. Ein zweiter Nachfolger ist nicht vorgesehen: für eine neue
|
||||
Rotation einen neuen Schlüssel anlegen, genauso zuweisen und diesen danach widerrufen.
|
||||
</p>
|
||||
)}
|
||||
{rotationsSperre?.neuerPublicKeyNoetig && !imWiderruf && (
|
||||
<p className={`text-muted text-[14px] ${kette.length > 1 ? "mt-3" : ""}`}>
|
||||
{rotationsSperre.grund}
|
||||
|
||||
@@ -59,9 +59,14 @@ export default async function RotierenSeite({ params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
const widerruf =
|
||||
stand.neu && alt.widerrufEingeleitetAm && alt.status !== "ROTIERT"
|
||||
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">
|
||||
@@ -74,6 +79,7 @@ export default async function RotierenSeite({ params }: { params: Promise<{ id:
|
||||
neuOffen: stand.neu ? stand.neuOffen : 0,
|
||||
tests: stand.neu ? stand.tests : [],
|
||||
widerruf,
|
||||
abbruch,
|
||||
darfPrivat: darf(benutzer.rolle, "privateKeyAusrollen"),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Leerzustand } from "@/components/ui/Leerzustand";
|
||||
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||||
import { WiderrufAblauf } from "@/components/widerruf/WiderrufAblauf";
|
||||
import { WiderrufFormular } from "@/components/widerruf/WiderrufFormular";
|
||||
import { idsAusSuchparameter } from "@/lib/schluessel";
|
||||
import { darf } from "@/server/auth/rechte";
|
||||
import { bestaetigungFrisch, sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { widerrufUebersicht } from "@/server/queries/widerruf";
|
||||
@@ -17,14 +18,6 @@ export const metadata: Metadata = { title: "Massenwiderruf" };
|
||||
|
||||
type Suchparameter = Promise<{ id?: string | string[]; zielkonten?: string | string[] }>;
|
||||
|
||||
function liste(wert: string | string[] | undefined): string[] {
|
||||
const roh = Array.isArray(wert) ? wert : wert ? [wert] : [];
|
||||
return [...new Set(roh.flatMap((w) => w.split(",")).filter((w) => /^[a-z0-9]{1,64}$/i.test(w)))].slice(
|
||||
0,
|
||||
200,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Massenwiderruf (#024): mehrere Schluessel – etwa alle einer Person nach Projektende – in einem
|
||||
* Durchgang mit gemeinsamem Dry-Run. Nur Admins; das Vormerken verlangt eine frische
|
||||
@@ -32,7 +25,7 @@ function liste(wert: string | string[] | undefined): string[] {
|
||||
*/
|
||||
export default async function MassenwiderrufSeite({ searchParams }: { searchParams: Suchparameter }) {
|
||||
const parameter = await searchParams;
|
||||
const ids = liste(parameter.id);
|
||||
const ids = idsAusSuchparameter(parameter.id);
|
||||
const selbst = `/schluessel/widerruf?${ids.map((id) => `id=${id}`).join("&")}`;
|
||||
const { benutzer } = await sitzungErforderlich("massenwiderruf", selbst);
|
||||
|
||||
@@ -61,7 +54,7 @@ export default async function MassenwiderrufSeite({ searchParams }: { searchPara
|
||||
const offeneStati = (s: { status: string }) => s.status === "AKTIV" || s.status === "ABGELAUFEN";
|
||||
const nochNichtEingeleitet = uebersicht.schluessel.filter((s) => offeneStati(s) && !s.eingeleitet);
|
||||
const laufend = uebersicht.schluessel.filter((s) => offeneStati(s) && s.eingeleitet);
|
||||
const vorauswahl = liste(parameter.zielkonten);
|
||||
const vorauswahl = idsAusSuchparameter(parameter.zielkonten);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-7">
|
||||
|
||||
Reference in New Issue
Block a user