Merge branch 'phase/023-024' (Rotation, Widerruf)
This commit is contained in:
@@ -23,6 +23,7 @@ export const SCHLUESSEL_LISTE_SELECT = {
|
||||
ablaufAm: true,
|
||||
masterKeyVersion: true,
|
||||
passphraseGeschuetzt: true,
|
||||
widerrufEingeleitetAm: true,
|
||||
person: { select: { id: true, name: true } },
|
||||
_count: {
|
||||
select: {
|
||||
@@ -50,12 +51,15 @@ export const SCHLUESSEL_DETAIL_SELECT = {
|
||||
tags: true,
|
||||
ablaufAm: true,
|
||||
zuletztVerwendetAm: true,
|
||||
widerrufEingeleitetAm: true,
|
||||
angelegtAm: true,
|
||||
personId: true,
|
||||
vorlageId: true,
|
||||
person: { select: { id: true, name: true } },
|
||||
vorlage: { select: { id: true, name: true } },
|
||||
erstelltVon: { select: { name: true } },
|
||||
vorgaenger: { select: { id: true, name: true, status: true } },
|
||||
nachfolger: { select: { id: true, name: true, status: true } },
|
||||
publicVerwendungen: {
|
||||
where: { zustand: { not: "ENTFERNT" } },
|
||||
orderBy: { angelegtAm: "asc" },
|
||||
@@ -101,13 +105,34 @@ function alsStatus(wert: string | undefined): SchluesselStatus | undefined {
|
||||
return STATI.find((s) => s === wert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heute 00:00 UTC nach dem Kalender in Berlin – Ablaufdaten liegen als UTC-Mitternacht ihres
|
||||
* Tages in der Datenbank, und `ablaufZustand()` rechnet ebenfalls mit dem Berliner Datum.
|
||||
*/
|
||||
function heute(jetzt = new Date()): Date {
|
||||
const tag = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Berlin" }).format(jetzt);
|
||||
return new Date(`${tag}T00:00:00Z`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Status als Filter (#024): ein aktiver Schluessel mit Ablaufdatum in der Vergangenheit gilt als
|
||||
* abgelaufen, auch wenn niemand den Status von Hand gesetzt hat. Automatisch entfernt wird nichts.
|
||||
*/
|
||||
function statusFilter(status: SchluesselStatus | undefined): Prisma.SchluesselWhereInput {
|
||||
if (status === "AKTIV") return { status, OR: [{ ablaufAm: null }, { ablaufAm: { gte: heute() } }] };
|
||||
if (status === "ABGELAUFEN") {
|
||||
return { OR: [{ status }, { status: "AKTIV", ablaufAm: { lt: heute() } }] };
|
||||
}
|
||||
return status ? { status } : {};
|
||||
}
|
||||
|
||||
export async function schluesselListe(filter: SchluesselFilter = {}) {
|
||||
const q = filter.q?.trim().slice(0, 100);
|
||||
const kategorie = alsKategorie(filter.kategorie);
|
||||
const status = alsStatus(filter.status);
|
||||
const where: Prisma.SchluesselWhereInput = {
|
||||
...(kategorie ? { kategorie } : {}),
|
||||
...(status ? { status } : {}),
|
||||
AND: [statusFilter(status)],
|
||||
...(q
|
||||
? {
|
||||
OR: [
|
||||
@@ -166,3 +191,44 @@ export async function vorlagenAuswahl() {
|
||||
select: { id: true, name: true, standardLaufzeitTage: true },
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Rotationskette (#023)
|
||||
|
||||
export type KettenGlied = { readonly id: string; readonly name: string; readonly status: SchluesselStatus };
|
||||
|
||||
/**
|
||||
* Die ganze Rotationskette eines Schluessels, aeltester zuerst. Begrenzt, damit eine (durch
|
||||
* Fehler entstandene) Schleife die Seite nicht aufhaengt.
|
||||
*/
|
||||
export async function rotationsKette(id: string, maximal = 25): Promise<KettenGlied[]> {
|
||||
const glied = (schluesselId: string) =>
|
||||
prisma.schluessel.findUnique({
|
||||
where: { id: schluesselId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
vorgaengerId: true,
|
||||
nachfolger: { select: { id: true } },
|
||||
},
|
||||
});
|
||||
const start = await glied(id);
|
||||
if (!start) return [];
|
||||
const kette: KettenGlied[] = [{ id: start.id, name: start.name, status: start.status }];
|
||||
const gesehen = new Set([start.id]);
|
||||
for (let davor = start.vorgaengerId; davor && kette.length < maximal && !gesehen.has(davor);) {
|
||||
const g = await glied(davor);
|
||||
if (!g) break;
|
||||
gesehen.add(g.id);
|
||||
kette.unshift({ id: g.id, name: g.name, status: g.status });
|
||||
davor = g.vorgaengerId;
|
||||
}
|
||||
for (let danach = start.nachfolger?.id; danach && kette.length < maximal && !gesehen.has(danach);) {
|
||||
const g = await glied(danach);
|
||||
if (!g) break;
|
||||
gesehen.add(g.id);
|
||||
kette.push({ id: g.id, name: g.name, status: g.status });
|
||||
danach = g.nachfolger?.id;
|
||||
}
|
||||
return kette;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
offeneEntfernungen,
|
||||
widerrufGesperrt,
|
||||
zielkontoKandidaten,
|
||||
type OffeneEntfernung,
|
||||
type ZielkontoKandidat,
|
||||
} from "@/server/services/widerruf";
|
||||
|
||||
/**
|
||||
* Was ein Widerruf bewirkt (#024) – fuer den Bestaetigungsdialog, die Massenwiderruf-Seite und
|
||||
* die Fortsetzen-Ansicht. Nur Namen, Ziele und Pfade; nie Schluesselinhalte.
|
||||
*/
|
||||
export type WiderrufSchluessel = {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly kategorie: "PERSON" | "DIENST" | "DEPLOYMENT";
|
||||
readonly status: "AKTIV" | "ROTIERT" | "WIDERRUFEN" | "ABGELAUFEN";
|
||||
readonly eingeleitet: boolean;
|
||||
readonly person: string | null;
|
||||
readonly gesperrt: string | null;
|
||||
};
|
||||
|
||||
export type WiderrufZiel = {
|
||||
readonly id: string;
|
||||
readonly schluesselId: string;
|
||||
readonly ziel: string;
|
||||
readonly archiviert: boolean;
|
||||
};
|
||||
|
||||
export type WiderrufPrivatZiel = WiderrufZiel & {
|
||||
readonly pfad: string;
|
||||
readonly alias: string | null;
|
||||
readonly beimWiderruf: "LOESCHEN" | "UMBENENNEN";
|
||||
};
|
||||
|
||||
export type WiderrufUebersicht = {
|
||||
readonly schluessel: WiderrufSchluessel[];
|
||||
readonly public: WiderrufZiel[];
|
||||
readonly privat: WiderrufPrivatZiel[];
|
||||
readonly zielkonten: ZielkontoKandidat[];
|
||||
readonly offen: OffeneEntfernung[];
|
||||
};
|
||||
|
||||
export async function widerrufUebersicht(schluesselIds: readonly string[]): Promise<WiderrufUebersicht> {
|
||||
const ids = [...new Set(schluesselIds)];
|
||||
const schluessel = await prisma.schluessel.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
kategorie: true,
|
||||
status: true,
|
||||
widerrufEingeleitetAm: true,
|
||||
person: { select: { name: true } },
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
const [oeffentlich, privat, zielkonten, offen] = await Promise.all([
|
||||
prisma.publicVerwendung.findMany({
|
||||
where: { schluesselId: { in: ids }, zustand: { not: "ENTFERNT" } },
|
||||
select: {
|
||||
id: true,
|
||||
schluesselId: true,
|
||||
zielbenutzer: true,
|
||||
host: { select: { name: true, archiviertAm: true } },
|
||||
hostgruppe: { select: { name: true } },
|
||||
},
|
||||
orderBy: { angelegtAm: "asc" },
|
||||
}),
|
||||
prisma.privateVerwendung.findMany({
|
||||
where: { schluesselId: { in: ids }, zustand: { not: "ENTFERNT" } },
|
||||
select: {
|
||||
id: true,
|
||||
schluesselId: true,
|
||||
zielbenutzer: true,
|
||||
pfad: true,
|
||||
sshConfig: true,
|
||||
beimWiderruf: true,
|
||||
host: { select: { name: true, archiviertAm: true } },
|
||||
},
|
||||
orderBy: { angelegtAm: "asc" },
|
||||
}),
|
||||
zielkontoKandidaten(ids),
|
||||
offeneEntfernungen(ids),
|
||||
]);
|
||||
return {
|
||||
schluessel: schluessel.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
kategorie: s.kategorie,
|
||||
status: s.status,
|
||||
eingeleitet: Boolean(s.widerrufEingeleitetAm),
|
||||
person: s.person?.name ?? null,
|
||||
gesperrt: widerrufGesperrt(s),
|
||||
})),
|
||||
public: oeffentlich.map((v) => ({
|
||||
id: v.id,
|
||||
schluesselId: v.schluesselId,
|
||||
ziel: v.host
|
||||
? `${v.zielbenutzer}@${v.host.name}`
|
||||
: `${v.zielbenutzer}@${v.hostgruppe?.name ?? "?"} (Gruppe)`,
|
||||
archiviert: Boolean(v.host?.archiviertAm),
|
||||
})),
|
||||
privat: privat.map((v) => {
|
||||
const config =
|
||||
v.sshConfig && typeof v.sshConfig === "object" ? (v.sshConfig as { alias?: unknown }) : null;
|
||||
return {
|
||||
id: v.id,
|
||||
schluesselId: v.schluesselId,
|
||||
ziel: `${v.zielbenutzer}@${v.host.name}`,
|
||||
pfad: v.pfad,
|
||||
alias: typeof config?.alias === "string" ? config.alias : null,
|
||||
beimWiderruf: v.beimWiderruf,
|
||||
archiviert: Boolean(v.host.archiviertAm),
|
||||
};
|
||||
}),
|
||||
zielkonten,
|
||||
offen,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user