feat(ui): improve password validation display with detailed requirements

- Enhance PasswordStrengthIndicator with visual checkmarks for each requirement
- Add explicit validation for uppercase, lowercase, and number requirements
- Show clear pass/fail indicators (CheckCircle/XCircle icons) for each criterion
- Add front-end validation matching production password policy
- Display specific error messages listing all missing requirements
- Align with production standards (8+ chars, uppercase, lowercase, number)
This commit is contained in:
Radosław Gierwiało
2025-12-06 18:34:03 +01:00
parent 54e8e513ee
commit 640ca2a563
2 changed files with 95 additions and 22 deletions

View File

@@ -1,24 +1,37 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { CheckCircle, XCircle } from 'lucide-react';
/** /**
* Password Strength Indicator Component * Password Strength Indicator Component
* Calculates and displays password strength with visual feedback * Calculates and displays password strength with visual feedback
* Shows requirements based on production environment standards
*/ */
const PasswordStrengthIndicator = ({ password }) => { const PasswordStrengthIndicator = ({ password }) => {
const requirements = useMemo(() => {
const checks = {
minLength: password.length >= 8,
hasUppercase: /[A-Z]/.test(password),
hasLowercase: /[a-z]/.test(password),
hasNumber: /[0-9]/.test(password),
};
return checks;
}, [password]);
const strength = useMemo(() => { const strength = useMemo(() => {
if (!password) return { score: 0, label: '', color: '' }; if (!password) return { score: 0, label: '', color: '' };
let score = 0; let score = 0;
// Length check // Count met requirements (production standards)
if (password.length >= 8) score++; if (requirements.minLength) score++;
if (password.length >= 12) score++; if (requirements.hasUppercase) score++;
if (requirements.hasLowercase) score++;
if (requirements.hasNumber) score++;
// Character variety checks // Bonus points for extra length and special chars
if (/[a-z]/.test(password)) score++; // lowercase if (password.length >= 12) score++;
if (/[A-Z]/.test(password)) score++; // uppercase if (/[^a-zA-Z0-9]/.test(password)) score++;
if (/[0-9]/.test(password)) score++; // numbers
if (/[^a-zA-Z0-9]/.test(password)) score++; // special chars
// Determine label and color // Determine label and color
if (score <= 2) { if (score <= 2) {
@@ -28,7 +41,13 @@ const PasswordStrengthIndicator = ({ password }) => {
} else { } else {
return { score, label: 'Strong', color: 'bg-green-500' }; return { score, label: 'Strong', color: 'bg-green-500' };
} }
}, [password]); }, [password, requirements]);
// Check if all required criteria are met
const allRequirementsMet = requirements.minLength &&
requirements.hasUppercase &&
requirements.hasLowercase &&
requirements.hasNumber;
if (!password) return null; if (!password) return null;
@@ -52,17 +71,56 @@ const PasswordStrengthIndicator = ({ password }) => {
style={{ width: `${widthPercentage}%` }} style={{ width: `${widthPercentage}%` }}
/> />
</div> </div>
<ul className="mt-2 text-xs text-gray-600 space-y-1"> <div className="mt-2 space-y-1">
<li className={password.length >= 8 ? 'text-green-600' : ''}> <p className="text-xs font-medium text-gray-700 mb-1.5">Password requirements:</p>
At least 8 characters <ul className="space-y-1">
</li> <li className="flex items-center gap-1.5 text-xs">
<li className={/[A-Z]/.test(password) && /[a-z]/.test(password) ? 'text-green-600' : ''}> {requirements.minLength ? (
Upper and lowercase letters <CheckCircle className="w-4 h-4 text-green-600 flex-shrink-0" />
</li> ) : (
<li className={/[0-9]/.test(password) ? 'text-green-600' : ''}> <XCircle className="w-4 h-4 text-gray-400 flex-shrink-0" />
At least one number )}
</li> <span className={requirements.minLength ? 'text-green-600' : 'text-gray-600'}>
</ul> At least 8 characters
</span>
</li>
<li className="flex items-center gap-1.5 text-xs">
{requirements.hasUppercase ? (
<CheckCircle className="w-4 h-4 text-green-600 flex-shrink-0" />
) : (
<XCircle className="w-4 h-4 text-gray-400 flex-shrink-0" />
)}
<span className={requirements.hasUppercase ? 'text-green-600' : 'text-gray-600'}>
At least one uppercase letter (A-Z)
</span>
</li>
<li className="flex items-center gap-1.5 text-xs">
{requirements.hasLowercase ? (
<CheckCircle className="w-4 h-4 text-green-600 flex-shrink-0" />
) : (
<XCircle className="w-4 h-4 text-gray-400 flex-shrink-0" />
)}
<span className={requirements.hasLowercase ? 'text-green-600' : 'text-gray-600'}>
At least one lowercase letter (a-z)
</span>
</li>
<li className="flex items-center gap-1.5 text-xs">
{requirements.hasNumber ? (
<CheckCircle className="w-4 h-4 text-green-600 flex-shrink-0" />
) : (
<XCircle className="w-4 h-4 text-gray-400 flex-shrink-0" />
)}
<span className={requirements.hasNumber ? 'text-green-600' : 'text-gray-600'}>
At least one number (0-9)
</span>
</li>
</ul>
</div>
{!allRequirementsMet && password.length > 0 && (
<p className="mt-2 text-xs text-red-600">
Please meet all requirements above
</p>
)}
</div> </div>
); );
}; };

View File

@@ -174,14 +174,29 @@ const RegisterPage = () => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
// Validation // Validation - passwords match
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match'); setError('Passwords do not match');
return; return;
} }
// Validation - password requirements (production standards)
const passwordErrors = [];
if (formData.password.length < 8) { if (formData.password.length < 8) {
setError('Password must be at least 8 characters long'); passwordErrors.push('at least 8 characters');
}
if (!/[A-Z]/.test(formData.password)) {
passwordErrors.push('one uppercase letter');
}
if (!/[a-z]/.test(formData.password)) {
passwordErrors.push('one lowercase letter');
}
if (!/[0-9]/.test(formData.password)) {
passwordErrors.push('one number');
}
if (passwordErrors.length > 0) {
setError(`Password must contain ${passwordErrors.join(', ')}`);
return; return;
} }