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; +}