/** * Auth 状态机:Actors(异步服务) */ import { fromPromise } from "xstate"; import { AuthPlatform, type AuthProvider } from "@/lib/auth/auth_platform"; import type { LoginStatus } from "@/models/auth/login-status"; import { authRepository } from "@/data/repositories/auth_repository"; import type { IAuthRepository } from "@/data/repositories/interfaces"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; import { deviceIdentifier } from "@/utils/device_identifier"; import { fetchFacebookUserData } from "@/utils/facebook-graph"; import { Logger } from "@/utils/logger"; import { Result } from "@/utils/result"; import { readGuestId } from "./auth-helpers"; const log = new Logger("AuthActors"); // 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例 const authRepo: IAuthRepository = authRepository; export const emailLoginActor = fromPromise( async ({ input }) => { console.log("[auth-machine] emailLoginActor ENTRY", { emailLength: input.email.length, emailPreview: input.email.slice(0, 8), passwordLength: input.password.length, }); const guestId = await readGuestId(); console.log("[auth-machine] emailLoginActor calling authRepo.emailLogin", { hasGuestId: !!guestId, }); const result = await authRepo.emailLogin({ ...input, guestId }); console.log("[auth-machine] emailLoginActor authRepo.emailLogin DONE", { success: result.success, hasData: result.success ? !!result.data : null, errorName: result.success ? null : result.error?.name, errorMessage: result.success ? null : result.error?.message, }); if (Result.isErr(result)) throw result.error; return "email" as LoginStatus; }, ); export const emailRegisterThenLoginActor = fromPromise< LoginStatus, { email: string; password: string; username: string; confirmPassword: string } >(async ({ input }) => { console.log("[auth-machine] emailRegisterThenLoginActor ENTRY", { emailLength: input.email.length, usernameLength: input.username.length, passwordLength: input.password.length, confirmPasswordLength: input.confirmPassword.length, }); const guestId = await readGuestId(); console.log("[auth-machine] emailRegisterThenLoginActor calling authRepo.register"); const registerResult = await authRepo.register({ ...input, guestId }); console.log("[auth-machine] emailRegisterThenLoginActor authRepo.register DONE", { success: registerResult.success, errorName: registerResult.success ? null : registerResult.error?.name, errorMessage: registerResult.success ? null : registerResult.error?.message, }); if (Result.isErr(registerResult)) throw registerResult.error; // 注册后自动登录(对齐 Dart 行为) console.log( "[auth-machine] emailRegisterThenLoginActor calling authRepo.emailLogin (post-register)", ); const loginResult = await authRepo.emailLogin({ email: input.email, password: input.password, guestId, }); console.log( "[auth-machine] emailRegisterThenLoginActor authRepo.emailLogin DONE", { success: loginResult.success, errorName: loginResult.success ? null : loginResult.error?.name, errorMessage: loginResult.success ? null : loginResult.error?.message, }, ); if (Result.isErr(loginResult)) throw loginResult.error; return "email" as LoginStatus; }); // OAuth 登录 actor:调 AuthPlatform 触发 OAuth 跳转。 // 成功路径下 NextAuth 会重定向离开本应用,actor 通常不会 resolve; // onDone 仍保留以处理 SDK 同步返回(极少见)。 export const oauthSignInActor = fromPromise(async ({ input }) => { if (input === "google") { await AuthPlatform.googleSignIn(); } else { await AuthPlatform.facebookSignIn(); } }); export const logoutActor = fromPromise(async () => { const result = await authRepo.logout(); if (Result.isErr(result)) throw result.error; }); // ========== OAuth token → 后端业务 token sync actors ========== // 由 在 NextAuth 回调后派发(监听 useSession())。 // 用 OAuth provider 的 idToken / accessToken 调后端换业务 token + user。 // 后端返回的 LoginResponse 已经被 authRepository._saveLoginData 持久化。 export const syncGoogleBackendActor = fromPromise( async ({ input }) => { const guestId = await readGuestId(); const result = await authRepo.googleLogin({ idToken: input.idToken, guestId, }); if (Result.isErr(result)) throw result.error; return "google" as LoginStatus; }, ); export const syncFacebookBackendActor = fromPromise< LoginStatus, { accessToken: string } >(async ({ input }) => { const guestId = await readGuestId(); const result = await authRepo.facebookLogin({ accessToken: input.accessToken, guestId, }); if (Result.isErr(result)) throw result.error; // 业务 token 已到手 —— 顺带用同一个 accessToken 调 Facebook Graph `/me` // 拉用户资料(id / name / email / picture),把 pictureUrl + Facebook ID // 写本地。 // // 为什么要这一步?后端 LoginResponse.user.avatarUrl 不一定总有(依赖 // 后端在 OAuth 流程里有没有抓过);前端用 accessToken 直接调 Graph // 拿到的 picture 是最新的,独立于后端缓存。 // // 失败策略:best-effort。Graph 调用挂掉 / 用户没头像 / token 过期都不 // 影响 Facebook 登录主流程 —— 业务 token 已经在 result 里。 await persistFacebookProfile(input.accessToken); return "facebook" as LoginStatus; }); /** * 用 accessToken 调 Facebook Graph `/me` 拉用户资料,写本地: * - `id` → `AuthStorage.setFacebookId()`(独立于后端 userId) * - `pictureUrl` → `UserStorage.setAvatarUrl()`(头像专用 slot,便于快速读取) * * 全部 best-effort:单条失败只 warn,不抛错。`pictureUrl == null` 也算成功 * (用户没设头像),只是不写入。 */ async function persistFacebookProfile(accessToken: string): Promise { const fbR = await fetchFacebookUserData(accessToken); if (Result.isErr(fbR)) { log.warn( { err: fbR.error.message }, "[auth-machine] syncFacebookBackendActor: fetchFacebookUserData failed (continuing anyway)", ); return; } const fbUser = fbR.data; log.info( { hasId: !!fbUser.id, hasPicture: !!fbUser.pictureUrl, }, "[auth-machine] syncFacebookBackendActor: Facebook profile fetched", ); const authStorage = AuthStorage.getInstance(); if (fbUser.id) { const r = await authStorage.setFacebookId(fbUser.id); if (!r.success) { log.warn( { err: r.error.message }, "[auth-machine] syncFacebookBackendActor: setFacebookId failed", ); } } if (fbUser.pictureUrl) { const r = await UserStorage.getInstance().setAvatarUrl(fbUser.pictureUrl); if (!r.success) { log.warn( { err: r.error.message }, "[auth-machine] syncFacebookBackendActor: setAvatarUrl failed", ); } } } // ========== 显式游客登录 actor(opt-in) ========== // 由 splash UI 派发 `AuthGuestLoginSubmitted` 触发,不自动跑。 // 取代了之前 checkAuthStatusActor 的 auto-guest-login 行为 —— 避免每次 // 访 splash 都自动创建游客账号污染后端。 // // 短路优化:本地已有 guestToken 时直接 return "guest",跳过 deviceId // 探测 + 后端 round-trip(splash 每打开一次就可能点一次 Skip,命中率高)。 // 已知 trade-off:若后端已吊销该 token,actor 仍返回 "guest" —— 任何后续 // 受保护 API 401 会由 HTTP 层清掉 token 回到 splash,无需在此处主动校验。 export const guestLoginActor = fromPromise(async () => { console.log("[auth-machine] guestLoginActor ENTRY"); // 1. 先查本地 guestToken —— 已有就直接进 idle(带 pendingRedirect) const hasTokenR = await AuthStorage.getInstance().hasGuestToken(); console.log("[auth-machine] guestLoginActor hasGuestToken", { success: hasTokenR.success, hasData: hasTokenR.success ? !!hasTokenR.data : null, }); if (hasTokenR.success && hasTokenR.data) { console.log("[auth-machine] guestLoginActor SHORT-CIRCUIT (has local guestToken) → return guest"); return "guest" as LoginStatus; } // Result.err(_) 时不抛 —— 落到 API 路径让真正的错误信号出现 // 2. 没有(或读不出)才走完整流程:拿 deviceId → 调后端 const deviceId = await deviceIdentifier.getDeviceId(); console.log("[auth-machine] guestLoginActor deviceId", { length: deviceId.length, prefix: deviceId.slice(0, 8), }); console.log("[auth-machine] guestLoginActor calling authRepository.guestLogin"); const result = await authRepository.guestLogin(deviceId); console.log("[auth-machine] guestLoginActor authRepository.guestLogin DONE", { success: result.success, hasData: result.success ? !!result.data : null, errorName: result.success ? null : result.error?.name, errorMessage: result.success ? null : result.error?.message, }); if (Result.isErr(result)) throw result.error; return "guest" as LoginStatus; }); // ========== 启动 / 恢复时检查登录态 actor(只查不创) ========== // 由 在 mount 时派发(一次性)。 // 流程(纯只读): // 1. deviceIdentifier.getDeviceId() —— 无则通过 fingerprintjs 生成 + 落盘 // (给后续 OAuth/email 登录准备 deviceId,不在 splash 首次访问就 auto-create 游客) // 2. AuthStorage.getLoginToken() —— 有 → "email" // 3. AuthStorage.getGuestToken() —— 有 → "guest" // 4. 都没有 → "notLoggedIn"(让用户显式选 OAuth / Email / Guest 入口) export const checkAuthStatusActor = fromPromise(async () => { // 1. 拿 deviceId(不用于 auto-guest-login,仅供后续登录用) await deviceIdentifier.getDeviceId(); const storage = AuthStorage.getInstance(); // 2. 查 loginToken const loginTokenR = await storage.getLoginToken(); if (loginTokenR.success && loginTokenR.data) { return "email" as LoginStatus; } // 3. 查 guestToken const guestTokenR = await storage.getGuestToken(); if (guestTokenR.success && guestTokenR.data) { return "guest" as LoginStatus; } // 4. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口) return "notLoggedIn" as LoginStatus; });