Add email verification handling and resend functionality to login
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m2s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m2s
This commit is contained in:
@@ -1,19 +1,43 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import { Mail, Lock, ArrowRight, Loader2 } from 'lucide-react';
|
import { Mail, Lock, ArrowRight, Loader2, AlertTriangle, RefreshCw } from 'lucide-react';
|
||||||
import BrandLogo from '../components/BrandLogo';
|
import BrandLogo from '../components/BrandLogo';
|
||||||
|
import api from '../services/api';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [needsVerification, setNeedsVerification] = useState(false);
|
||||||
|
const [resendCooldown, setResendCooldown] = useState(0);
|
||||||
|
const [resending, setResending] = useState(false);
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resendCooldown <= 0) return;
|
||||||
|
const timer = setTimeout(() => setResendCooldown(c => c - 1), 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [resendCooldown]);
|
||||||
|
|
||||||
|
const handleResend = async () => {
|
||||||
|
if (resendCooldown > 0 || resending) return;
|
||||||
|
setResending(true);
|
||||||
|
try {
|
||||||
|
await api.post('/auth/resend-verification', { email });
|
||||||
|
toast.success(t('auth.emailVerificationResendSuccess'));
|
||||||
|
setResendCooldown(60);
|
||||||
|
} catch {
|
||||||
|
toast.error(t('auth.emailVerificationResendFailed'));
|
||||||
|
} finally {
|
||||||
|
setResending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -22,7 +46,11 @@ export default function Login() {
|
|||||||
toast.success(t('auth.loginSuccess'));
|
toast.success(t('auth.loginSuccess'));
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err.response?.data?.needsVerification) {
|
||||||
|
setNeedsVerification(true);
|
||||||
|
} else {
|
||||||
toast.error(err.response?.data?.error || t('auth.loginFailed'));
|
toast.error(err.response?.data?.error || t('auth.loginFailed'));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -101,6 +129,25 @@ export default function Login() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{needsVerification && (
|
||||||
|
<div className="mt-4 p-4 rounded-xl bg-amber-500/10 border border-amber-500/30 space-y-2">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertTriangle size={16} className="text-amber-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-amber-200">{t('auth.emailVerificationBanner')}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleResend}
|
||||||
|
disabled={resendCooldown > 0 || resending}
|
||||||
|
className="flex items-center gap-1.5 text-sm text-amber-400 hover:text-amber-300 underline underline-offset-2 transition-colors disabled:opacity-60 disabled:no-underline disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<RefreshCw size={13} className={resending ? 'animate-spin' : ''} />
|
||||||
|
{resendCooldown > 0
|
||||||
|
? t('auth.emailVerificationResendCooldown').replace('{seconds}', resendCooldown)
|
||||||
|
: t('auth.emailVerificationResend')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="mt-6 text-center text-sm text-th-text-s">
|
<p className="mt-6 text-center text-sm text-th-text-s">
|
||||||
{t('auth.noAccount')}{' '}
|
{t('auth.noAccount')}{' '}
|
||||||
<Link to="/register" className="text-th-accent hover:underline font-medium">
|
<Link to="/register" className="text-th-accent hover:underline font-medium">
|
||||||
|
|||||||
Reference in New Issue
Block a user