Fix first-login username flow
这个提交包含在:
@@ -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<ReturnType<typeof loginMutation.mutateAsync>>["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 (
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户