Mitarbeiter-Passwörter auf 25 Zeichen (BSI-Empfehlung)

Portal-Customer-Schwellwert bleibt 12 (Handy-Eingabe → längere PWs
erhöhen Reuse-Risiko). Mitarbeiter/Admin nutzen Passwort-Manager,
für die kostet die Länge nichts.

passwordGenerator.ts:
- STAFF_MIN_PASSWORD_LENGTH = 25, PORTAL_MIN_PASSWORD_LENGTH = 12
- validatePasswordComplexity({ minLength }) parametrisiert

Mitarbeiter-Pfade auf 25:
- createUser, register, setUserPassword
- confirmPasswordReset: Audience aus Token bestimmen
  (getPasswordResetAudience), User → 25, Customer → 12. Kein
  Body-Hint, damit kein Downgrade-Trick möglich.

Portal-Pfade unverändert (default 12):
- setPortalPassword, changeInitialPortalPassword

Seed-Admin:
- 28-char Zufallspasswort (statt 16) mit allen 4 Klassen garantiert
- SEED_ADMIN_PASSWORD-ENV nur akzeptiert wenn ≥ 25 Zeichen,
  sonst Log-Warnung + Random-Fallback

Frontend:
- UserList: Hinweis "Mind. 25 Zeichen". Update + PW gleichzeitig →
  zwei API-Calls (PUT + POST /users/:id/password) statt
  Password im Body durchzuschmuggeln (Backend strippt es eh)
- PasswordResetConfirm: Hinweis "Mind. 12 (Mitarbeiter: 25)"
- userApi.setPassword(id, password) neu

Live-verifiziert:
- POST /users/6/password "Hallo123!Test" (12) → 400 "mindestens 25"
- POST /users/6/password "MeinExtremLangesPW2026!Test" → 200,
  Login mit neuem PW → success
- POST /customers/3/portal/password "Hallo123!Test" (12) → 200
- POST /users createUser mit 12-char-PW → 400 "mindestens 25"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:19:58 +02:00
parent cf8c6c84c2
commit 3e1fc3eab2
9 changed files with 127 additions and 25 deletions
+7 -3
View File
@@ -27,8 +27,10 @@ export default function PasswordResetConfirm() {
return;
}
if (password.length < 6) {
setError('Das Passwort muss mindestens 6 Zeichen lang sein.');
// Server prüft Komplexität endgültig (Mitarbeiter: 25 Zeichen, Portal-
// Kunden: 12). Frontend macht nur die naheliegenden Sanity-Checks.
if (password.length < 12) {
setError('Das Passwort muss mindestens 12 Zeichen lang sein (Mitarbeiter: 25).');
return;
}
@@ -124,7 +126,9 @@ export default function PasswordResetConfirm() {
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
<p className="text-xs text-gray-500 mt-1">Mindestens 6 Zeichen</p>
<p className="text-xs text-gray-500 mt-1">
Mind. 12 Zeichen (Mitarbeiter: 25), Groß-/Kleinbuchstabe, Ziffer, Sonderzeichen
</p>
</div>
<Input
+23 -9
View File
@@ -323,10 +323,19 @@ function UserModal({
telegramUsername: formData.telegramUsername || undefined,
signalNumber: formData.signalNumber || undefined,
};
// Passwort-Setzen ist serverseitig ein eigener Endpoint (separater
// Audit-Eintrag). Wenn beides gefragt: erst Daten, dann PW.
if (formData.password) {
updateData.password = formData.password;
updateMutation.mutate(updateData, {
onSuccess: () => {
userApi.setPassword(user.id, formData.password).catch((err) => {
alert(err?.response?.data?.error || 'Passwort konnte nicht gesetzt werden');
});
},
});
} else {
updateMutation.mutate(updateData);
}
updateMutation.mutate(updateData);
} else {
createMutation.mutate({
email: formData.email,
@@ -390,13 +399,18 @@ function UserModal({
required
/>
<Input
label={user ? 'Neues Passwort (leer = unverändert)' : 'Passwort *'}
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required={!user}
/>
<div>
<Input
label={user ? 'Neues Passwort (leer = unverändert)' : 'Passwort *'}
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required={!user}
/>
<p className="text-xs text-gray-500 mt-1">
Mind. 25 Zeichen, Groß-/Kleinbuchstabe, Ziffer, Sonderzeichen.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Messaging-Kanäle (für Datenschutz-Versand)</label>