From bcdd790d913a5a58c703e4f1e3f6881c4df50a59 Mon Sep 17 00:00:00 2001 From: cryptocommuniums-afk Date: Sat, 14 Mar 2026 22:37:15 +0800 Subject: [PATCH] Fix first-login username flow --- client/src/pages/Login.tsx | 40 ++++++++++++++++++++++++++---------- tests/e2e/app.spec.ts | 5 ++++- tests/e2e/helpers/mockApp.ts | 7 +++++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 8458faa..3b091ce 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -10,23 +10,41 @@ import { Target, Loader2 } from "lucide-react"; export default function Login() { const [username, setUsername] = useState(""); const [, setLocation] = useLocation(); - const loginMutation = trpc.auth.loginWithUsername.useMutation({ - onSuccess: (data) => { - toast.success(data.isNew ? `欢迎加入,${data.user.name}!` : `欢迎回来,${data.user.name}!`); - setLocation("/dashboard"); - }, - onError: (err) => { - toast.error("登录失败: " + err.message); - }, - }); + const utils = trpc.useUtils(); + const loginMutation = trpc.auth.loginWithUsername.useMutation(); - const handleLogin = (e: React.FormEvent) => { + const syncAuthenticatedUser = async (fallbackUser: Awaited>["user"]) => { + // Seed the cache immediately so protected routes do not bounce back to /login. + utils.auth.me.setData(undefined, fallbackUser); + + for (let attempt = 0; attempt < 3; attempt++) { + const user = await utils.auth.me.fetch(); + if (user) { + utils.auth.me.setData(undefined, user); + return user; + } + await new Promise(resolve => window.setTimeout(resolve, 120 * (attempt + 1))); + } + + return fallbackUser; + }; + + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); if (!username.trim()) { toast.error("请输入用户名"); return; } - loginMutation.mutate({ username: username.trim() }); + + try { + const data = await loginMutation.mutateAsync({ username: username.trim() }); + const user = await syncAuthenticatedUser(data.user); + toast.success(data.isNew ? `欢迎加入,${user.name}!` : `欢迎回来,${user.name}!`); + setLocation("/dashboard"); + } catch (err) { + const message = err instanceof Error ? err.message : "未知错误"; + toast.error("登录失败: " + message); + } }; return ( diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts index 2d34a9b..15716f9 100644 --- a/tests/e2e/app.spec.ts +++ b/tests/e2e/app.spec.ts @@ -2,7 +2,10 @@ import { expect, test } from "@playwright/test"; import { installAppMocks } from "./helpers/mockApp"; test("login redirects into dashboard with mocked auth", async ({ page }) => { - await installAppMocks(page, { authenticated: false }); + await installAppMocks(page, { + authenticated: false, + authMeNullResponsesAfterLogin: 1, + }); await page.goto("/login"); await expect(page.getByTestId("login-title")).toBeVisible(); diff --git a/tests/e2e/helpers/mockApp.ts b/tests/e2e/helpers/mockApp.ts index 816f9de..b58cbb5 100644 --- a/tests/e2e/helpers/mockApp.ts +++ b/tests/e2e/helpers/mockApp.ts @@ -61,6 +61,7 @@ type MockAppState = { analyses: any[]; mediaSession: MockMediaSession | null; nextVideoId: number; + authMeNullResponsesAfterLogin: number; }; function trpcResult(json: unknown) { @@ -154,6 +155,10 @@ async function handleTrpc(route: Route, state: MockAppState) { const results = operations.map((operation) => { switch (operation) { case "auth.me": + if (state.authenticated && state.authMeNullResponsesAfterLogin > 0) { + state.authMeNullResponsesAfterLogin -= 1; + return trpcResult(null); + } return trpcResult(state.authenticated ? state.user : null); case "auth.loginWithUsername": state.authenticated = true; @@ -282,6 +287,7 @@ export async function installAppMocks( videos?: any[]; analyses?: any[]; userName?: string; + authMeNullResponsesAfterLogin?: number; } ) { const state: MockAppState = { @@ -312,6 +318,7 @@ export async function installAppMocks( ], mediaSession: null, nextVideoId: 100, + authMeNullResponsesAfterLogin: options?.authMeNullResponsesAfterLogin ?? 0, }; await page.addInitScript(() => {