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:
117
maternal-web/components/common/EmailVerificationBanner.tsx
Normal file
117
maternal-web/components/common/EmailVerificationBanner.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Alert, Button, Snackbar, Box } from '@mui/material';
|
||||
import { Email } from '@mui/icons-material';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
export const EmailVerificationBanner: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'success',
|
||||
});
|
||||
|
||||
// Don't show if user is not logged in, email is verified, or banner was dismissed
|
||||
if (!user || user.emailVerified || dismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleResendVerification = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await apiClient.post('/api/v1/auth/email/send-verification');
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: 'Verification email sent! Please check your inbox.',
|
||||
severity: 'success',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to resend verification email:', error);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.response?.data?.message || 'Failed to send verification email. Please try again.',
|
||||
severity: 'error',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
setDismissed(true);
|
||||
// Store dismissal in localStorage to persist across sessions
|
||||
localStorage.setItem('emailVerificationBannerDismissed', 'true');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
severity="warning"
|
||||
icon={<Email />}
|
||||
onClose={handleDismiss}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
mb: 2,
|
||||
'& .MuiAlert-message': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<strong>Verify your email address</strong>
|
||||
<br />
|
||||
Please check your inbox and click the verification link to access all features.
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleResendVerification}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
borderColor: 'warning.main',
|
||||
color: 'warning.dark',
|
||||
'&:hover': {
|
||||
borderColor: 'warning.dark',
|
||||
bgcolor: 'warning.light',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? 'Sending...' : 'Resend Email'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
||||
<Snackbar
|
||||
open={snackbar.open}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
severity={snackbar.severity}
|
||||
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
{snackbar.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user