/** * Sanitize-Helpers: entfernen sensible Felder aus DB-Ergebnissen, bevor sie * als API-Response rausgehen. Zentrale Stelle, damit keine Passwort-Hashes, * Verschlüsselungen oder Reset-Tokens versehentlich durch die API leaken. */ // Felder die NIE in einer API-Response an den Client gehen dürfen const SENSITIVE_CUSTOMER_FIELDS = [ 'portalPasswordHash', 'portalPasswordResetToken', 'portalPasswordResetExpiresAt', ] as const; const SENSITIVE_USER_FIELDS = [ 'password', 'passwordResetToken', 'passwordResetExpiresAt', 'tokenInvalidatedAt', ] as const; /** * Entfernt Passwort-Hash, Reset-Token etc. aus einem Customer-Objekt. * `portalPasswordEncrypted` bleibt nur drin, wenn der Caller Admin-Rechte hat * (wird in einem zweiten Schritt vom Controller gemacht). Dieser Helper entfernt * es standardmäßig. */ export function sanitizeCustomer>(customer: T | null): T | null { if (!customer) return customer; const copy = { ...customer }; for (const field of SENSITIVE_CUSTOMER_FIELDS) { delete copy[field]; } // portalPasswordEncrypted bleibt hier zunächst drin, damit Mitarbeiter das // Portal-Passwort ggf. in der UI anzeigen können. Wird per requirePermission // auf 'customers:update' implizit gesichert. return copy; } /** * Entfernt portalPasswordEncrypted zusätzlich zu den anderen sensiblen Feldern. * Für Kontexte in denen der Caller KEIN Admin ist (z.B. Portal-Kunde). */ export function sanitizeCustomerStrict>(customer: T | null): T | null { if (!customer) return customer; const copy = sanitizeCustomer(customer) as Record | null; if (!copy) return null; delete copy.portalPasswordEncrypted; return copy as T; } /** * Sanitize-Liste von Customers. */ export function sanitizeCustomers>(customers: T[]): T[] { return customers.map((c) => sanitizeCustomer(c)).filter((c): c is T => c !== null); } /** * Sanitize User-Objekt für API-Responses. */ export function sanitizeUser>(user: T | null): T | null { if (!user) return user; const copy = { ...user }; for (const field of SENSITIVE_USER_FIELDS) { delete copy[field]; } return copy; } // ==================== REQUEST-BODY WHITELISTS ==================== // Gegen Mass-Assignment: Nur explizit erlaubte Felder aus req.body übernehmen. const CUSTOMER_UPDATABLE_FIELDS = [ 'type', 'salutation', 'useInformalAddress', 'firstName', 'lastName', 'companyName', 'foundingDate', 'birthDate', 'birthPlace', 'email', 'phone', 'mobile', 'taxNumber', 'commercialRegisterNumber', 'notes', 'portalEnabled', 'portalEmail', 'autoBirthdayGreeting', 'autoBirthdayChannel', // Nicht: portalPasswordHash, portalPasswordEncrypted, portalPasswordResetToken, // portalTokenInvalidatedAt, customerNumber, id, createdAt, updatedAt, consentHash, // lastBirthdayGreetingYear, privacyPolicyPath, businessRegistrationPath, commercialRegisterPath ] as const; const CUSTOMER_CREATE_FIELDS = [ ...CUSTOMER_UPDATABLE_FIELDS, // customerNumber wird vom Service generiert – nicht aus req.body übernehmen ] as const; const USER_UPDATABLE_FIELDS = [ 'email', 'firstName', 'lastName', 'isActive', 'whatsappNumber', 'telegramUsername', 'signalNumber', 'roleIds', 'password', // nur Admin, wird im Service gehashed // hasGdprAccess + hasDeveloperAccess sind keine User-Spalten – der Service // mappt sie auf die versteckten Rollen DSGVO/Developer (siehe // setUserGdprAccess / setUserDeveloperAccess). Müssen aber auf der Whitelist // stehen, damit pick() sie nicht aus dem Request entfernt. 'hasGdprAccess', 'hasDeveloperAccess', // Nicht: id, customerId, tokenInvalidatedAt, passwordResetToken, passwordResetExpiresAt ] as const; const USER_CREATE_FIELDS = USER_UPDATABLE_FIELDS; /** * Filtert req.body anhand einer Whitelist. Unerlaubte Felder werden verworfen. * Verhindert Mass-Assignment-Angriffe (z.B. { portalPasswordHash: "..." } im Body). */ function pick(obj: T, allowed: readonly string[]): Partial { const result: Partial = {}; for (const key of allowed) { if (key in obj) { (result as any)[key] = (obj as any)[key]; } } return result; } export function pickCustomerUpdate(body: unknown): Partial> { return pick((body as object) || {}, CUSTOMER_UPDATABLE_FIELDS); } export function pickCustomerCreate(body: unknown): Partial> { return pick((body as object) || {}, CUSTOMER_CREATE_FIELDS); } export function pickUserUpdate(body: unknown): Partial> { return pick((body as object) || {}, USER_UPDATABLE_FIELDS); } export function pickUserCreate(body: unknown): Partial> { return pick((body as object) || {}, USER_CREATE_FIELDS); }