From 263fb0480605d55cd5c3757cd722698ac4bba1cb Mon Sep 17 00:00:00 2001 From: Marcelo Date: Thu, 7 May 2026 15:29:44 -0300 Subject: [PATCH] fix(frontend): show password complexity errors and inline checklist (EVO-992) Map backend 422 `codes` array to localized error toasts instead of a generic message. Add real-time complexity checklist below the new-password field (ticks as each rule is met). Correct frontend min-length guard from 6 to 8 characters to match backend policy. Co-Authored-By: Claude Sonnet 4.6 --- src/i18n/locales/en/profile.json | 16 +++- src/i18n/locales/pt-BR/profile.json | 16 +++- src/pages/Shared/Profile/Profile.tsx | 134 +++++++++++++++++---------- 3 files changed, 114 insertions(+), 52 deletions(-) diff --git a/src/i18n/locales/en/profile.json b/src/i18n/locales/en/profile.json index 8f8c7e66..7ea507dd 100644 --- a/src/i18n/locales/en/profile.json +++ b/src/i18n/locales/en/profile.json @@ -47,7 +47,21 @@ "validation": { "allFields": "All fields are required", "mismatch": "Passwords do not match", - "minLength": "Password must be at least 6 characters" + "minLength": "Password must be at least 8 characters" + }, + "errors": { + "too_short": "Password must be at least 8 characters", + "missing_uppercase": "Must include at least one uppercase letter (A-Z)", + "missing_lowercase": "Must include at least one lowercase letter (a-z)", + "missing_number": "Must include at least one number (0-9)", + "missing_special_char": "Must include at least one special character (!@#$...)" + }, + "rules": { + "minLength": "At least 8 characters", + "uppercase": "At least one uppercase letter (A-Z)", + "lowercase": "At least one lowercase letter (a-z)", + "number": "At least one number (0-9)", + "specialChar": "At least one special character (!@#$...)" } }, "accessToken": { diff --git a/src/i18n/locales/pt-BR/profile.json b/src/i18n/locales/pt-BR/profile.json index 56be8800..b93fdc49 100644 --- a/src/i18n/locales/pt-BR/profile.json +++ b/src/i18n/locales/pt-BR/profile.json @@ -47,7 +47,21 @@ "validation": { "allFields": "Todos os campos são obrigatórios", "mismatch": "As senhas não coincidem", - "minLength": "A senha deve ter no mínimo 6 caracteres" + "minLength": "A senha deve ter no mínimo 8 caracteres" + }, + "errors": { + "too_short": "A senha deve ter no mínimo 8 caracteres", + "missing_uppercase": "Deve incluir pelo menos uma letra maiúscula (A-Z)", + "missing_lowercase": "Deve incluir pelo menos uma letra minúscula (a-z)", + "missing_number": "Deve incluir pelo menos um número (0-9)", + "missing_special_char": "Deve incluir pelo menos um caractere especial (!@#$...)" + }, + "rules": { + "minLength": "Mínimo 8 caracteres", + "uppercase": "Pelo menos uma letra maiúscula (A-Z)", + "lowercase": "Pelo menos uma letra minúscula (a-z)", + "number": "Pelo menos um número (0-9)", + "specialChar": "Pelo menos um caractere especial (!@#$...)" } }, "accessToken": { diff --git a/src/pages/Shared/Profile/Profile.tsx b/src/pages/Shared/Profile/Profile.tsx index 69771828..6e325acd 100644 --- a/src/pages/Shared/Profile/Profile.tsx +++ b/src/pages/Shared/Profile/Profile.tsx @@ -28,7 +28,7 @@ import { Switch, Checkbox, } from '@evoapi/design-system'; -import { AlertTriangle, Mail, Volume2, Bell, Keyboard, Play } from 'lucide-react'; +import { AlertTriangle, Mail, Volume2, Bell, Keyboard, Play, Check, X } from 'lucide-react'; import { toast } from 'sonner'; import { useAuth } from '@/contexts/AuthContext'; import { useAuthStore } from '@/store/authStore'; @@ -38,6 +38,7 @@ import { type ProfileUpdateData, type PasswordChangeData, } from '@/services/profile/profileService'; +import { extractError } from '@/utils/apiHelpers'; import notificationSettingsService from '@/services/notifications/NotificationSettingsService'; import { getAudioSettings, playNotificationSoundPreview } from '@/utils/audioNotificationUtils'; import { getModifierKey } from '@/utils/platform'; @@ -396,7 +397,6 @@ const Profile = () => { }; const handleChangePassword = async () => { - // Validação básica if (!passwords.current_password || !passwords.password || !passwords.password_confirmation) { toast.error(t('password.validation.allFields')); return; @@ -407,7 +407,7 @@ const Profile = () => { return; } - if (passwords.password.length < 6) { + if (passwords.password.length < 8) { toast.error(t('password.validation.minLength')); return; } @@ -430,7 +430,20 @@ const Profile = () => { }); } catch (error) { console.error('Password change error:', error); - toast.error(t('notifications.passwordChangeError')); + const errorInfo = extractError(error); + if (errorInfo.code === 'VALIDATION_ERROR' && Array.isArray(errorInfo.details)) { + const passwordDetail = errorInfo.details.find((d: { field: string; codes?: string[] }) => d.field === 'password'); + if (passwordDetail?.codes?.length) { + passwordDetail.codes.forEach((code: string) => { + const key = `password.errors.${code.replace('password.', '')}`; + toast.error(t(key)); + }); + } else { + toast.error(t('notifications.passwordChangeError')); + } + } else { + toast.error(t('notifications.passwordChangeError')); + } } finally { setIsLoading(false); } @@ -516,56 +529,77 @@ const Profile = () => { ); - const renderSenha = () => ( - - - {t('password.title')} - {t('password.description')} - - -
-
- - -
+ const renderSenha = () => { + const newPassword = passwords.password; + const complexityRules = [ + { key: 'minLength', met: newPassword.length >= 8 }, + { key: 'uppercase', met: /[A-Z]/.test(newPassword) }, + { key: 'lowercase', met: /[a-z]/.test(newPassword) }, + { key: 'number', met: /\d/.test(newPassword) }, + { key: 'specialChar', met: /[^A-Za-z0-9]/.test(newPassword) }, + ]; -
- - -
+ return ( + + + {t('password.title')} + {t('password.description')} + + +
+
+ + +
-
- - -
+
+ + + {newPassword.length > 0 && ( +
    + {complexityRules.map(({ key, met }) => ( +
  • + {met ? : } + {t(`password.rules.${key}`)} +
  • + ))} +
+ )} +
-
- +
+ + +
+ +
+ +
-
-
-
- ); + + + ); + }; // const handleCopyToken = async () => { // // Primeiro tenta buscar do perfil carregado, depois do contexto