feat: add email verification
This commit is contained in:
@@ -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={
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -338,6 +338,15 @@
|
||||
"dontHaveAccount": "还没有账号?",
|
||||
"alreadyHaveAccount": "已经有账号了?",
|
||||
"registrationSuccess": "注册成功!请登录。",
|
||||
"registrationSuccessWithVerification": "注册成功!请前往邮箱点击验证链接激活账号。",
|
||||
"inactiveUserError": "账号未激活,请前往邮箱激活。",
|
||||
"resendVerification": "重新发送验证邮件",
|
||||
"verificationSent": "验证邮件已发送,请查收。",
|
||||
"verifyEmailTitle": "邮箱验证",
|
||||
"verifyingEmail": "正在验证您的邮箱...",
|
||||
"verifyEmailSuccess": "邮箱验证成功!您现在可以登录了。",
|
||||
"verifyEmailFailed": "邮箱验证失败或链接已过期。",
|
||||
"goToLogin": "前往登录",
|
||||
"errorOccurred": "发生了一个错误",
|
||||
"mcpConfig": "MCP 配置",
|
||||
"mcp": "MCP",
|
||||
|
||||
@@ -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