feat: add email verification

This commit is contained in:
qixinbo
2026-03-29 17:12:46 +08:00
parent 320817e6db
commit 551898c19b
16 changed files with 344 additions and 15 deletions
+2
View File
@@ -14,6 +14,7 @@ import { KnowledgeBases } from "./pages/KnowledgeBases";
import { DataSources } from "./pages/DataSources";
import { Modeling } from "./pages/Modeling";
import { Subagents } from "./pages/Subagents";
import { VerifyEmail } from "./pages/VerifyEmail";
import { useAuthStore } from "./store/authStore";
import { ThemeToggle } from "./components/ThemeToggle";
@@ -62,6 +63,7 @@ function App() {
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/verify-email" element={<VerifyEmail />} />
{/* Protected Routes */}
<Route path="/" element={
+9
View File
@@ -338,6 +338,15 @@
"dontHaveAccount": "Don't have an account?",
"alreadyHaveAccount": "Already have an account?",
"registrationSuccess": "Registration successful! Please login.",
"registrationSuccessWithVerification": "Registration successful! Please check your email to verify your account.",
"inactiveUserError": "Account is inactive. Please verify your email.",
"resendVerification": "Resend Verification Email",
"verificationSent": "Verification email sent. Please check your inbox.",
"verifyEmailTitle": "Email Verification",
"verifyingEmail": "Verifying your email...",
"verifyEmailSuccess": "Email verified successfully! You can now log in.",
"verifyEmailFailed": "Email verification failed or link expired.",
"goToLogin": "Go to Login",
"errorOccurred": "An error occurred",
"mcpConfig": "MCP Configuration",
"mcp": "MCP",
+9
View File
@@ -338,6 +338,15 @@
"dontHaveAccount": "还没有账号?",
"alreadyHaveAccount": "已经有账号了?",
"registrationSuccess": "注册成功!请登录。",
"registrationSuccessWithVerification": "注册成功!请前往邮箱点击验证链接激活账号。",
"inactiveUserError": "账号未激活,请前往邮箱激活。",
"resendVerification": "重新发送验证邮件",
"verificationSent": "验证邮件已发送,请查收。",
"verifyEmailTitle": "邮箱验证",
"verifyingEmail": "正在验证您的邮箱...",
"verifyEmailSuccess": "邮箱验证成功!您现在可以登录了。",
"verifyEmailFailed": "邮箱验证失败或链接已过期。",
"goToLogin": "前往登录",
"errorOccurred": "发生了一个错误",
"mcpConfig": "MCP 配置",
"mcp": "MCP",
+50 -4
View File
@@ -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>
)}
+69
View File
@@ -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>
);
}