From e03505dca2d361eacd2f607326a8b73acad765c6 Mon Sep 17 00:00:00 2001 From: jubnl Date: Wed, 1 Apr 2026 07:02:53 +0200 Subject: [PATCH] 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 --- client/src/i18n/translations/ar.ts | 4 ++-- client/src/i18n/translations/br.ts | 4 ++-- client/src/i18n/translations/cs.ts | 4 ++-- client/src/i18n/translations/de.ts | 4 ++-- client/src/i18n/translations/en.ts | 4 ++-- client/src/i18n/translations/es.ts | 4 ++-- client/src/i18n/translations/fr.ts | 4 ++-- client/src/i18n/translations/hu.ts | 4 ++-- client/src/i18n/translations/it.ts | 4 ++-- client/src/i18n/translations/nl.ts | 4 ++-- client/src/i18n/translations/ru.ts | 4 ++-- client/src/i18n/translations/zh.ts | 4 ++-- client/src/pages/AdminPage.tsx | 12 +++++++++++- client/src/pages/LoginPage.tsx | 2 +- client/src/pages/RegisterPage.tsx | 2 +- server/src/routes/admin.ts | 8 ++++++++ server/src/routes/auth.ts | 17 +++++------------ server/src/services/passwordPolicy.ts | 27 +++++++++++++++++++++++++++ 18 files changed, 77 insertions(+), 39 deletions(-) create mode 100644 server/src/services/passwordPolicy.ts diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 1974c6f..f38a2c7 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -255,7 +255,7 @@ const ar: Record = { 'settings.passwordRequired': 'أدخل كلمة المرور الحالية والجديدة', 'settings.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل', 'settings.passwordMismatch': 'كلمتا المرور غير متطابقتين', - 'settings.passwordWeak': 'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم', + 'settings.passwordWeak': 'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم ورمز خاص', 'settings.passwordChanged': 'تم تغيير كلمة المرور بنجاح', 'settings.deleteAccount': 'حذف الحساب', 'settings.deleteAccountTitle': 'هل تريد حذف حسابك؟', @@ -354,7 +354,7 @@ const ar: Record = { // Register 'register.passwordMismatch': 'كلمتا المرور غير متطابقتين', - 'register.passwordTooShort': 'يجب أن تتكون كلمة المرور من 6 أحرف على الأقل', + 'register.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل', 'register.failed': 'فشل التسجيل', 'register.getStarted': 'ابدأ الآن', 'register.subtitle': 'أنشئ حسابًا وابدأ التخطيط لرحلات أحلامك.', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 5ae2943..0f0cef1 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -225,7 +225,7 @@ const br: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index a57b3b7..b5c96ae 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -203,7 +203,7 @@ const cs: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index f998bfd..e83a4a3 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -250,7 +250,7 @@ const de: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 15aba3b..816b4ad 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -250,7 +250,7 @@ const en: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 48dd65a..5a1f180 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -347,7 +347,7 @@ const es: Record = { // 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 = { // 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', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 3550b4f..55b334b 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -250,7 +250,7 @@ const fr: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 3815d73..67928e8 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -201,7 +201,7 @@ const hu: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 53acacc..85ed1b2 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -202,7 +202,7 @@ const it: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 7f8a985..e448a87 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -250,7 +250,7 @@ const nl: Record = { '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 = { // 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.', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index cd23c2d..0710cdb 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -250,7 +250,7 @@ const ru: Record = { 'settings.passwordRequired': 'Введите текущий и новый пароль', 'settings.passwordTooShort': 'Пароль должен содержать не менее 8 символов', 'settings.passwordMismatch': 'Пароли не совпадают', - 'settings.passwordWeak': 'Пароль должен содержать заглавные, строчные буквы и цифру', + 'settings.passwordWeak': 'Пароль должен содержать заглавные, строчные буквы, цифру и специальный символ', 'settings.passwordChanged': 'Пароль успешно изменён', 'settings.deleteAccount': 'Удалить аккаунт', 'settings.deleteAccountTitle': 'Удалить ваш аккаунт?', @@ -349,7 +349,7 @@ const ru: Record = { // Register 'register.passwordMismatch': 'Пароли не совпадают', - 'register.passwordTooShort': 'Пароль должен содержать не менее 6 символов', + 'register.passwordTooShort': 'Пароль должен содержать не менее 8 символов', 'register.failed': 'Ошибка регистрации', 'register.getStarted': 'Начать', 'register.subtitle': 'Создайте аккаунт и начните планировать поездки мечты.', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 0e887df..47bab93 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -250,7 +250,7 @@ const zh: Record = { 'settings.passwordRequired': '请输入当前密码和新密码', 'settings.passwordTooShort': '密码至少需要 8 个字符', 'settings.passwordMismatch': '两次输入的密码不一致', - 'settings.passwordWeak': '密码必须包含大写字母、小写字母和数字', + 'settings.passwordWeak': '密码必须包含大写字母、小写字母、数字和特殊字符', 'settings.passwordChanged': '密码修改成功', 'settings.deleteAccount': '删除账户', 'settings.deleteAccountTitle': '确定删除账户?', @@ -349,7 +349,7 @@ const zh: Record = { // Register 'register.passwordMismatch': '两次输入的密码不一致', - 'register.passwordTooShort': '密码至少需要 6 个字符', + 'register.passwordTooShort': '密码至少需要 8 个字符', 'register.failed': '注册失败', 'register.getStarted': '开始使用', 'register.subtitle': '创建账户,开始规划你的梦想旅行。', diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index d4d6306..d5aad0e 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -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) diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 47e244c..1f8283b 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -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) diff --git a/client/src/pages/RegisterPage.tsx b/client/src/pages/RegisterPage.tsx index 762d1f1..824a615 100644 --- a/client/src/pages/RegisterPage.tsx +++ b/client/src/pages/RegisterPage.tsx @@ -26,7 +26,7 @@ export default function RegisterPage(): React.ReactElement { return } - if (password.length < 6) { + if (password.length < 8) { setError(t('register.passwordTooShort')) return } diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 5b62298..cfe6544 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -10,6 +10,7 @@ import { writeAudit, getClientIp, logInfo } from '../services/auditLog'; import { getAllPermissions, savePermissions, PERMISSION_ACTIONS } from '../services/permissions'; import { revokeUserSessions } from '../mcp'; import { maybe_encrypt_api_key, decrypt_api_key } from '../services/apiKeyCrypto'; +import { validatePassword } from '../services/passwordPolicy'; import { updateJwtSecret } from '../config'; const router = express.Router(); @@ -47,6 +48,9 @@ router.post('/users', (req: Request, res: Response) => { return res.status(400).json({ error: 'Username, email and password are required' }); } + const pwCheck = validatePassword(password.trim()); + if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason }); + if (role && !['user', 'admin'].includes(role)) { return res.status(400).json({ error: 'Invalid role' }); } @@ -97,6 +101,10 @@ router.put('/users/:id', (req: Request, res: Response) => { if (conflict) return res.status(409).json({ error: 'Email already taken' }); } + if (password) { + const pwCheck = validatePassword(password); + if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason }); + } const passwordHash = password ? bcrypt.hashSync(password, 12) : null; db.prepare(` diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 6cfc40d..f86357e 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -10,6 +10,7 @@ import fetch from 'node-fetch'; import { authenticator } from 'otplib'; import QRCode from 'qrcode'; import { db } from '../db/database'; +import { validatePassword } from '../services/passwordPolicy'; import { authenticate, optionalAuth, demoUploadBlock } from '../middleware/auth'; import { JWT_SECRET } from '../config'; import { encryptMfaSecret, decryptMfaSecret } from '../services/mfaCrypto'; @@ -268,13 +269,8 @@ router.post('/register', authLimiter, (req: Request, res: Response) => { return res.status(400).json({ error: 'Username, email and password are required' }); } - if (password.length < 8) { - return res.status(400).json({ error: 'Password must be at least 8 characters' }); - } - - if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password)) { - return res.status(400).json({ error: 'Password must contain at least one uppercase letter, one lowercase letter, and one number' }); - } + const pwCheck = validatePassword(password); + if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason }); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { @@ -382,11 +378,8 @@ router.put('/me/password', authenticate, rateLimiter(5, RATE_LIMIT_WINDOW), (req const { current_password, new_password } = req.body; if (!current_password) return res.status(400).json({ error: 'Current password is required' }); if (!new_password) return res.status(400).json({ error: 'New password is required' }); - if (new_password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters' }); - - if (!/[A-Z]/.test(new_password) || !/[a-z]/.test(new_password) || !/[0-9]/.test(new_password)) { - return res.status(400).json({ error: 'Password must contain at least one uppercase letter, one lowercase letter, and one number' }); - } + const pwCheck = validatePassword(new_password); + if (!pwCheck.ok) return res.status(400).json({ error: pwCheck.reason }); const user = db.prepare('SELECT password_hash FROM users WHERE id = ?').get(authReq.user.id) as { password_hash: string } | undefined; if (!user || !bcrypt.compareSync(current_password, user.password_hash)) { diff --git a/server/src/services/passwordPolicy.ts b/server/src/services/passwordPolicy.ts new file mode 100644 index 0000000..ad817d1 --- /dev/null +++ b/server/src/services/passwordPolicy.ts @@ -0,0 +1,27 @@ +const COMMON_PASSWORDS = new Set([ + 'password', '12345678', '123456789', '1234567890', 'password1', + 'qwerty123', 'iloveyou', 'admin123', 'letmein12', 'welcome1', + 'monkey123', 'dragon12', 'master12', 'qwerty12', 'abc12345', + 'trustno1', 'baseball', 'football', 'shadow12', 'michael1', + 'jennifer', 'superman', 'abcdefgh', 'abcd1234', 'password123', + 'admin1234', 'changeme', 'welcome123', 'passw0rd', 'p@ssword', +]); + +export function validatePassword(password: string): { ok: boolean; reason?: string } { + if (password.length < 8) return { ok: false, reason: 'Password must be at least 8 characters' }; + + if (/^(.)\1+$/.test(password)) { + return { ok: false, reason: 'Password is too repetitive' }; + } + + if (COMMON_PASSWORDS.has(password.toLowerCase())) { + return { ok: false, reason: 'Password is too common. Please choose a unique password.' }; + } + + if (!/[A-Z]/.test(password)) return { ok: false, reason: 'Password must contain at least one uppercase letter' }; + if (!/[a-z]/.test(password)) return { ok: false, reason: 'Password must contain at least one lowercase letter' }; + if (!/[0-9]/.test(password)) return { ok: false, reason: 'Password must contain at least one number' }; + if (!/[^A-Za-z0-9]/.test(password)) return { ok: false, reason: 'Password must contain at least one special character' }; + + return { ok: true }; +}