Files
opencrm/backend/src/utils/passwordGenerator.ts
T
duffyduck 8a5ffbb563 Passwort-Komplexität + Portal-Credentials-UX
validatePasswordComplexity (12 Zeichen, Groß/Klein/Zahl/Sonderzeichen)
zentral in passwordGenerator.ts; jetzt erzwungen in setPortalPassword,
confirmPasswordReset, register, createUser, updateUser.

Neue Endpoints:
- POST /customers/:id/portal/password/generate → 16-Zeichen Zufallspasswort
- POST /customers/:id/portal/send-credentials → Versand per Mail
  (nur wenn portalEnabled aktiv)

Frontend (CustomerDetail): Generate-Button vor Setzen, Send-Credentials
nach gesetztem Passwort, Live-Komplexitäts-Hint (✓/○) während Eingabe,
alert() durch Toast-Notifications ersetzt.

Live-verifiziert: schwaches Passwort → 400 mit Detail-Fehler, komplexes
Passwort → 200, Generator liefert 16-Zeichen-Passwort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:26:11 +02:00

139 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==================== PASSWORD GENERATOR ====================
// Generiert sichere, zufällige Passwörter
import { randomBytes } from 'crypto';
// Zeichensätze für Passwort-Generierung
const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const NUMBERS = '0123456789';
const SPECIAL = '!@#$%^&*()_+-=[]{}|;:,.<>?';
// Standard-Passwortlänge
const DEFAULT_LENGTH = 16;
export interface PasswordOptions {
length?: number;
includeLowercase?: boolean;
includeUppercase?: boolean;
includeNumbers?: boolean;
includeSpecial?: boolean;
}
/**
* Generiert ein kryptografisch sicheres Passwort
*/
export function generateSecurePassword(options: PasswordOptions = {}): string {
const {
length = DEFAULT_LENGTH,
includeLowercase = true,
includeUppercase = true,
includeNumbers = true,
includeSpecial = true,
} = options;
// Zeichensatz zusammenstellen
let charset = '';
const requiredChars: string[] = [];
if (includeLowercase) {
charset += LOWERCASE;
requiredChars.push(getRandomChar(LOWERCASE));
}
if (includeUppercase) {
charset += UPPERCASE;
requiredChars.push(getRandomChar(UPPERCASE));
}
if (includeNumbers) {
charset += NUMBERS;
requiredChars.push(getRandomChar(NUMBERS));
}
if (includeSpecial) {
charset += SPECIAL;
requiredChars.push(getRandomChar(SPECIAL));
}
if (charset.length === 0) {
throw new Error('Mindestens ein Zeichensatz muss aktiviert sein');
}
// Restliche Zeichen auffüllen
const remainingLength = Math.max(0, length - requiredChars.length);
const randomChars: string[] = [];
for (let i = 0; i < remainingLength; i++) {
randomChars.push(getRandomChar(charset));
}
// Alle Zeichen mischen (Fisher-Yates Shuffle)
const allChars = [...requiredChars, ...randomChars];
for (let i = allChars.length - 1; i > 0; i--) {
const j = getRandomInt(i + 1);
[allChars[i], allChars[j]] = [allChars[j], allChars[i]];
}
return allChars.join('');
}
/**
* Generiert ein einfaches Passwort ohne Sonderzeichen (für APIs die das nicht mögen)
*/
export function generateSimplePassword(length = 12): string {
return generateSecurePassword({
length,
includeLowercase: true,
includeUppercase: true,
includeNumbers: true,
includeSpecial: false,
});
}
// ==================== PASSWORD COMPLEXITY VALIDATION ====================
/**
* Mindestanforderungen für vom User vergebene Passwörter.
* Generator-Output (generateSecurePassword) erfüllt diese standardmäßig.
*/
export interface PasswordComplexityResult {
ok: boolean;
errors: string[];
}
export function validatePasswordComplexity(pw: unknown): PasswordComplexityResult {
const errors: string[] = [];
if (typeof pw !== 'string') {
return { ok: false, errors: ['Passwort fehlt oder ist kein Text'] };
}
if (pw.length < 12) errors.push('mindestens 12 Zeichen');
if (!/[a-z]/.test(pw)) errors.push('mindestens einen Kleinbuchstaben');
if (!/[A-Z]/.test(pw)) errors.push('mindestens einen Großbuchstaben');
if (!/[0-9]/.test(pw)) errors.push('mindestens eine Ziffer');
// Sonderzeichen-Set bewusst breit auch Leerzeichen + Unicode-Punktuation
// zulassen, damit gängige Passwort-Manager-Outputs nicht abgelehnt werden.
if (!/[^A-Za-z0-9]/.test(pw)) errors.push('mindestens ein Sonderzeichen');
return { ok: errors.length === 0, errors };
}
/**
* Wirft mit sprechender Fehlermeldung, wenn das Passwort die Komplexität
* nicht erfüllt. Für Aufruf direkt im Controller, der die Exception fängt.
*/
export function assertPasswordComplexity(pw: unknown): void {
const r = validatePasswordComplexity(pw);
if (!r.ok) {
throw new Error('Passwort erfüllt Mindestanforderungen nicht: ' + r.errors.join(', '));
}
}
// Kryptografisch sichere Zufallszahl
function getRandomInt(max: number): number {
const bytes = randomBytes(4);
const value = bytes.readUInt32BE(0);
return value % max;
}
// Zufälliges Zeichen aus einem Zeichensatz
function getRandomChar(charset: string): string {
return charset[getRandomInt(charset.length)];
}