feat: Implement password reset and email verification with Mailgun
Backend changes: - Add password reset token database migration (V011) - Create email service with Mailgun integration (EU/US regions) - Implement password reset flow with secure token generation - Add email verification endpoints and logic - Create beautiful HTML email templates for reset and verification - Add password reset DTOs with validation - Update User entity with email verification fields Frontend changes: - Create forgot password page with email submission - Create reset password page with token validation - Add email verification banner component - Integrate verification banner into main dashboard - Add password requirements and validation UI Features: - Mailgun API ready for EU and US regions - Secure token expiration (1h for reset, 24h for verification) - Rate limiting on resend (2min interval) - Protection against email enumeration - IP address and user agent tracking - Token reuse prevention 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
245
maternal-web/app/(auth)/forgot-password/page.tsx
Normal file
245
maternal-web/app/(auth)/forgot-password/page.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Paper,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Link as MuiLink,
|
||||
} from '@mui/material';
|
||||
import { Email, ArrowBack } from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email.trim()) {
|
||||
setError('Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await apiClient.post('/api/v1/auth/password/forgot', { email });
|
||||
setSuccess(true);
|
||||
} catch (err: any) {
|
||||
console.error('Forgot password error:', err);
|
||||
setError(err.response?.data?.message || 'Failed to send reset email. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #FFE4E1 0%, #FFDAB9 100%)',
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
maxWidth: 480,
|
||||
width: '100%',
|
||||
p: 4,
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
{!success ? (
|
||||
<>
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'primary.light',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
mx: 'auto',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Email sx={{ fontSize: 32, color: 'primary.main' }} />
|
||||
</Box>
|
||||
<Typography variant="h4" fontWeight="600" gutterBottom>
|
||||
Forgot Password?
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No worries! Enter your email address and we'll send you a link to reset your password.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email Address"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={loading}
|
||||
required
|
||||
autoFocus
|
||||
sx={{
|
||||
mb: 3,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 3,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={loading}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
py: 1.5,
|
||||
textTransform: 'none',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} color="inherit" /> : 'Send Reset Link'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
||||
<MuiLink
|
||||
component={Link}
|
||||
href="/login"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
color: 'primary.main',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBack sx={{ fontSize: 18 }} />
|
||||
Back to Login
|
||||
</MuiLink>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'success.light',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
mx: 'auto',
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Email sx={{ fontSize: 32, color: 'success.main' }} />
|
||||
</Box>
|
||||
|
||||
<Typography variant="h5" fontWeight="600" gutterBottom>
|
||||
Check Your Email 📧
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
If an account with that email exists, we've sent you a password reset link. Please check your inbox and follow the instructions.
|
||||
</Typography>
|
||||
|
||||
<Alert severity="info" sx={{ mb: 3, borderRadius: 2, textAlign: 'left' }}>
|
||||
<Typography variant="body2" fontWeight="600" gutterBottom>
|
||||
Didn't receive the email?
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
• Check your spam or junk folder
|
||||
<br />
|
||||
• Make sure you entered the correct email
|
||||
<br />
|
||||
• The link expires in 1 hour
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setSuccess(false);
|
||||
setEmail('');
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
py: 1.5,
|
||||
textTransform: 'none',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
Try Another Email
|
||||
</Button>
|
||||
|
||||
<MuiLink
|
||||
component={Link}
|
||||
href="/login"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
color: 'primary.main',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBack sx={{ fontSize: 18 }} />
|
||||
Back to Login
|
||||
</MuiLink>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user