feat(seed): sieben Vorlagen, Beispielbestand und Demo-Kunden
Die sieben Ablaeufe des Hauses liegen nach dem Seed als veroeffentlichte Version 1 bereit: 36 Abschnitte, 155 Schritte, 28 Variablen, jeder Schritt mit Hilfetext. Sie stehen als JSON im Austauschformat unter prisma/seed-daten/vorlagen/ und werden beim Einrichten gegen dasselbe Schema geprueft wie ein Import. Der Seed richtet ein, er raeumt nicht auf: jede Zeile hat einen sprechenden Schluessel, und was es schon gibt, bleibt unberuehrt. Erst Entwurf, dann veroeffentlichen - die Trigger aus #010 lassen keinen anderen Weg zu. Im Modus DIENSTLEISTUNG haengt der Beispielbestand an der Musterfirma, LZ-7K3F9Q fuehrt also auf MUS-PC-001. Dazu zwei Zusagen eingeloest, die niemand deckte: {{ou_gesperrt}} kommt jetzt wirklich aus den Einstellungen, und Titel und Hilfetext eines Laufs bekommen dasselbe generierte Passwort wie der Befehl daneben - vorher stand im Befehl A und im Hilfetext B. Closes #17 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae290345b7
commit
5c81a62cfd
@@ -0,0 +1,145 @@
|
||||
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<string, string> {
|
||||
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<void> {
|
||||
// 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<string, unknown>).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<string> {
|
||||
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<void> {
|
||||
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})`);
|
||||
}
|
||||
Reference in New Issue
Block a user