/** * 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 } from "@/utils/device_identifier"; import { Logger } from "@/utils/logger"; import { Result } from "@/utils/result"; import { hasCompleteBusinessAuthSession } from "@/lib/auth/auth_session"; import { readGuestId, readPsid } 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(); const psid = await readPsid(); log.debug("[auth-machine] emailLoginActor calling authRepo.emailLogin", { hasGuestId: !!guestId, hasPsid: !!psid, }); const result = await authRepo.emailLogin({ ...input, guestId, psid }); 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; const psid = await readPsid(); // 注册后自动登录(对齐 Dart 行为) log.debug( "[auth-machine] emailRegisterThenLoginActor calling authRepo.emailLogin (post-register)", ); const loginResult = await authRepo.emailLogin({ email: input.email, password: input.password, guestId, psid, }); 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; return "guest" as LoginStatus; }); // ========== 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 psid = await readPsid(); const result = await authRepo.googleLogin({ idToken: input.idToken, guestId, psid, }); 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 psid = await readPsid(); const result = await authRepo.facebookLogin({ accessToken: input.accessToken, guestId, psid, }); 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.setAsid()`(独立于后端 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.setAsid(fbUser.id); if (!r.success) { log.warn( { err: r.error.message }, "[auth-machine] syncFacebookBackendActor: setAsid 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 ========== // AuthInit 检查不到任何本地登录态时触发,用于自动补齐游客会话。 export const guestLoginActor = fromPromise(async () => { log.debug("[auth-machine] guestLoginActor ENTRY"); // 1. 先查本地 guestToken —— 已有就直接进入游客态 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; }); export interface BindFacebookIdentityInput { asid?: string; psid?: string; } export const bindFacebookIdentityActor = fromPromise< void, BindFacebookIdentityInput >(async ({ input }) => { const result = await getAuthRepository().bindFacebookIdentity(input); if (Result.isErr(result)) { log.warn( { err: result.error.message }, "[auth-machine] bindFacebookIdentityActor failed", ); } }); // ========== 启动 / 恢复时检查登录态 actor(只查不创) ========== export const checkAuthStatusActor = fromPromise(async () => { // 1. 拿启动期可能参与身份同步的本地标识。 const deviceId = await deviceIdentifier.getDeviceId(); const storage = AuthStorage.getInstance(); const asidR = await storage.getAsid(); const psidR = await storage.getPsid(); const asid = asidR.success ? asidR.data : null; const psid = psidR.success ? psidR.data : null; const authRepo = getAuthRepository(); // 2. 查业务登录 token:loginToken 和 refreshToken 必须同时存在。 if (await hasCompleteBusinessAuthSession(storage)) { const providerR = await storage.getLoginProvider(); const provider = providerR.success && isBusinessLoginProvider(providerR.data) ? providerR.data : ("email" as LoginStatus); return provider; } // 3. 有 PSID 时优先让后端用本地绑定关系直接登录;未匹配则返回游客态。 if (psid) { const psidLoginResult = await authRepo.facebookPsidLogin({ psid, deviceId, bindToGuest: true, }); if (Result.isOk(psidLoginResult)) { return psidLoginResult.data; } log.warn( { err: psidLoginResult.error.message }, "[auth-machine] checkAuthStatusActor: facebookPsidLogin failed", ); } // 4. 无 PSID 或 PSID 直登失败时,继续用 ASID fallback 换业务 token。 if (asid) { const avatarUrlR = await UserStorage.getInstance().getAvatarUrl(); const result = await authRepo.facebookAsidLogin({ asid, avatarUrl: avatarUrlR.success ? avatarUrlR.data ?? undefined : undefined, psid: psid ?? undefined, }); if (Result.isErr(result)) { log.warn( { err: result.error.message }, "[auth-machine] checkAuthStatusActor: facebookAsidLogin failed", ); return "notLoggedIn" as LoginStatus; } return "facebook" as LoginStatus; } // 5. 查 guestToken const guestTokenR = await storage.getGuestToken(); if (guestTokenR.success && guestTokenR.data) { return "guest" as LoginStatus; } // 6. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口) return "notLoggedIn" as LoginStatus; }); function isBusinessLoginProvider( provider: LoginStatus | null, ): provider is LoginStatus { return ( provider === "email" || provider === "facebook" || provider === "google" ); }