fix(users): show password rules in form + match FE check to BE (#1303)

Create/Edit User modal previously had no hint about backend password
  complexity, and the FE pre-check was only length>=6 — so any weak
  password got bounced as a bare "HTTP 422" toast after the round-trip.

  - New checkPasswordComplexity util mirroring backend's validator order
  - Helper text under password inputs in both modals
  - Submit disabled until every rule passes
  - client.ts 422 parser falls back to JSON.stringify(detail) when the
    mapped array is empty, so a bare status code never masks the real
    Pydantic detail again
  - i18n keys added in all 8 locales (EN/DE fully translated, others
    per project's English-seed convention)
  - 7 unit tests pinning the validator contract, including the
    reporter's "12345678" input
This commit is contained in:
maziggy
2026-05-13 09:50:42 +02:00
parent db308aa80b
commit d08183278f
15 changed files with 602 additions and 461 deletions
+26
View File
@@ -0,0 +1,26 @@
/**
* Password complexity check matching the backend rules in
* `backend/app/schemas/auth.py:_validate_password_complexity` plus the
* implicit `min_length=8` that most server-side schemas enforce.
*
* Returning the FIRST unmet requirement as a translation-key suffix keeps the
* UI message order identical to what the backend would have returned — the
* user sees the same rule fail whether the check happens client- or server-
* side, which avoids the confusion of fixing one issue only to immediately
* trip another after the round-trip.
*/
export type PasswordRequirementKey =
| 'tooShort'
| 'needsUppercase'
| 'needsLowercase'
| 'needsDigit'
| 'needsSpecial';
export function checkPasswordComplexity(password: string): PasswordRequirementKey | null {
if (password.length < 8) return 'tooShort';
if (!/[A-Z]/.test(password)) return 'needsUppercase';
if (!/[a-z]/.test(password)) return 'needsLowercase';
if (!/\d/.test(password)) return 'needsDigit';
if (!/[^A-Za-z0-9]/.test(password)) return 'needsSpecial';
return null;
}