Fundament Benutzer-/Rollenverwaltung: Schema, Passwort-Policy, Force-Change
Vorbereitung für die Benutzer- & Rollenverwaltung (Pakete A–C) ohne E-Mail-Flow. - Schema: User.mustChangePassword (erzwungener Wechsel), User.mfaEnrolledAt + recoveryCodes (optionale Nutzer-MFA, Paket C); neues Singleton PlatformSetting mit mfaRequired (Default aus → MFA optional). Migration additiv + Grant/Seed. - RBAC: neue Permission role:manage (Rollenverwaltung), dem Mandanten-Admin zugewiesen; für den bestehenden Demo-Mandanten nachgezogen (neue Tenants via provision/seed automatisch). - Passwort-Policy: lib/password-policy.ts (client-safe Validierung/Beschreibung, Defaults + Ableitung aus TenantSettings.securityPolicy) und server/password.ts (Argon2id-Hash + policy-konformer Einmal-Passwort-Generator). - Force-Change-Flow: /change-password (außerhalb der (app)-Shell) + Self-Service- Action changeOwnPassword (policy-geprüft, Audit). (app)-Layout erzwingt den Wechsel autoritativ aus der DB und sperrt deaktivierte Konten (Abmelden-Screen). tsc + lint + build + Guard-Check grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Passwort-Policy (Benutzer- & Rollenverwaltung, Querschnitt). Reine Validierung
|
||||
* ohne Server-Abhängigkeiten — auch in Client-Komponenten nutzbar (Hinweistexte).
|
||||
* Werte stammen aus TenantSettings.securityPolicy.password bzw. dem Plattform-Default.
|
||||
*/
|
||||
export interface PasswordPolicy {
|
||||
minLength: number;
|
||||
requireUpper: boolean;
|
||||
requireLower: boolean;
|
||||
requireDigit: boolean;
|
||||
requireSymbol: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_PASSWORD_POLICY: PasswordPolicy = {
|
||||
minLength: 12,
|
||||
requireUpper: true,
|
||||
requireLower: true,
|
||||
requireDigit: true,
|
||||
requireSymbol: false,
|
||||
};
|
||||
|
||||
/** Liest die Policy aus einem TenantSettings.securityPolicy-JSON (mit Defaults). */
|
||||
export function resolvePasswordPolicy(securityPolicy: unknown): PasswordPolicy {
|
||||
const sp = securityPolicy && typeof securityPolicy === "object" ? (securityPolicy as Record<string, unknown>) : {};
|
||||
const p = (sp.password && typeof sp.password === "object" ? (sp.password as Partial<PasswordPolicy>) : {}) ?? {};
|
||||
return {
|
||||
minLength: typeof p.minLength === "number" ? p.minLength : DEFAULT_PASSWORD_POLICY.minLength,
|
||||
requireUpper: p.requireUpper ?? DEFAULT_PASSWORD_POLICY.requireUpper,
|
||||
requireLower: p.requireLower ?? DEFAULT_PASSWORD_POLICY.requireLower,
|
||||
requireDigit: p.requireDigit ?? DEFAULT_PASSWORD_POLICY.requireDigit,
|
||||
requireSymbol: p.requireSymbol ?? DEFAULT_PASSWORD_POLICY.requireSymbol,
|
||||
};
|
||||
}
|
||||
|
||||
/** Gibt die verletzten Anforderungen zurück (leeres Array = gültig). */
|
||||
export function validatePassword(pw: string, policy: PasswordPolicy = DEFAULT_PASSWORD_POLICY): string[] {
|
||||
const missing: string[] = [];
|
||||
if (pw.length < policy.minLength) missing.push(`mindestens ${policy.minLength} Zeichen`);
|
||||
if (policy.requireUpper && !/[A-Z]/.test(pw)) missing.push("einen Großbuchstaben");
|
||||
if (policy.requireLower && !/[a-z]/.test(pw)) missing.push("einen Kleinbuchstaben");
|
||||
if (policy.requireDigit && !/\d/.test(pw)) missing.push("eine Ziffer");
|
||||
if (policy.requireSymbol && !/[^A-Za-z0-9]/.test(pw)) missing.push("ein Sonderzeichen");
|
||||
return missing;
|
||||
}
|
||||
|
||||
/** Menschlich lesbare Beschreibung der Anforderungen (für Hinweistexte). */
|
||||
export function describePasswordPolicy(policy: PasswordPolicy = DEFAULT_PASSWORD_POLICY): string {
|
||||
const parts = [`mind. ${policy.minLength} Zeichen`];
|
||||
if (policy.requireUpper) parts.push("Großbuchstabe");
|
||||
if (policy.requireLower) parts.push("Kleinbuchstabe");
|
||||
if (policy.requireDigit) parts.push("Ziffer");
|
||||
if (policy.requireSymbol) parts.push("Sonderzeichen");
|
||||
return parts.join(", ");
|
||||
}
|
||||
Reference in New Issue
Block a user