Files
Laufzettel/src/lib/verzeichnis.ts
T
florian.wilde ae290345b7
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(verzeichnis): LDAP-Abgleich, Zeitplan, CSV-Import und Log (#30)
2026-09-26 00:21:56 +00:00

155 lines
5.6 KiB
TypeScript

/**
* Was ueber eine Verzeichnisquelle auf beiden Seiten gilt (#016).
*
* Hier stehen die Attributzuordnung und die Grenzen des Abgleich-Intervalls: Zahlen
* und Namen, die das Formular genauso braucht wie der Dienst. Sie ein zweites Mal
* im Formular zu schreiben ging eine Weile gut und lief dann auseinander — die
* Oberflaeche liess hoechstens 1440 Minuten zu, der Dienst 10080, und wer das
* Gegenteil glaubte, stand vor einer Grenze, die nirgends stand (Reviewbefund zu
* #016).
*
* Die Zuordnung steht je Quelle in der Datenbank (`Verzeichnisquelle.attributZuordnung`,
* ein Json-Feld), weil nicht jedes Verzeichnis ein Active Directory ist. Die
* Vorgaben sind die AD-Namen; wer ein OpenLDAP anbindet, aendert einzelne Zeilen.
*
* Diese Datei liegt in `src/lib` und nicht bei den uebrigen LDAP-Modulen, weil das
* Formular dieselben Feldnamen und dieselben Vorgaben braucht — und eine
* Client-Komponente nichts aus `src/server/*` importieren darf. Hier steht
* deshalb nur, was ohne Netz und ohne Datenbank gilt.
*/
/**
* Die Grenzen des Abgleich-Intervalls, in Minuten.
*
* Unten fuenf Minuten: haeufiger ist kein Abgleich, sondern Last auf dem
* Domaenencontroller. Oben eine Woche: seltener gelesen heisst, dass ein
* ausgetretener Mitarbeiter tagelang aktiv bleibt.
*/
export const INTERVALL_MINDESTENS = 5;
export const INTERVALL_HOECHSTENS = 7 * 24 * 60;
/** Dieselbe Regel als Satz, damit Dienst und Formular dasselbe sagen. */
export function intervallMeldung(): string {
return `Das Abgleich-Intervall muss zwischen ${INTERVALL_MINDESTENS} und ${INTERVALL_HOECHSTENS} Minuten liegen (fünf Minuten bis eine Woche).`;
}
export type AttributZuordnung = {
vorname: string;
nachname: string;
anzeigename: string;
mail: string;
upn: string;
sam: string;
abteilung: string;
titel: string;
vorgesetzter: string;
externeId: string;
};
/** Die Vorgaben aus dem Auftrag: die Attributnamen eines Active Directory. */
export const AD_ZUORDNUNG: AttributZuordnung = {
vorname: "givenName",
nachname: "sn",
anzeigename: "displayName",
mail: "mail",
upn: "userPrincipalName",
sam: "sAMAccountName",
abteilung: "department",
titel: "title",
vorgesetzter: "manager",
externeId: "objectGUID",
};
/** Die Felder in der Reihenfolge, in der das Formular sie zeigt. */
export const ZUORDNUNGS_FELDER = Object.keys(AD_ZUORDNUNG) as (keyof AttributZuordnung)[];
/** Wie das Feld in der Oberflaeche heisst. Menschenlesbar, also mit Umlauten. */
export const ZUORDNUNGS_BESCHRIFTUNG: Record<keyof AttributZuordnung, string> = {
vorname: "Vorname",
nachname: "Nachname",
anzeigename: "Anzeigename",
mail: "E-Mail",
upn: "UPN",
sam: "Anmeldename (sam)",
abteilung: "Abteilung",
titel: "Titel",
vorgesetzter: "Vorgesetzter",
externeId: "Kennung (dauerhaft)",
};
/**
* Ein Attributname, wie ein Verzeichnis ihn kennt: Buchstabe voran, dann
* Buchstaben, Ziffern und Bindestriche. Mehr braucht es nicht, und mehr
* zuzulassen hiesse, Klammern und Sternchen in eine LDAP-Anfrage zu lassen.
*/
export const ATTRIBUT_MUSTER = /^[A-Za-z][A-Za-z0-9-]*$/u;
/**
* Liest eine gespeicherte Zuordnung aus dem Json-Feld.
*
* Fehlende oder leere Felder bekommen die AD-Vorgabe. Das ist Absicht: eine
* Quelle, die nur `anzeigename` abweichend belegt, soll nicht alle zehn Zeilen
* mitschreiben muessen, und eine von Hand geleerte Zeile darf kein Attribut mit
* leerem Namen anfragen.
*/
export function zuordnungLesen(roh: unknown): AttributZuordnung {
const felder = (typeof roh === "object" && roh !== null ? roh : {}) as Record<string, unknown>;
const gelesen = { ...AD_ZUORDNUNG };
for (const schluessel of ZUORDNUNGS_FELDER) {
const wert = felder[schluessel];
if (typeof wert === "string" && wert.trim()) gelesen[schluessel] = wert.trim();
}
return gelesen;
}
/**
* Die Attributzuordnung aus den Formularfeldern, geprueft.
*
* Steht hier und nicht in der Server Action, weil es reine Textarbeit ist und
* jeden Sonderfall einzeln geprueft gehoert: ein leeres Feld faellt heraus (der
* Dienst legt dann die Vorgabe darueber), und ein Name, der kein Attributname
* ist, wird abgewiesen statt in eine LDAP-Anfrage geschrieben.
*/
export function zuordnungAusEingabe(
werte: Partial<Record<keyof AttributZuordnung, string>>,
): { readonly zuordnung: Partial<AttributZuordnung> } | { readonly fehler: string } {
const zuordnung: Partial<AttributZuordnung> = {};
for (const feld of ZUORDNUNGS_FELDER) {
const wert = werte[feld]?.trim();
if (!wert) continue;
if (!ATTRIBUT_MUSTER.test(wert)) {
return {
fehler:
`„${wert}“ ist kein Attributname (${ZUORDNUNGS_BESCHRIFTUNG[feld]}). ` +
"Erwartet werden Buchstaben, Ziffern und Bindestriche, beginnend mit einem Buchstaben.",
};
}
zuordnung[feld] = wert;
}
return { zuordnung };
}
/**
* Was mit dem CA-Zertifikat geschehen soll: drei Faelle aus zwei Feldern.
*
* `undefined` heisst **unveraendert** — das hinterlegte bleibt. `""` heisst
* entfernen. Alles andere ist der neue Text. Beides zugleich ist keine Eingabe,
* die sich ausfuehren liesse, sondern ein Missverstaendnis, und wird als solches
* gemeldet.
*/
export function zertifikatWahl(
text: string,
entfernen: boolean,
): { readonly wert: string | undefined } | { readonly fehler: string } {
const neu = text.trim();
if (neu && entfernen) {
return {
fehler:
"Entweder ein neues CA-Zertifikat eintragen oder das hinterlegte entfernen — beides zugleich " +
"ergibt keinen Sinn.",
};
}
if (entfernen) return { wert: "" };
return { wert: neu || undefined };
}