"use client"; import { useConvexAuth, useMutation, useQuery } from "convex/react"; import { useAuthActions } from "@convex-dev/auth/react"; import { useState, useCallback, useEffect } from "react"; import { useRouter } from "next/navigation"; import { isConvexEnabled } from "@/lib/convex/enabled"; import { api } from "@/lib/convex/api"; import { getUserFacingErrorMessage } from "@/lib/auth/errors"; type AuthStep = "signIn" | "signUp"; // 测试账号凭据常量 const TEST_CREDENTIALS = { email: "mnote.e2e@example.com", password: "MnoteE2E123!", } as const; /** * Convex Auth 登录/注册页面 * * 支持功能: * - 邮箱密码登录 * - 邮箱密码注册 */ export default function AuthPage() { const { isLoading, isAuthenticated } = useConvexAuth(); const { signIn } = useAuthActions(); const setMyUsername = useMutation(api.users.setMyUsername); const router = useRouter(); const currentUser = useQuery(api.users.currentUser, isAuthenticated ? {} : "skip"); const navigateToHome = useCallback(() => { if (typeof window !== "undefined") { window.location.assign("/"); return; } router.replace("/"); }, [router]); useEffect(() => { if (isLoading) return; if (!isAuthenticated) return; if (currentUser === undefined) return; if (currentUser && currentUser.name) { navigateToHome(); } }, [currentUser, isAuthenticated, isLoading, navigateToHome]); const [flow, setFlow] = useState("signIn"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [username, setUsername] = useState(""); const [pendingUsernameToSave, setPendingUsernameToSave] = useState(null); const [isSavingUsername, setIsSavingUsername] = useState(false); const [message, setMessage] = useState<{ type: "success" | "error" | "info"; text: string; } | null>(null); // 注册完成后,等待登录态同步完成再写入用户名,避免出现“未登录”的竞态 useEffect(() => { if (!pendingUsernameToSave) return; if (!isAuthenticated) return; if (currentUser === undefined || currentUser === null) return; if (currentUser.name) { setPendingUsernameToSave(null); return; } let cancelled = false; setIsSavingUsername(true); (async () => { try { await setMyUsername({ username: pendingUsernameToSave }); if (cancelled) return; setPendingUsernameToSave(null); setMessage({ type: "success", text: "注册成功!" }); navigateToHome(); } catch (err: unknown) { if (cancelled) return; setPendingUsernameToSave(null); setMessage({ type: "error", text: getUserFacingErrorMessage(err, "用户名设置失败,请重试") }); } finally { if (!cancelled) setIsSavingUsername(false); } })(); return () => { cancelled = true; }; }, [currentUser, isAuthenticated, navigateToHome, pendingUsernameToSave, setMyUsername]); // 执行登录的核心逻辑 const performSignIn = useCallback(async (email: string, password: string, flow: AuthStep) => { setMessage(null); try { if (flow === "signUp" && !username.trim()) { setMessage({ type: "error", text: "用户名不能为空" }); return; } const formData = new FormData(); formData.append("email", email); formData.append("password", password); formData.append("flow", flow); const result = await signIn("password", formData); if (result.signingIn) { if (flow === "signUp") { // 说明:注册已成功,但此刻登录态可能尚未同步到 Convex functions。 // 这里先进入“待写入用户名”状态,等 currentUser 可用后再写,避免出现“未登录”的竞态。 setMessage({ type: "info", text: "注册成功,正在保存用户名..." }); setPendingUsernameToSave(username.trim()); return; } // 说明:登录成功后不要立刻强推 `/`。 // Convex Auth cookie 与 currentUser 查询存在短暂同步窗口,过早跳转会把页面推进到根路由空白态。 // 这里仅提示“正在同步”,真正跳转交给上面的 useEffect 在登录态稳定后执行。 setMessage({ type: "info", text: "登录成功,正在同步登录状态..." }); return; } // 说明:部分认证方式会返回 redirect(例如 OAuth);密码模式一般不会走到这里。 if (result.redirect) { setMessage({ type: "info", text: "正在跳转..." }); router.push(result.redirect.toString()); return; } setMessage({ type: "error", text: "操作失败,请重试" }); } catch (error: any) { setMessage({ type: "error", text: getUserFacingErrorMessage(error, "操作失败,请重试") }); } }, [router, signIn, username]); const handleSubmit = useCallback(async (event: React.FormEvent) => { event.preventDefault(); await performSignIn(email, password, flow); }, [email, password, flow, performSignIn]); // 检查是否启用了 Convex const isConvex = isConvexEnabled(); if (!isConvex) { return (

认证功能不可用

请启用 Convex 后端后再试

); } if (isLoading) { return (

加载中...

); } if (isAuthenticated && currentUser && currentUser.name) { return (

正在跳转...

); } if (isAuthenticated) { if (currentUser === undefined || currentUser === null) { return (

正在同步登录状态...

); } const saveUsername = async () => { setMessage(null); try { if (isSavingUsername) return; setIsSavingUsername(true); await setMyUsername({ username: username.trim() }); setMessage({ type: "success", text: "用户名已保存!" }); navigateToHome(); } catch (error: any) { setMessage({ type: "error", text: getUserFacingErrorMessage(error, "保存失败,请重试") }); } finally { setIsSavingUsername(false); } }; return (

设置用户名

用于分享功能的用户查找(全局唯一)

{message && (

{message.text}

)}
setUsername(e.target.value)} disabled={isSavingUsername} className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" placeholder="用户名(2-32 位,不含空格)" />
); } return (

{flow === "signIn" ? "登录账户" : "创建账户"}

使用 Convex Auth

{message && (

{message.text}

)}
setEmail(e.target.value)} className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="邮箱地址" />
{flow === "signUp" && (
setUsername(e.target.value)} className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="用户名(2-32 位,不含空格)" />
)}
setPassword(e.target.value)} className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="密码(至少 8 位)" />
{flow === "signIn" && (
)}
); }