/** * Auth 状态机:Actors(异步服务) */ import { fromPromise } from "xstate"; import { AuthPlatform, type AuthProvider } from "@/lib/auth/auth_platform"; import type { LoginStatus } from "@/data/dto/auth"; import { getAuthRepository } from "@/data/repositories/auth_repository"; import { fetchFacebookUserData } from "@/data/services"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; import { deviceIdentifier, Logger, Result } from "@/utils"; import { readGuestId } from "./auth-helpers"; const log = new Logger("AuthActors"); export const emailLoginActor = fromPromise( async ({ input }) => { const authRepo = getAuthRepository(); log.debug("[auth-machine] emailLoginActor ENTRY", { emailLength: input.email.length, emailPreview: input.email.slice(0, 8), passwordLength: input.password.length, }); const guestId = await readGuestId(); log.debug("[auth-machine] emailLoginActor calling authRepo.emailLogin", { hasGuestId: !!guestId, }); const result = await authRepo.emailLogin({ ...input, guestId }); log.debug("[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 }) => { const authRepo = getAuthRepository(); log.debug("[auth-machine] emailRegisterThenLoginActor ENTRY", { emailLength: input.email.length, usernameLength: input.username.length, passwordLength: input.password.length, confirmPasswordLength: input.confirmPassword.length, }); const guestId = await readGuestId(); log.debug("[auth-machine] emailRegisterThenLoginActor calling authRepo.register"); const registerResult = await authRepo.register({ ...input, guestId }); log.debug("[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 行为) log.debug( "[auth-machine] emailRegisterThenLoginActor calling authRepo.emailLogin (post-register)", ); const loginResult = await authRepo.emailLogin({ email: input.email, password: input.password, guestId, }); log.debug( "[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 authRepo = getAuthRepository(); 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 authRepo = getAuthRepository(); 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 authRepo = getAuthRepository(); 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` 触发,不自动跑。 export const guestLoginActor = fromPromise(async () => { log.debug("[auth-machine] guestLoginActor ENTRY"); // 1. 先查本地 guestToken —— 已有就直接进 idle(带 pendingRedirect) const hasTokenR = await AuthStorage.getInstance().hasGuestToken(); log.debug("[auth-machine] guestLoginActor hasGuestToken", { success: hasTokenR.success, hasData: hasTokenR.success ? !!hasTokenR.data : null, }); if (hasTokenR.success && hasTokenR.data) { log.debug("[auth-machine] guestLoginActor SHORT-CIRCUIT (has local guestToken) → return guest"); return "guest" as LoginStatus; } // Result.err(_) 时不抛 —— 落到 API 路径让真正的错误信号出现 // 2. 没有(或读不出)才走完整流程:拿 deviceId → 调后端 const deviceId = await deviceIdentifier.getDeviceId(); log.debug("[auth-machine] guestLoginActor deviceId", { length: deviceId.length, prefix: deviceId.slice(0, 8), }); log.debug("[auth-machine] guestLoginActor calling authRepo.guestLogin"); const result = await getAuthRepository().guestLogin(deviceId); log.debug("[auth-machine] guestLoginActor authRepo.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;非生产环境使用本地 UUID) // (给后续 OAuth/email 登录准备 deviceId,不在 splash 首次访问就 auto-create 游客) // 2. AuthStorage.getLoginToken() —— 有 → "email" // 3. AuthStorage.getFacebookId() —— 有 → facebookIdLogin → "facebook" // 4. AuthStorage.getGuestToken() —— 有 → "guest" // 5. 都没有 → "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. 外部浏览器深链同步:仅 fbid 落地时,用它换业务 token。 const facebookIdR = await storage.getFacebookId(); if (facebookIdR.success && facebookIdR.data) { const authRepo = getAuthRepository(); const avatarUrlR = await UserStorage.getInstance().getAvatarUrl(); const result = await authRepo.facebookIdLogin({ fbId: facebookIdR.data, avatarUrl: avatarUrlR.success ? avatarUrlR.data ?? undefined : undefined, }); if (Result.isErr(result)) { log.warn( { err: result.error.message }, "[auth-machine] checkAuthStatusActor: facebookIdLogin failed", ); return "notLoggedIn" as LoginStatus; } return "facebook" as LoginStatus; } // 4. 查 guestToken const guestTokenR = await storage.getGuestToken(); if (guestTokenR.success && guestTokenR.data) { return "guest" as LoginStatus; } // 5. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口) return "notLoggedIn" as LoginStatus; });