Files
certvia/src/lib/password-policy.ts
T
msolarczekandClaude Opus 4.8 db36c795bd 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>
2026-07-22 09:18:05 +02:00

55 lines
2.5 KiB
TypeScript

/**
* 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(", ");
}