import { hash } from "@node-rs/argon2"; import { Betriebsmodus, Rolle } from "../../src/generated/prisma/enums.js"; import type { PrismaClient } from "../../src/generated/prisma/client.js"; /** * Der Grundbestand, ohne den die Anwendung nicht bedienbar waere (#006, #017): * Betriebsmodus samt Namensregeln, die eigene Organisation und ein Administrator. * * Steht neben dem Seed-Skript und nicht darin, damit Tests diese Schritte einzeln * aufrufen koennen. `prisma/seed.ts` fuehrt beim Import den ganzen Seed aus — ein * Test, der es importierte, schriebe in die Datenbank aus `DATABASE_URL`. * * Die Funktionen nehmen den Client als Argument. Sie schreiben nur, was fehlt. */ /** Akzeptiert den im Auftrag dokumentierten Wert SERVICE als Alias. */ export function modusLesen(): Betriebsmodus { const roh = (process.env["SEED_MODE"] ?? "INTERN").trim().toUpperCase(); if (roh === "DIENSTLEISTUNG" || roh === "SERVICE") return Betriebsmodus.DIENSTLEISTUNG; if (roh === "INTERN") return Betriebsmodus.INTERN; throw new Error(`SEED_MODE kennt nur INTERN und DIENSTLEISTUNG (Alias SERVICE), nicht "${roh}".`); } /** * Namensregeln, die zu den Beispieldaten passen. * * `{v}.{nachname}@firma.example` ergibt fuer Maria Huber genau * `m.huber@firma.example` und `mhuber` — dieselben Werte, die auch im * Beispielbestand stehen. Ohne diese Regeln antwortet „Namen aus den Regeln * vorschlagen“ im Onboarding mit „Für diesen Kunden sind keine Namensregeln * hinterlegt“, und das Abnahmekriterium „Vorname und Nachname eingeben → UPN, * sam und Mail werden vorgeschlagen“ liesse sich nach einem frischen Seed nicht * vorfuehren (Reviewbefund zu #017). * * Die Hostname-Regel haengt am Modus: im Modus DIENSTLEISTUNG traegt der Name * das Kundenkuerzel, wie der Auftrag es vorgibt (`{kundenkuerzel}-PC-{nnn}`). */ export function namensregelnFuerModus(modus: Betriebsmodus): Record { return { upn: "{v}.{nachname}@firma.example", mail: "{v}.{nachname}@firma.example", sam: "{v}{nachname}", hostname: modus === Betriebsmodus.DIENSTLEISTUNG ? "{kundenkuerzel}-PC-{nnn}" : "PC-{nnn}", }; } /** Ziel-OU fuer deaktivierte Konten – Vorbelegung von `{{ou_gesperrt}}` im Offboarding. */ export const GESPERRTE_OU = "OU=Gesperrt,OU=Benutzer,DC=firma,DC=local"; export async function einstellungAnlegen(db: PrismaClient, modus: Betriebsmodus): Promise { // Nur anlegen, nicht ueberschreiben: ein spaeterer Seed-Lauf darf einen im // Betrieb umgeschalteten Modus nicht zuruecksetzen. const vorhanden = await db.einstellung.findUnique({ where: { id: "global" } }); if (!vorhanden) { await db.einstellung.create({ data: { id: "global", modus, namensregeln: namensregelnFuerModus(modus), gesperrteOu: GESPERRTE_OU, }, }); console.log(` Einstellung: angelegt, Modus ${modus}, Namensregeln und Sperr-OU gesetzt`); return; } /* * Die Zeile steht schon. Ergaenzt wird nur, was **leer** ist: eine gepflegte * Namensregel oder eine eigene Sperr-OU zurueckzusetzen waere eine Aenderung an * Betriebsdaten, und dafuer ist der Seed nicht da. */ const ohneRegeln = typeof vorhanden.namensregeln !== "object" || vorhanden.namensregeln === null || Object.keys(vorhanden.namensregeln as Record).length === 0; const ohneSperrOu = (vorhanden.gesperrteOu ?? "").trim() === ""; if (ohneRegeln || ohneSperrOu) { await db.einstellung.update({ where: { id: "global" }, data: { ...(ohneRegeln ? { namensregeln: namensregelnFuerModus(vorhanden.modus) } : {}), ...(ohneSperrOu ? { gesperrteOu: GESPERRTE_OU } : {}), }, }); } const ergaenzt = [ohneRegeln ? "Namensregeln" : null, ohneSperrOu ? "Sperr-OU" : null].filter( (teil) => teil !== null, ); console.log( ` Einstellung: vorhanden, Modus ${vorhanden.modus} bleibt` + (ergaenzt.length > 0 ? ` (${ergaenzt.join(" und ")} ergänzt)` : ""), ); } export async function eigeneOrganisationAnlegen(db: PrismaClient): Promise { const name = process.env["SEED_ORG_NAME"]?.trim() || "Eigene Organisation"; const kuerzel = (process.env["SEED_ORG_KUERZEL"]?.trim() || "EIGEN").toUpperCase(); if (!/^[A-Z0-9]{2,6}$/.test(kuerzel)) { throw new Error(`SEED_ORG_KUERZEL muss 2 bis 6 Grossbuchstaben oder Ziffern sein, nicht "${kuerzel}".`); } // Bewusst nicht aktualisieren: wer Name oder Kuerzel im Betrieb geaendert hat, // soll das nicht beim naechsten Seed-Lauf zurueckbekommen. Geaenderte Werte in // der Umgebung bleiben deshalb wirkungslos, sobald die Zeile existiert. const vorhanden = await db.kunde.findFirst({ where: { istEigeneOrganisation: true } }); if (vorhanden) { console.log(` Eigene Organisation: vorhanden (${vorhanden.name})`); return vorhanden.id; } const kunde = await db.kunde.create({ data: { name, kuerzel, istEigeneOrganisation: true }, }); console.log(` Eigene Organisation: angelegt (${kunde.name}, ${kunde.kuerzel})`); return kunde.id; } export async function adminAnlegen(db: PrismaClient): Promise { const email = process.env["SEED_ADMIN_EMAIL"]?.trim().toLowerCase(); const passwort = process.env["SEED_ADMIN_PASSWORD"]; if (!email || !passwort) { console.log(" Administrator: uebersprungen (SEED_ADMIN_EMAIL oder SEED_ADMIN_PASSWORD fehlt)"); return; } // Gesucht wird ueber die Adresse: eine geaenderte SEED_ADMIN_EMAIL legt also // einen zweiten Administrator an, statt den ersten umzubenennen. Das ist // gewollt – umbenennen waere eine Aenderung an einem Konto, das laengst benutzt // sein kann. const vorhanden = await db.benutzer.findUnique({ where: { email } }); if (vorhanden) { console.log(` Administrator: vorhanden (${email})`); return; } // Das Passwort wird nur als argon2-Hash gespeichert, nie im Klartext. await db.benutzer.create({ data: { email, name: "Administrator", rolle: Rolle.ADMIN, passwortHash: await hash(passwort), aktiv: true, }, }); console.log(` Administrator: angelegt (${email})`); }