import { Betriebsmodus } from "@/generated/prisma/enums"; import { prisma } from "@/lib/db"; import { istAlsVariableErlaubt } from "@/lib/platzhalter"; import { betriebsmodus } from "@/server/mandant"; /** * Die Werte hinter den Platzhaltern (#012). * * Diese Datei traegt zusammen, was ein Lauf ueber sich weiss: Geraet, * Mitarbeiter, Abteilung, Kunde, Vorlagenvariablen. Das **Ersetzen** selbst * steht framework-frei in `src/lib/platzhalter-werte.ts`, die Liste der * eingebauten Namen in `src/lib/platzhalter.ts`. Drei Dateien, drei Aufgaben — * und nur diese eine kennt die Datenbank. * * Eine Regel zieht sich durch: **ein fehlender Wert ist leer, kein Fehler.** Ein * Lauf ohne Mitarbeiter ist gueltig (Server, Firewall, Patchday), und ein * Befehl mit einem leeren `-Department` faellt beim Lesen auf. Abzubrechen * hiesse, dem Techniker die Arbeit zu verweigern, weil ein Feld fehlt, das er * vielleicht gar nicht braucht. */ export type PlatzhalterWerte = Readonly>; /** * Der Eltern-Container eines `dn`. * * Aus `CN=Maria Huber,OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local` wird * `OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local` — alles nach dem ersten Komma. * Das ist der Ort, an dem das Konto liegt, und genau den braucht ein * `New-ADUser -Path`. * * Ein Komma **innerhalb** eines Namens ist im DN maskiert (`CN=Huber\, Maria`), * deshalb wird ein Komma hinter einem Backslash nicht als Trenner gelesen. */ export function ouAusDn(dn: string): string { /* * Gezaehlt statt geraten: `(?, schluessel: string, wert: string): void { if (wert !== "") ziel[schluessel] = wert; } export type LaufKontext = { readonly laufId: string; /** Wer den Lauf gerade abarbeitet – speist `{{techniker}}`. */ readonly technikerName?: string | null; /** Fuer `{{datum}}`; hereingereicht, damit Tests nicht an der Uhr haengen. */ readonly jetzt?: Date; }; const DATUM = new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" }); /** * Traegt alle Werte eines Laufs zusammen. * * Die Reihenfolge ist Absicht: erst Geraet und Lauf, dann Mitarbeiter, dann * Abteilung, zuletzt die Vorlagenvariablen. Wer weiter hinten steht, gewinnt — * eine Variable, die der Redakteur `hostname` nennt, ueberschreibt also den * Hostnamen des Geraets. Das ist richtig so: sie ist die ausdrueckliche Angabe * dieses Laufs, der Geraetewert nur die Voreinstellung. * * `{{ou}}` ist der einzige Wert mit zwei Quellen: der Eltern-Container des `dn` * gewinnt, weil er sagt, wo das Konto **wirklich** liegt; die Ziel-OU der * Abteilung ist die Absicht fuer ein Konto, das es noch nicht gibt. */ export async function platzhalterWerteDesLaufs(kontext: LaufKontext): Promise { const lauf = await prisma.lauf.findUnique({ where: { id: kontext.laufId }, select: { ticket: true, variablen: true, mitarbeiterName: true, kunde: { select: { name: true, kuerzel: true } }, geraet: { select: { hostname: true, bezeichnung: true, seriennummer: true, inventarnummer: true }, }, mitarbeiter: { select: { anzeigename: true, vorname: true, nachname: true, upn: true, sam: true, mail: true, abteilung: true, titel: true, dn: true, quelleId: true, vorgesetzterExterneId: true, }, }, abteilung: { select: { name: true, zielOu: true, standardGruppen: true } }, }, }); if (!lauf) return {}; const werte: Record = {}; const jetzt = kontext.jetzt ?? new Date(); // --- Geraet und Lauf --- setzen(werte, "hostname", alsText(lauf.geraet?.hostname ?? lauf.geraet?.bezeichnung)); setzen(werte, "seriennummer", alsText(lauf.geraet?.seriennummer)); setzen(werte, "inventarnummer", alsText(lauf.geraet?.inventarnummer)); setzen(werte, "ticket", alsText(lauf.ticket)); setzen(werte, "datum", DATUM.format(jetzt)); setzen(werte, "techniker", alsText(kontext.technikerName)); /* * Kunde und Kuerzel gibt es nur im Modus Dienstleistung. Im Modus Intern ist * die eigene Organisation fuer den Benutzer unsichtbar (#008) — ihren Namen in * einen Befehl zu schreiben waere eine Auskunft ueber eine Ebene, die es dort * gar nicht gibt. Die Platzhalter bleiben deshalb leer, nicht unbekannt. */ if ((await betriebsmodus()) === Betriebsmodus.DIENSTLEISTUNG) { setzen(werte, "kunde", alsText(lauf.kunde.name)); setzen(werte, "kundenkuerzel", alsText(lauf.kunde.kuerzel)); } // --- Mitarbeiter --- const person = lauf.mitarbeiter; if (person) { setzen(werte, "mitarbeiter", alsText(person.anzeigename)); setzen(werte, "benutzer", alsText(person.anzeigename)); setzen(werte, "vorname", alsText(person.vorname)); setzen(werte, "nachname", alsText(person.nachname)); setzen(werte, "upn", alsText(person.upn)); setzen(werte, "sam", alsText(person.sam)); setzen(werte, "mail", alsText(person.mail)); setzen(werte, "abteilung", alsText(person.abteilung)); setzen(werte, "titel", alsText(person.titel)); setzen(werte, "dn", alsText(person.dn)); if (person.dn) setzen(werte, "ou", ouAusDn(person.dn)); const vorgesetzter = await vorgesetztenLesen(person); if (vorgesetzter) { setzen(werte, "vorgesetzter", alsText(vorgesetzter.anzeigename)); setzen(werte, "vorgesetzter_sam", alsText(vorgesetzter.sam)); } } else { // Beim Onboarding steht die Person noch nicht im Verzeichnis; dann traegt // der Lauf ihren Namen als Freitext. setzen(werte, "mitarbeiter", alsText(lauf.mitarbeiterName)); setzen(werte, "benutzer", alsText(lauf.mitarbeiterName)); } // --- Abteilung --- if (lauf.abteilung) { setzen(werte, "abteilung", alsText(lauf.abteilung.name)); setzen(werte, "gruppen", lauf.abteilung.standardGruppen.join(", ")); // Nur, wenn der Mitarbeiter keine bessere Auskunft hatte. if (werte["ou"] === undefined) setzen(werte, "ou", alsText(lauf.abteilung.zielOu)); } /* * --- Aus den Einstellungen --- * * Die Ziel-OU fuer deaktivierte Konten steht global in `Einstellung` und wird * im Offboarding gebraucht. Sie stand dort schon, wurde aber von keinem * Platzhalter gelesen: `Move-ADObject -TargetPath "{{ou_gesperrt}}"` blieb leer, * obwohl die Einstellungsseite „Wird beim Offboarding vorbelegt“ verspricht * (Reviewbefund zu #017). Eine Variable gleichen Namens gewinnt weiter — sie * steht weiter unten. */ const einstellung = await prisma.einstellung.findUnique({ where: { id: "global" }, select: { gesperrteOu: true }, }); setzen(werte, "ou_gesperrt", alsText(einstellung?.gesperrteOu)); /* * --- Vorlagenvariablen --- * * Sie kommen zuletzt und gewinnen — aber nur, wo sie duerfen. Eine Variable * `techniker` oder `datum` liesse sonst einen Befehl etwas unterschreiben, das * nicht stimmt, und eine Variable `kunde` haette im Modus INTERN die * Kundenebene zurueck in den Befehl geholt, die es dort nicht gibt. * Gesperrt sind genau fuenf Namen (siehe `GESPERRTE_VARIABLENNAMEN`); alles * andere darf eine Vorlage belegen — beim Onboarding gibt es die Person im * Verzeichnis noch nicht, und `upn`, `sam` und `mail` kommen dann aus dem Lauf. */ for (const [schluessel, wert] of Object.entries(variablenLesen(lauf.variablen))) { if (!istAlsVariableErlaubt(schluessel)) continue; setzen(werte, schluessel, wert); } return werte; } /** * Der Vorgesetzte, ueber `vorgesetzterExterneId` **derselben** Verzeichnisquelle. * * Die Kennung ist nur innerhalb einer Quelle eindeutig; ohne diese * Einschraenkung zoege ein zweites Verzeichnis fremde Treffer herbei. */ async function vorgesetztenLesen(person: { readonly quelleId: string | null; readonly vorgesetzterExterneId: string | null; }): Promise<{ anzeigename: string; sam: string | null } | null> { if (!person.vorgesetzterExterneId || !person.quelleId) return null; return prisma.mitarbeiter.findFirst({ where: { quelleId: person.quelleId, externeId: person.vorgesetzterExterneId }, select: { anzeigename: true, sam: true }, }); } /** `Lauf.variablen` ist ein Json-Feld und kann alles enthalten. */ function variablenLesen(roh: unknown): Record { if (typeof roh !== "object" || roh === null || Array.isArray(roh)) return {}; const werte: Record = {}; for (const [schluessel, wert] of Object.entries(roh as Record)) { // Zahlen und Wahrheitswerte kommen aus den Variablentypen ZAHL und HAKEN; // alles andere waere ein verunglueckter Eintrag und bleibt draussen. if (typeof wert === "string") werte[schluessel] = wert; else if (typeof wert === "number" || typeof wert === "boolean") werte[schluessel] = String(wert); } return werte; }