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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1309,6 +1309,12 @@ export const userApi = {
|
||||
const res = await api.put<ApiResponse<User>>(`/users/${id}`, data);
|
||||
return res.data;
|
||||
},
|
||||
// Passwort eines Users zurücksetzen (Admin-Funktion). Separat vom generischen
|
||||
// Update, damit der Vorgang einen eigenen Audit-Eintrag bekommt.
|
||||
setPassword: async (id: number, password: string) => {
|
||||
const res = await api.post<ApiResponse<void>>(`/users/${id}/password`, { password });
|
||||
return res.data;
|
||||
},
|
||||
delete: async (id: number) => {
|
||||
const res = await api.delete<ApiResponse<void>>(`/users/${id}`);
|
||||
return res.data;
|
||||
|
||||
Reference in New Issue
Block a user