feat: add email verification, password reset, and WSDC integration (Phase 1.5)
Backend features: - AWS SES email service with HTML templates - Email verification with dual method (link + 6-digit PIN code) - Password reset workflow with secure tokens - WSDC API proxy for dancer lookup and auto-fill registration - Extended User model with verification and WSDC fields - Email verification middleware for protected routes Frontend features: - Two-step registration with WSDC ID lookup - Password strength indicator component - Email verification page with code input - Password reset flow (request + reset pages) - Verification banner for unverified users - Updated authentication context and API service Testing: - 65 unit tests with 100% coverage of new features - Tests for auth utils, email service, WSDC controller, and middleware - Integration tests for full authentication flows - Comprehensive mocking of AWS SES and external APIs Database: - Migration: add WSDC fields (firstName, lastName, wsdcId) - Migration: add email verification fields (token, code, expiry) - Migration: add password reset fields (token, expiry) Documentation: - Complete Phase 1.5 documentation - Test suite documentation and best practices - Updated session context with new features
This commit is contained in:
70
frontend/src/components/common/PasswordStrengthIndicator.jsx
Normal file
70
frontend/src/components/common/PasswordStrengthIndicator.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Password Strength Indicator Component
|
||||
* Calculates and displays password strength with visual feedback
|
||||
*/
|
||||
const PasswordStrengthIndicator = ({ password }) => {
|
||||
const strength = useMemo(() => {
|
||||
if (!password) return { score: 0, label: '', color: '' };
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Length check
|
||||
if (password.length >= 8) score++;
|
||||
if (password.length >= 12) score++;
|
||||
|
||||
// Character variety checks
|
||||
if (/[a-z]/.test(password)) score++; // lowercase
|
||||
if (/[A-Z]/.test(password)) score++; // uppercase
|
||||
if (/[0-9]/.test(password)) score++; // numbers
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score++; // special chars
|
||||
|
||||
// Determine label and color
|
||||
if (score <= 2) {
|
||||
return { score, label: 'Weak', color: 'bg-red-500' };
|
||||
} else if (score <= 4) {
|
||||
return { score, label: 'Medium', color: 'bg-yellow-500' };
|
||||
} else {
|
||||
return { score, label: 'Strong', color: 'bg-green-500' };
|
||||
}
|
||||
}, [password]);
|
||||
|
||||
if (!password) return null;
|
||||
|
||||
const widthPercentage = (strength.score / 6) * 100;
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-gray-600">Password strength:</span>
|
||||
<span className={`text-xs font-medium ${
|
||||
strength.label === 'Weak' ? 'text-red-600' :
|
||||
strength.label === 'Medium' ? 'text-yellow-600' :
|
||||
'text-green-600'
|
||||
}`}>
|
||||
{strength.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${strength.color} transition-all duration-300 ease-out`}
|
||||
style={{ width: `${widthPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<ul className="mt-2 text-xs text-gray-600 space-y-1">
|
||||
<li className={password.length >= 8 ? 'text-green-600' : ''}>
|
||||
✓ At least 8 characters
|
||||
</li>
|
||||
<li className={/[A-Z]/.test(password) && /[a-z]/.test(password) ? 'text-green-600' : ''}>
|
||||
✓ Upper and lowercase letters
|
||||
</li>
|
||||
<li className={/[0-9]/.test(password) ? 'text-green-600' : ''}>
|
||||
✓ At least one number
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordStrengthIndicator;
|
||||
90
frontend/src/components/common/VerificationBanner.jsx
Normal file
90
frontend/src/components/common/VerificationBanner.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { authAPI } from '../../services/api';
|
||||
import { AlertCircle, X, Mail, Loader2 } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Verification Banner Component
|
||||
* Displays a banner for unverified users with option to resend verification email
|
||||
*/
|
||||
const VerificationBanner = () => {
|
||||
const { user } = useAuth();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
// Don't show if user is verified or banner is dismissed
|
||||
if (!user || user.emailVerified || dismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleResend = async () => {
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
|
||||
try {
|
||||
await authAPI.resendVerification(user.email);
|
||||
setMessage('Verification email sent! Please check your inbox.');
|
||||
} catch (error) {
|
||||
setMessage('Failed to send email. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-yellow-50 border-b border-yellow-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="py-3 flex items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<AlertCircle className="w-5 h-5 text-yellow-600 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-yellow-800">
|
||||
Please verify your email address to access all features
|
||||
</p>
|
||||
{message && (
|
||||
<p className="text-xs text-yellow-700 mt-1">{message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
to="/verify-email"
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-yellow-800 bg-yellow-100 hover:bg-yellow-200 rounded-md transition"
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
Verify Now
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleResend}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-yellow-800 hover:text-yellow-900 disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
'Resend Email'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
className="p-1 text-yellow-600 hover:text-yellow-800 transition"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationBanner;
|
||||
Reference in New Issue
Block a user