fix: enforce consistent password policy across all auth flows

Replace duplicated inline validation with a shared validatePassword()
utility that checks minimum length (8), rejects repetitive and common
passwords, and requires uppercase, lowercase, a digit, and a special
character.

- Add server/src/services/passwordPolicy.ts as single source of truth
- Apply to registration, password change, and admin create/edit user
  (admin routes previously had zero validation)
- Fix client min-length mismatch (6 vs 8) in RegisterPage and LoginPage
- Add client-side password length guard to AdminPage forms
- Update register.passwordTooShort and settings.passwordWeak i18n keys
  in all 12 locales to reflect the corrected requirements
This commit is contained in:
jubnl
2026-04-01 07:02:53 +02:00
parent ce8d498f2d
commit e03505dca2
18 changed files with 77 additions and 39 deletions

View File

@@ -255,7 +255,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'settings.passwordRequired': 'أدخل كلمة المرور الحالية والجديدة',
'settings.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل',
'settings.passwordMismatch': 'كلمتا المرور غير متطابقتين',
'settings.passwordWeak': 'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم',
'settings.passwordWeak': 'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم ورمز خاص',
'settings.passwordChanged': 'تم تغيير كلمة المرور بنجاح',
'settings.deleteAccount': 'حذف الحساب',
'settings.deleteAccountTitle': 'هل تريد حذف حسابك؟',
@@ -354,7 +354,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
// Register
'register.passwordMismatch': 'كلمتا المرور غير متطابقتين',
'register.passwordTooShort': 'يجب أن تتكون كلمة المرور من 6 أحرف على الأقل',
'register.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل',
'register.failed': 'فشل التسجيل',
'register.getStarted': 'ابدأ الآن',
'register.subtitle': 'أنشئ حسابًا وابدأ التخطيط لرحلات أحلامك.',

View File

@@ -225,7 +225,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'settings.passwordRequired': 'Informe a senha atual e a nova',
'settings.passwordTooShort': 'A senha deve ter pelo menos 8 caracteres',
'settings.passwordMismatch': 'As senhas não coincidem',
'settings.passwordWeak': 'A senha deve ter maiúscula, minúscula e número',
'settings.passwordWeak': 'A senha deve ter maiúscula, minúscula, número e um caractere especial',
'settings.passwordChanged': 'Senha alterada com sucesso',
'settings.deleteAccount': 'Excluir conta',
'settings.deleteAccountTitle': 'Excluir sua conta?',
@@ -349,7 +349,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
// Register
'register.passwordMismatch': 'As senhas não coincidem',
'register.passwordTooShort': 'A senha deve ter pelo menos 6 caracteres',
'register.passwordTooShort': 'A senha deve ter pelo menos 8 caracteres',
'register.failed': 'Falha no cadastro',
'register.getStarted': 'Começar',
'register.subtitle': 'Crie uma conta e comece a planejar suas viagens.',

View File

@@ -203,7 +203,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'settings.passwordRequired': 'Zadejte prosím současné i nové heslo',
'settings.passwordTooShort': 'Heslo musí mít alespoň 8 znaků',
'settings.passwordMismatch': 'Hesla se neshodují',
'settings.passwordWeak': 'Heslo musí obsahovat velké a malé písmeno a číslici',
'settings.passwordWeak': 'Heslo musí obsahovat velké a malé písmeno, číslici a speciální znak',
'settings.passwordChanged': 'Heslo bylo úspěšně změněno',
'settings.deleteAccount': 'Smazat účet',
'settings.deleteAccountTitle': 'Smazat váš účet?',
@@ -350,7 +350,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
// Registrace (Register)
'register.passwordMismatch': 'Hesla se neshodují',
'register.passwordTooShort': 'Heslo musí mít alespoň 6 znaků',
'register.passwordTooShort': 'Heslo musí mít alespoň 8 znaků',
'register.failed': 'Registrace se nezdařila',
'register.getStarted': 'Začínáme',
'register.subtitle': 'Vytvořte si účet a začněte plánovat svou vysněnou cestu.',

View File

