feat: add email verification
This commit is contained in:
@@ -19,6 +19,9 @@ export function Login() {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [isInactiveError, setIsInactiveError] = useState(false);
|
||||
const [resendStatus, setResendStatus] = useState("");
|
||||
const [isResending, setIsResending] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const login = useAuthStore((state) => state.login);
|
||||
|
||||
@@ -28,9 +31,26 @@ export function Login() {
|
||||
password: "",
|
||||
});
|
||||
|
||||
const handleResendVerification = async () => {
|
||||
setIsResending(true);
|
||||
setResendStatus("");
|
||||
try {
|
||||
await api.post("/api/v1/auth/resend-verification", {
|
||||
username: formData.username,
|
||||
});
|
||||
setResendStatus(t("verificationSent"));
|
||||
} catch (err: any) {
|
||||
setResendStatus(err.message || t("errorOccurred"));
|
||||
} finally {
|
||||
setIsResending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setIsInactiveError(false);
|
||||
setResendStatus("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
@@ -66,10 +86,17 @@ export function Login() {
|
||||
|
||||
// Auto login after successful registration
|
||||
setIsLogin(true);
|
||||
setError(t("registrationSuccess"));
|
||||
// Assuming backend returns is_active=false for users requiring verification
|
||||
// For now, we will show the verification prompt based on the translation
|
||||
setError(t("registrationSuccessWithVerification"));
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || t("errorOccurred"));
|
||||
if (err.message && err.message.toLowerCase().includes("inactive user")) {
|
||||
setError(t("inactiveUserError"));
|
||||
setIsInactiveError(true);
|
||||
} else {
|
||||
setError(err.message || t("errorOccurred"));
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -108,8 +135,27 @@ export function Login() {
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className={`p-3 rounded-lg mb-6 text-sm ${error.includes(t("registrationSuccess")) ? "bg-emerald-50 text-emerald-600" : "bg-red-50 text-red-600"}`}>
|
||||
{error}
|
||||
<div className={`p-3 rounded-lg mb-6 text-sm flex flex-col gap-2 ${error.includes(t("registrationSuccessWithVerification")) ? "bg-emerald-50 text-emerald-600" : "bg-red-50 text-red-600"}`}>
|
||||
<span>{error}</span>
|
||||
{isInactiveError && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleResendVerification}
|
||||
disabled={isResending}
|
||||
className="w-fit mt-1 border-red-200 text-red-700 hover:bg-red-100"
|
||||
>
|
||||
{isResending ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
|
||||
{t("resendVerification")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resendStatus && (
|
||||
<div className={`p-3 rounded-lg mb-6 text-sm ${resendStatus.includes(t("verificationSent")) ? "bg-emerald-50 text-emerald-600" : "bg-red-50 text-red-600"}`}>
|
||||
{resendStatus}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export function VerifyEmail() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const hasAttempted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get("token");
|
||||
if (!token) {
|
||||
setStatus("error");
|
||||
setErrorMessage(t("verifyEmailFailed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAttempted.current) return;
|
||||
hasAttempted.current = true;
|
||||
|
||||
const verifyToken = async () => {
|
||||
try {
|
||||
await api.get(`/api/v1/auth/verify-email?token=${encodeURIComponent(token)}`);
|
||||
setStatus("success");
|
||||
} catch (err: any) {
|
||||
setStatus("error");
|
||||
setErrorMessage(err.message || t("verifyEmailFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
verifyToken();
|
||||
}, [searchParams, t]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-muted/50 px-4">
|
||||
<div className="w-full max-w-md bg-background rounded-2xl shadow-xl border border-border p-8 text-center">
|
||||
<div className="mb-6 flex justify-center">
|
||||
{status === "loading" && <Loader2 className="h-16 w-16 text-indigo-600 animate-spin" />}
|
||||
{status === "success" && <CheckCircle2 className="h-16 w-16 text-emerald-500" />}
|
||||
{status === "error" && <XCircle className="h-16 w-16 text-red-500" />}
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-foreground/90 mb-4">
|
||||
{t("verifyEmailTitle")}
|
||||
</h2>
|
||||
|
||||
<div className="mb-8 text-muted-foreground">
|
||||
{status === "loading" && <p>{t("verifyingEmail")}</p>}
|
||||
{status === "success" && <p className="text-emerald-600 font-medium">{t("verifyEmailSuccess")}</p>}
|
||||
{status === "error" && <p className="text-red-600 font-medium">{errorMessage}</p>}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate("/login")}
|
||||
className="w-full h-11 bg-indigo-600 hover:bg-indigo-700 text-primary-foreground font-medium text-base rounded-xl transition-all shadow-md"
|
||||
>
|
||||
{t("goToLogin")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user