From 2475cb3e452a0b2de7d757d9a6318072546e8c17 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 11 Jun 2026 13:23:29 +0800 Subject: [PATCH] feat(auth): add startup auth status check with device id fallback Add AuthStatusChecker mounted in RootProviders to dispatch AuthStatusCheckSubmitted on mount. The new checkAuthStatusActor retrieves the device id, checks for an existing login or guest token, and falls back to a guest login API call when neither is present. Wires the new event/actor through the auth machine to enable automatic session restoration and guest-mode bootstrap. --- src/providers/root-providers.tsx | 10 +++-- src/stores/auth/auth-events.ts | 2 + src/stores/auth/auth-machine.ts | 58 +++++++++++++++++++++++++ src/stores/auth/auth-status-checker.tsx | 25 +++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 src/stores/auth/auth-status-checker.tsx diff --git a/src/providers/root-providers.tsx b/src/providers/root-providers.tsx index 3dbbfb93..a4971008 100644 --- a/src/providers/root-providers.tsx +++ b/src/providers/root-providers.tsx @@ -5,7 +5,8 @@ * * 把所有功能 Provider 串起来: * AuthProvider → UserProvider → SidebarProvider → ChatProvider - * + OAuthSessionSync 桥接器(监听 NextAuth session → auth machine) + * + AuthStatusChecker(启动时一次:派发 AuthStatusCheckSubmitted) + * + OAuthSessionSync (持续监听 NextAuth session → auth machine) * * 它们始终挂载,保证各页面直接 `use*State()` / `use*Dispatch()` 即可, * 无需在每个页面单独包裹。 @@ -14,6 +15,7 @@ import type { ReactNode } from "react"; import { AuthProvider } from "@/stores/auth/auth-context"; +import { AuthStatusChecker } from "@/stores/auth/auth-status-checker"; import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync"; import { ChatProvider } from "@/stores/chat/chat-context"; import { SidebarProvider } from "@/stores/sidebar/sidebar-context"; @@ -26,8 +28,10 @@ export interface RootProvidersProps { export function RootProviders({ children }: RootProvidersProps) { return ( - {/* OAuthSessionSync 必须放在 AuthProvider 内部(依赖 useAuthDispatch/useAuthState), - * 也必须在 ChatProvider 外部(ChatProvider 内部组件可能在 sync 完成前就需要登录态) */} + {/* AuthStatusChecker 必须在 AuthProvider 内部(依赖 useAuthDispatch), + * 必须在 UserProvider/ChatProvider 外部(它们可能在 init 完成前就 mount) */} + + {/* OAuthSessionSync 同样依赖 AuthProvider 上下文 */} diff --git a/src/stores/auth/auth-events.ts b/src/stores/auth/auth-events.ts index 6bfc0531..e16cac15 100644 --- a/src/stores/auth/auth-events.ts +++ b/src/stores/auth/auth-events.ts @@ -13,6 +13,8 @@ export type AuthEvent = | { type: "AuthModeChanged"; mode: AuthMode } | { type: "AuthFormCleared" } | { type: "AuthReset" } + /** 启动 / 恢复时检查登录态 —— 拿 deviceId → 查 token → 必要时游客登录 */ + | { type: "AuthStatusCheckSubmitted" } // 业务事件(提交) | { type: "AuthEmailLoginSubmitted"; email: string; password: string } | { diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 095e5c40..8a1537d6 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -15,6 +15,7 @@ import type { AuthProvider } from "@/lib/auth/auth_platform"; import type { LoginStatus } from "@/models/auth/login-status"; import { authRepository } from "@/data/repositories/auth_repository"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; +import { deviceIdentifier } from "@/utils/device_identifier"; import { Result } from "@/utils/result"; import { AuthState, initialState } from "./auth-state"; @@ -108,6 +109,39 @@ const syncFacebookBackendActor = fromPromise< return "facebook" as LoginStatus; }); +// ========== 新增:启动 / 恢复时检查登录态 actor ========== +// 由 在 mount 时派发(一次性)。 +// 流程: +// 1. deviceIdentifier.getDeviceId() —— 无则通过 fingerprintjs 生成 + 落盘 +// 2. AuthStorage.getLoginToken() —— 有 → "email"(具体 provider 由 OAuthSessionSync 后续覆盖) +// 3. AuthStorage.getGuestToken() —— 有 → "guest" +// 4. 都没有 → authRepository.guestLogin(deviceId) 内部自动 setGuestToken + setDeviceId + setUserId + +const checkAuthStatusActor = fromPromise(async () => { + // 1. 拿 deviceId + const deviceId = await deviceIdentifier.getDeviceId(); + const storage = AuthStorage.getInstance(); + + // 2. 查 loginToken(OAuth / 邮箱登录后端写下的) + const loginTokenR = await storage.getLoginToken(); + if (loginTokenR.success && loginTokenR.data) { + // token 字符串无法区分 google/facebook/email/apple + // 默认 "email" —— 若实际是 OAuth,OAuthSessionSync 后续会通过 AuthGoogle/FacebookSyncSubmitted 覆盖 + return "email" as LoginStatus; + } + + // 3. 查 guestToken + const guestTokenR = await storage.getGuestToken(); + if (guestTokenR.success && guestTokenR.data) { + return "guest" as LoginStatus; + } + + // 4. 都没有 —— 调游客登录接口(内部自动 setGuestToken + setDeviceId + setUserId) + const result = await authRepository.guestLogin(deviceId); + if (Result.isErr(result)) throw result.error; + return "guest" as LoginStatus; +}); + // ============================================================ // Machine // ============================================================ @@ -122,6 +156,7 @@ export const authMachine = setup({ oauthSignIn: oauthSignInActor, syncGoogleBackend: syncGoogleBackendActor, syncFacebookBackend: syncFacebookBackendActor, + checkAuthStatus: checkAuthStatusActor, }, }).createMachine({ id: "auth", @@ -154,6 +189,8 @@ export const authMachine = setup({ AuthReset: { actions: assign(() => initialState), }, + // 启动 / 恢复时检查登录态 —— 由 派发 + AuthStatusCheckSubmitted: "checkingAuthStatus", AuthEmailLoginSubmitted: "loadingEmailLogin", AuthEmailRegisterSubmitted: "loadingEmailRegister", AuthGoogleLoginSubmitted: "loadingOAuth", @@ -169,6 +206,27 @@ export const authMachine = setup({ }, }, + checkingAuthStatus: { + entry: assign({ errorMessage: null }), + invoke: { + src: "checkAuthStatus", + onDone: { + target: "success", + actions: assign({ + loginStatus: ({ event }) => event.output, + errorMessage: null, + }), + }, + onError: { + target: "idle", + actions: assign({ + errorMessage: ({ event }) => + event.error instanceof Error ? event.error.message : String(event.error), + }), + }, + }, + }, + loadingEmailLogin: { invoke: { src: "emailLogin", diff --git a/src/stores/auth/auth-status-checker.tsx b/src/stores/auth/auth-status-checker.tsx new file mode 100644 index 00000000..1a250b75 --- /dev/null +++ b/src/stores/auth/auth-status-checker.tsx @@ -0,0 +1,25 @@ +"use client"; +/** + * AuthStatusChecker 启动检查器 + * + * App 启动时派发一次 `AuthStatusCheckSubmitted`, + * 让 auth machine 走"拿 deviceId → 查 token → 必要时游客登录"流程。 + * + * 与 平级: + * - AuthStatusChecker: 一次性 init + * - OAuthSessionSync: 持续监听 NextAuth session + */ +import { useEffect } from "react"; + +import { useAuthDispatch } from "./auth-context"; + +export function AuthStatusChecker() { + const dispatch = useAuthDispatch(); + + useEffect(() => { + dispatch({ type: "AuthStatusCheckSubmitted" }); + }, [dispatch]); + + // 无 UI + return null; +}