feat(hosts): Services, Lesemodelle und Aktionen fuer Hosts (#008)
Hosts und Hostgruppen brauchen eine Schreibschicht, bevor Host-Key-Pruefung (#009) und Bootstrap (#010) darauf aufsetzen. Die Regeln (Name, Adresse wie der DB-CHECK, Port, Tags, Anmeldeart) liegen framework-frei in src/lib/hosts.ts, damit Action (Feldmeldungen) und Service (Schutz fuer Seed und Assistenten) dieselben pruefen. Archivieren statt Loeschen haelt Protokollbezuege lesbar; dabei fallen die Gruppenmitgliedschaften weg, damit Zuweisungen an eine Gruppe nie einen archivierten Host treffen. Jede Aenderung landet mit den geaenderten Feldern im Protokoll, Eindeutigkeitsverletzungen kommen als deutsche HostFehler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { hostKeyZusammenfassen, hostStatus } from "@/lib/hosts";
|
||||
|
||||
/**
|
||||
* Lesemodelle fuer Hosts und Hostgruppen (#008).
|
||||
*
|
||||
* Alles hier darf jede angemeldete Rolle sehen (Recht `lesen`); die Seiten pruefen das per
|
||||
* `sitzungErforderlich()`. Archivierte Hosts erscheinen in keiner Liste, nur ueber ihre
|
||||
* Detailadresse – fuer Verweise aus dem Protokoll.
|
||||
*/
|
||||
|
||||
const OFFEN = { behobenAm: null } satisfies Prisma.AbweichungWhereInput;
|
||||
|
||||
export type HostFilter = { readonly q?: string; readonly gruppeId?: string };
|
||||
|
||||
export async function hostListe(filter: HostFilter = {}) {
|
||||
const q = filter.q?.trim().slice(0, 100);
|
||||
const where: Prisma.HostWhereInput = {
|
||||
archiviertAm: null,
|
||||
...(filter.gruppeId ? { gruppen: { some: { hostgruppeId: filter.gruppeId } } } : {}),
|
||||
...(q
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: q, mode: "insensitive" } },
|
||||
{ adresse: { contains: q, mode: "insensitive" } },
|
||||
{ tags: { has: q.toLowerCase() } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const hosts = await prisma.host.findMany({
|
||||
where,
|
||||
orderBy: { name: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
adresse: true,
|
||||
port: true,
|
||||
tags: true,
|
||||
erreichbar: true,
|
||||
driftZustand: true,
|
||||
letzterAbgleichAm: true,
|
||||
kunde: { select: { name: true } },
|
||||
gruppen: { select: { hostgruppe: { select: { id: true, name: true } } } },
|
||||
hostSchluessel: {
|
||||
where: { status: { in: ["VERTRAUT", "AUSSTEHEND"] } },
|
||||
select: { status: true },
|
||||
},
|
||||
_count: { select: { abweichungen: { where: OFFEN } } },
|
||||
},
|
||||
});
|
||||
return hosts.map((h) => ({
|
||||
id: h.id,
|
||||
name: h.name,
|
||||
adresse: h.adresse,
|
||||
port: h.port,
|
||||
tags: h.tags,
|
||||
kunde: h.kunde?.name ?? null,
|
||||
gruppen: h.gruppen.map((g) => g.hostgruppe).sort((a, b) => a.name.localeCompare(b.name, "de")),
|
||||
letzterAbgleichAm: h.letzterAbgleichAm,
|
||||
offeneAbweichungen: h._count.abweichungen,
|
||||
status: hostStatus({
|
||||
erreichbar: h.erreichbar,
|
||||
driftZustand: h.driftZustand,
|
||||
offeneAbweichungen: h._count.abweichungen,
|
||||
}),
|
||||
hostKey: hostKeyZusammenfassen(h.hostSchluessel.map((s) => s.status)),
|
||||
}));
|
||||
}
|
||||
|
||||
export type HostListenEintrag = Awaited<ReturnType<typeof hostListe>>[number];
|
||||
|
||||
export async function hostDetail(id: string) {
|
||||
const host = await prisma.host.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
adresse: true,
|
||||
port: true,
|
||||
beschreibung: true,
|
||||
tags: true,
|
||||
anmeldeart: true,
|
||||
bootstrapAm: true,
|
||||
erreichbar: true,
|
||||
zuletztGeprueftAm: true,
|
||||
letzterAbgleichAm: true,
|
||||
driftZustand: true,
|
||||
sshdBefund: true,
|
||||
archiviertAm: true,
|
||||
angelegtAm: true,
|
||||
kunde: { select: { id: true, name: true } },
|
||||
gruppen: { select: { hostgruppe: { select: { id: true, name: true } } } },
|
||||
hostSchluessel: {
|
||||
orderBy: [{ status: "asc" }, { algorithmus: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
algorithmus: true,
|
||||
fingerprint: true,
|
||||
status: true,
|
||||
erstmalsGesehen: true,
|
||||
zuletztGesehen: true,
|
||||
entschiedenAm: true,
|
||||
},
|
||||
},
|
||||
zielkonten: {
|
||||
where: { entferntAm: null },
|
||||
orderBy: { benutzername: "asc" },
|
||||
select: { id: true, benutzername: true, sudoModus: true, vonSshkvAngelegt: true },
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
abweichungen: { where: OFFEN },
|
||||
publicVerwendungen: { where: { zustand: { not: "ENTFERNT" } } },
|
||||
privateVerwendungen: { where: { zustand: { not: "ENTFERNT" } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!host) return null;
|
||||
const { _count, gruppen, ...rest } = host;
|
||||
return {
|
||||
...rest,
|
||||
gruppen: gruppen.map((g) => g.hostgruppe).sort((a, b) => a.name.localeCompare(b.name, "de")),
|
||||
offeneAbweichungen: _count.abweichungen,
|
||||
publicVerwendungen: _count.publicVerwendungen,
|
||||
privateVerwendungen: _count.privateVerwendungen,
|
||||
status: hostStatus({
|
||||
erreichbar: host.erreichbar,
|
||||
driftZustand: host.driftZustand,
|
||||
offeneAbweichungen: _count.abweichungen,
|
||||
}),
|
||||
hostKey: hostKeyZusammenfassen(host.hostSchluessel.map((s) => s.status)),
|
||||
};
|
||||
}
|
||||
|
||||
export type HostDetail = NonNullable<Awaited<ReturnType<typeof hostDetail>>>;
|
||||
|
||||
/** Fuer das Formular „Bearbeiten“: genau die Felder, die das Formular kennt. */
|
||||
export async function hostFormularWerte(id: string) {
|
||||
const host = await prisma.host.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
adresse: true,
|
||||
port: true,
|
||||
tags: true,
|
||||
beschreibung: true,
|
||||
anmeldeart: true,
|
||||
archiviertAm: true,
|
||||
kunde: { select: { name: true } },
|
||||
gruppen: { select: { hostgruppeId: true } },
|
||||
},
|
||||
});
|
||||
if (!host) return null;
|
||||
return {
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
adresse: host.adresse,
|
||||
port: host.port,
|
||||
tags: host.tags,
|
||||
beschreibung: host.beschreibung ?? "",
|
||||
anmeldeart: host.anmeldeart,
|
||||
kundeName: host.kunde?.name ?? "",
|
||||
gruppenIds: host.gruppen.map((g) => g.hostgruppeId),
|
||||
archiviert: host.archiviertAm !== null,
|
||||
};
|
||||
}
|
||||
|
||||
export type HostFormularWerte = NonNullable<Awaited<ReturnType<typeof hostFormularWerte>>>;
|
||||
|
||||
/** Auswahllisten fuer Formulare. */
|
||||
export async function gruppenAuswahl() {
|
||||
return prisma.hostgruppe.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } });
|
||||
}
|
||||
|
||||
export async function hostAuswahl() {
|
||||
return prisma.host.findMany({
|
||||
where: { archiviertAm: null },
|
||||
orderBy: { name: "asc" },
|
||||
select: { id: true, name: true, adresse: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function kundenNamen() {
|
||||
const kunden = await prisma.kunde.findMany({ orderBy: { name: "asc" }, select: { name: true } });
|
||||
return kunden.map((k) => k.name);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Hostgruppen
|
||||
|
||||
export async function gruppenListe() {
|
||||
const gruppen = await prisma.hostgruppe.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
beschreibung: true,
|
||||
_count: { select: { mitglieder: { where: { host: { archiviertAm: null } } } } },
|
||||
},
|
||||
});
|
||||
return gruppen.map(({ _count, ...g }) => ({ ...g, mitglieder: _count.mitglieder }));
|
||||
}
|
||||
|
||||
export async function gruppeDetail(id: string) {
|
||||
const gruppe = await prisma.hostgruppe.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
beschreibung: true,
|
||||
angelegtAm: true,
|
||||
mitglieder: {
|
||||
where: { host: { archiviertAm: null } },
|
||||
orderBy: { host: { name: "asc" } },
|
||||
select: {
|
||||
host: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
adresse: true,
|
||||
port: true,
|
||||
erreichbar: true,
|
||||
driftZustand: true,
|
||||
_count: { select: { abweichungen: { where: OFFEN } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!gruppe) return null;
|
||||
return {
|
||||
id: gruppe.id,
|
||||
name: gruppe.name,
|
||||
beschreibung: gruppe.beschreibung,
|
||||
angelegtAm: gruppe.angelegtAm,
|
||||
mitglieder: gruppe.mitglieder.map(({ host: { _count, ...h } }) => ({
|
||||
id: h.id,
|
||||
name: h.name,
|
||||
adresse: h.adresse,
|
||||
port: h.port,
|
||||
status: hostStatus({
|
||||
erreichbar: h.erreichbar,
|
||||
driftZustand: h.driftZustand,
|
||||
offeneAbweichungen: _count.abweichungen,
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user