@@ -250,7 +250,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'settings.passwordRequired': 'Bitte aktuelles und neues Passwort eingeben',
'settings.passwordTooShort': 'Passwort muss mindestens 8 Zeichen lang sein',
'settings.passwordMismatch': 'Passwörter stimmen nicht überein',
'settings.passwordWeak': 'Passwort muss Groß-, Kleinbuchstaben und eine Zahl enthalten',
'settings.passwordWeak': 'Passwort muss Groß-, Kleinbuchstaben, eine Zahl und ein Sonderzeichen enthalten',
'settings.passwordChanged': 'Passwort erfolgreich geändert',
'settings.deleteAccount': 'Löschen',
'settings.deleteAccountTitle': 'Account wirklich löschen?',
@@ -349,7 +349,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
// Register
'register.passwordMismatch': 'Passwörter stimmen nicht überein',
'register.passwordTooShort': 'Passwort muss mindestens 6 Zeichen lang sein',
'register.passwordTooShort': 'Passwort muss mindestens 8 Zeichen lang sein',
'register.failed': 'Registrierung fehlgeschlagen',
'register.getStarted': 'Jetzt starten',
'register.subtitle': 'Erstellen Sie ein Konto und beginnen Sie, Ihre Traumreisen zu planen.',

View File

@@ -250,7 +250,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'settings.passwordRequired': 'Please enter current and new password',
'settings.passwordTooShort': 'Password must be at least 8 characters',
'settings.passwordMismatch': 'Passwords do not match',
'settings.passwordWeak': 'Password must contain uppercase, lowercase, and a number',
'settings.passwordWeak': 'Password must contain uppercase, lowercase, a number, and a special character',
'settings.passwordChanged': 'Password changed successfully',
'settings.mustChangePassword': 'You must change your password before you can continue. Please set a new password below.',
'settings.deleteAccount': 'Delete account',
@@ -350,7 +350,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
// Register
'register.passwordMismatch': 'Passwords do not match',
'register.passwordTooShort': 'Password must be at least 6 characters',
'register.passwordTooShort': 'Password must be at least 8 characters',
'register.failed': 'Registration failed',
'register.getStarted': 'Get Started',
'register.subtitle': 'Create an account and start planning your dream trips.',

View File

