feat: add email verification resend functionality to login page and backend route
All checks were successful
Build and Push App / build (push) Successful in 5m35s
All checks were successful
Build and Push App / build (push) Successful in 5m35s
This commit is contained in:
98
app/api/auth/resend-verification/route.ts
Normal file
98
app/api/auth/resend-verification/route.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { email } = await request.json();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Email requis' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// For security, same message even if user doesn't exist
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: 'Si un compte existe pour cet email et n\'est pas encore vérifié, un nouvel email de vérification a été envoyé.' },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.emailVerified) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Ce compte est déjà vérifié.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new verification token
|
||||||
|
const verificationToken = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
// Update user with new token
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { verificationToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send verification email
|
||||||
|
try {
|
||||||
|
const settings = await prisma.siteSetting.findUnique({
|
||||||
|
where: { id: 'singleton' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const siteName = settings?.siteName || 'Afrohub';
|
||||||
|
const subject = settings?.verifyEmailSubject || 'Vérification de votre compte - Afrohub';
|
||||||
|
const verifyUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/auth/verify?token=${verificationToken}`;
|
||||||
|
|
||||||
|
let html = settings?.verifyEmailTemplate || `
|
||||||
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
|
||||||
|
<h2 style="color: #0d9488; text-align: center;">{siteName}</h2>
|
||||||
|
<p>Bonjour {name},</p>
|
||||||
|
<p>Vous avez demandé le renvoi de l'email de vérification pour votre compte sur {siteName}. Veuillez vérifier votre adresse email en cliquant sur le bouton ci-dessous :</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="{verifyUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Vérifier mon compte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Replace placeholders
|
||||||
|
html = html
|
||||||
|
.replace(/{name}/g, user.name)
|
||||||
|
.replace(/{verifyUrl}/g, verifyUrl)
|
||||||
|
.replace(/{siteName}/g, siteName);
|
||||||
|
|
||||||
|
const { sendEmail } = await import('@/lib/mail');
|
||||||
|
await sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
} catch (mailError) {
|
||||||
|
console.error('Failed to resend verification email:', mailError);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erreur lors de l\'envoi de l\'email' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: 'Un nouvel email de vérification a été envoyé.' },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Resend verification error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Une erreur est survenue' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,10 @@ const LoginPage = () => {
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [isUnverified, setIsUnverified] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [resendLoading, setResendLoading] = useState(false);
|
||||||
|
const [resendMessage, setResendMessage] = useState('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
@@ -24,6 +27,8 @@ const LoginPage = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
setIsUnverified(false);
|
||||||
|
setResendMessage('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/login', {
|
const res = await fetch('/api/auth/login', {
|
||||||
@@ -35,6 +40,9 @@ const LoginPage = () => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 403 && data.error?.includes('vérifier')) {
|
||||||
|
setIsUnverified(true);
|
||||||
|
}
|
||||||
throw new Error(data.error || 'Erreur lors de la connexion');
|
throw new Error(data.error || 'Erreur lors de la connexion');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +56,33 @@ const LoginPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResendVerification = async () => {
|
||||||
|
setResendLoading(true);
|
||||||
|
setError('');
|
||||||
|
setResendMessage('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/resend-verification', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.error || "Erreur lors de l'envoi");
|
||||||
|
}
|
||||||
|
|
||||||
|
setResendMessage(data.message);
|
||||||
|
setIsUnverified(false); // Hide the resend link after successful send
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setResendLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[80vh] flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
<div className="min-h-[80vh] flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="max-w-md w-full space-y-8">
|
<div className="max-w-md w-full space-y-8">
|
||||||
@@ -69,11 +104,30 @@ const LoginPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{resendMessage && (
|
||||||
|
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 rounded flex items-center gap-3 text-blue-700 text-sm">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-blue-500 flex-shrink-0" />
|
||||||
|
<p>{resendMessage}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{(error || queryError) && (
|
{(error || queryError) && (
|
||||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded flex items-center gap-3 text-red-700 text-sm">
|
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded flex flex-col gap-2 text-red-700 text-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
|
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
|
||||||
<p>{error || queryError}</p>
|
<p>{error || queryError}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{isUnverified && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleResendVerification}
|
||||||
|
disabled={resendLoading}
|
||||||
|
className="ml-8 text-left font-bold underline hover:text-red-800 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{resendLoading ? 'Envoi en cours...' : "Renvoyer l'email de vérification"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
|
|||||||
Reference in New Issue
Block a user