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:
@@ -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,18 +71,57 @@ 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 className="flex items-center gap-1.5 text-xs">
|
||||||
|
{requirements.minLength ? (
|
||||||
|
<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.minLength ? 'text-green-600' : 'text-gray-600'}>
|
||||||
|
At least 8 characters
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className={/[A-Z]/.test(password) && /[a-z]/.test(password) ? 'text-green-600' : ''}>
|
<li className="flex items-center gap-1.5 text-xs">
|
||||||
✓ Upper and lowercase letters
|
{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>
|
||||||
<li className={/[0-9]/.test(password) ? 'text-green-600' : ''}>
|
<li className="flex items-center gap-1.5 text-xs">
|
||||||
✓ At least one number
|
{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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{!allRequirementsMet && password.length > 0 && (
|
||||||
|
<p className="mt-2 text-xs text-red-600">
|
||||||
|
Please meet all requirements above
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user