@@ -347,7 +347,7 @@ const es: Record<string, string> = {
// Register
'register.passwordMismatch': 'Las contraseñas no coinciden',
'register.passwordTooShort': 'La contraseña debe tener al menos 6 caracteres',
'register.passwordTooShort': 'La contraseña debe tener al menos 8 caracteres',
'register.failed': 'Falló el registro',
'register.getStarted': 'Empezar',
'register.subtitle': 'Crea una cuenta y empieza a planificar tus viajes.',
@@ -1425,7 +1425,7 @@ const es: Record<string, string> = {
// Settings (2.6.2)
'settings.currentPasswordRequired': 'La contraseña actual es obligatoria',
'settings.passwordWeak': 'La contraseña debe contener mayúsculas, minúsculas y números',
'settings.passwordWeak': 'La contraseña debe contener mayúsculas, minúsculas, números y un carácter especial',
// Permissions
'admin.tabs.permissions': 'Permisos',

View File

@@ -250,7 +250,7 @@ const fr: Record<string, string> = {
'settings.passwordRequired': 'Veuillez saisir le mot de passe actuel et le nouveau',
'settings.passwordTooShort': 'Le mot de passe doit comporter au moins 8 caractères',
'settings.passwordMismatch': 'Les mots de passe ne correspondent pas',
'settings.passwordWeak': 'Le mot de passe doit contenir des majuscules, des minuscules et un chiffre',
'settings.passwordWeak': 'Le mot de passe doit contenir des majuscules, des minuscules, un chiffre et un caractère spécial',
'settings.passwordChanged': 'Mot de passe modifié avec succès',
'settings.deleteAccount': 'Supprimer le compte',
'settings.deleteAccountTitle': 'Supprimer votre compte ?',
@@ -349,7 +349,7 @@ const fr: Record<string, string> = {
// Register
'register.passwordMismatch': 'Les mots de passe ne correspondent pas',
'register.passwordTooShort': 'Le mot de passe doit comporter au moins 6 caractères',
'register.passwordTooShort': 'Le mot de passe doit comporter au moins 8 caractères',
'register.failed': 'Échec de l\'inscription',
'register.getStarted': 'Commencer',
'register.subtitle': 'Créez un compte et commencez à planifier vos voyages de rêve.',

View File

@@ -201,7 +201,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'settings.passwordRequired': 'Kérjük, add meg a jelenlegi és az új jelszót',
'settings.currentPasswordRequired': 'A jelenlegi jelszó megadása kötelező',
'settings.passwordTooShort': 'A jelszónak legalább 8 karakter hosszúnak kell lennie',
'settings.passwordWeak': 'A jelszónak tartalmaznia kell nagybetűt, kisbetűt és számot',
'settings.passwordWeak': 'A jelszónak tartalmaznia kell nagybetűt, kisbetűt, számot és speciális karaktert',
'settings.passwordMismatch': 'A jelszavak nem egyeznek',
'settings.passwordChanged': 'Jelszó sikeresen módosítva',
'settings.deleteAccount': 'Törlés',
@@ -349,7 +349,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
// Regisztráció
'register.passwordMismatch': 'A jelszavak nem egyeznek',
'register.passwordTooShort': 'A jelszónak legalább 6 karakter hosszúnak kell lennie',
'register.passwordTooShort': 'A jelszónak legalább 8 karakter hosszúnak kell lennie',
'register.failed': 'Regisztráció sikertelen',
'register.getStarted': 'Kezdjük',
'register.subtitle': 'Hozz létre egy fiókot, és kezdd el megtervezni álomutazásaidat.',

View File

@@ -202,7 +202,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'settings.passwordRequired': 'Inserisci la password attuale e quella nuova',
'settings.passwordTooShort': 'La password deve contenere almeno 8 caratteri',
'settings.passwordMismatch': 'Le password non corrispondono',
'settings.passwordWeak': 'La password deve contenere lettere maiuscole, minuscole e un numero',
'settings.passwordWeak': 'La password deve contenere lettere maiuscole, minuscole, un numero e un carattere speciale',
'settings.passwordChanged': 'Password cambiata con successo',
'settings.deleteAccount': 'Elimina account',
'settings.deleteAccountTitle': 'Eliminare il tuo account?',
@@ -349,7 +349,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
// Register
'register.passwordMismatch': 'Le password non corrispondono',
'register.passwordTooShort': 'La password deve contenere almeno 6 caratteri',
'register.passwordTooShort': 'La password deve contenere almeno 8 caratteri',
'register.failed': 'Registrazione fallita',
'register.getStarted': 'Inizia',
'register.subtitle': 'Crea un account e inizia a programmare i viaggi dei tuoi sogni.',

View File

@@ -250,7 +250,7 @@ const nl: Record<string, string> = {
'settings.passwordRequired': 'Voer het huidige en nieuwe wachtwoord in',
'settings.passwordTooShort': 'Wachtwoord moet minimaal 8 tekens bevatten',
'settings.passwordMismatch': 'Wachtwoorden komen niet overeen',
'settings.passwordWeak': 'Wachtwoord moet hoofdletters, kleine letters en een cijfer bevatten',
'settings.passwordWeak': 'Wachtwoord moet hoofdletters, kleine letters, een cijfer en een speciaal teken bevatten',
'settings.passwordChanged': 'Wachtwoord succesvol gewijzigd',
'settings.deleteAccount': 'Account verwijderen',
'settings.deleteAccountTitle': 'Account verwijderen?',
@@ -349,7 +349,7 @@ const nl: Record<string, string> = {
// Register
'register.passwordMismatch': 'Wachtwoorden komen niet overeen',
'register.passwordTooShort': 'Wachtwoord moet minimaal 6 tekens bevatten',
'register.passwordTooShort': 'Wachtwoord moet minimaal 8 tekens bevatten',
'register.failed': 'Registratie mislukt',
'register.getStarted': 'Aan de slag',
'register.subtitle': 'Maak een account aan en begin met het plannen van je droomreizen.',

View File

@@ -250,7 +250,7 @@ const ru: Record<string, string> = {
'settings.passwordRequired': 'Введите текущий и новый пароль',
'settings.passwordTooShort': 'Пароль должен содержать не менее 8 символов',
'settings.passwordMismatch': 'Пароли не совпадают',
'settings.passwordWeak': 'Пароль должен содержать заглавные, строчные буквы и цифру',
'settings.passwordWeak': 'Пароль должен содержать заглавные, строчные буквы, цифру и специальный символ',
'settings.passwordChanged': 'Пароль успешно изменён',
'settings.deleteAccount': 'Удалить аккаунт',
'settings.deleteAccountTitle': 'Удалить ваш аккаунт?',
@@ -349,7 +349,7 @@ const ru: Record<string, string> = {
// Register
'register.passwordMismatch': 'Пароли не совпадают',
'register.passwordTooShort': 'Пароль должен содержать не менее 6 символов',
'register.passwordTooShort': 'Пароль должен содержать не менее 8 символов',
'register.failed': 'Ошибка регистрации',
'register.getStarted': 'Начать',
'register.subtitle': 'Создайте аккаунт и начните планировать поездки мечты.',

View File

@@ -250,7 +250,7 @@ const zh: Record<string, string> = {
'settings.passwordRequired': '请输入当前密码和新密码',
'settings.passwordTooShort': '密码至少需要 8 个字符',
'settings.passwordMismatch': '两次输入的密码不一致',
'settings.passwordWeak': '密码必须包含大写字母、小写字母和数字',
'settings.passwordWeak': '密码必须包含大写字母、小写字母、数字和特殊字符',
'settings.passwordChanged': '密码修改成功',
'settings.deleteAccount': '删除账户',
'settings.deleteAccountTitle': '确定删除账户?',
@@ -349,7 +349,7 @@ const zh: Record<string, string> = {
// Register
'register.passwordMismatch': '两次输入的密码不一致',
'register.passwordTooShort': '密码至少需要 6 个字符',
'register.passwordTooShort': '密码至少需要 8 个字符',
'register.failed': '注册失败',
'register.getStarted': '开始使用',
'register.subtitle': '创建账户,开始规划你的梦想旅行。',

View File

@@ -253,6 +253,10 @@ export default function AdminPage(): React.ReactElement {
toast.error(t('admin.toast.fieldsRequired'))
return
}
if (createForm.password.trim().length < 8) {
toast.error(t('settings.passwordTooShort'))
return
}
try {
const data = await adminApi.createUser(createForm)
setUsers(prev => [data.user, ...prev])
@@ -308,7 +312,13 @@ export default function AdminPage(): React.ReactElement {
email: editForm.email.trim() || undefined,
role: editForm.role,
}
if (editForm.password.trim()) payload.password = editForm.password.trim()
if (editForm.password.trim()) {
if (editForm.password.trim().length < 8) {
toast.error(t('settings.passwordTooShort'))
return
}
payload.password = editForm.password.trim()
}
const data = await adminApi.updateUser(editingUser.id, payload)
setUsers(prev => prev.map(u => u.id === editingUser.id ? data.user : u))
setEditingUser(null)

View File

@@ -150,7 +150,7 @@ export default function LoginPage(): React.ReactElement {
}
if (mode === 'register') {
if (!username.trim()) { setError('Username is required'); setIsLoading(false); return }
if (password.length < 6) { setError('Password must be at least 6 characters'); setIsLoading(false); return }
if (password.length < 8) { setError('Password must be at least 8 characters'); setIsLoading(false); return }
await register(username, email, password, inviteToken || undefined)
} else {
const result = await login(email, password)

View File

@@ -26,7 +26,7 @@ export default function RegisterPage(): React.ReactElement {
return
}
if (password.length < 6) {
if (password.length < 8) {
setError(t('register.passwordTooShort'))
return
}