refactor(auth): extract helpers and actors out of auth-machine
Move the file-local Helpers block (readGuestId) to auth-helpers.ts and the Actors block (authRepo + 7 fromPromise actors) to auth-actors.ts. Public API (authMachine, AuthEvent, AuthState, initialState, AuthMachine) is unchanged; auth-context.tsx and the barrel index.ts need no edits.
This commit is contained in:
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Auth 状态机:Actors(异步服务)
|
||||||
|
*
|
||||||
|
* 原始 Dart: lib/ui/auth/bloc/auth_bloc.dart + auth_state.dart + auth_event.dart
|
||||||
|
*
|
||||||
|
* 本轮迁移:所有认证登录调用(含 OAuth 社交登录)统一收口到状态机内。
|
||||||
|
* 业务层只派发事件(`AuthEmailLoginSubmitted` / `AuthGoogleLoginSubmitted` / 等),
|
||||||
|
* 状态机通过 actors 调 `authRepository`(邮箱 / OAuth token sync)或
|
||||||
|
* `next-auth/react.signIn`(OAuth 跳转)。
|
||||||
|
*/
|
||||||
|
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 { deviceIdentifier } from "@/utils/device_identifier";
|
||||||
|
import { Result } from "@/utils/result";
|
||||||
|
|
||||||
|
import { readGuestId } from "./auth-helpers";
|
||||||
|
|
||||||
|
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
|
||||||
|
const authRepo: IAuthRepository = authRepository;
|
||||||
|
|
||||||
|
export const emailLoginActor = fromPromise<LoginStatus, { email: string; password: string }>(
|
||||||
|
async ({ input }) => {
|
||||||
|
const guestId = await readGuestId();
|
||||||
|
const result = await authRepo.emailLogin({ ...input, guestId });
|
||||||
|
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 guestId = await readGuestId();
|
||||||
|
|
||||||
|
const registerResult = await authRepo.register({ ...input, guestId });
|
||||||
|
if (Result.isErr(registerResult)) throw registerResult.error;
|
||||||
|
|
||||||
|
// 注册后自动登录(对齐 Dart 行为)
|
||||||
|
const loginResult = await authRepo.emailLogin({
|
||||||
|
email: input.email,
|
||||||
|
password: input.password,
|
||||||
|
guestId,
|
||||||
|
});
|
||||||
|
if (Result.isErr(loginResult)) throw loginResult.error;
|
||||||
|
return "email" as LoginStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
// OAuth 登录 actor:调 AuthPlatform 触发 OAuth 跳转。
|
||||||
|
// 成功路径下 NextAuth 会重定向离开本应用,actor 通常不会 resolve;
|
||||||
|
// onDone 仍保留以处理 SDK 同步返回(极少见)。
|
||||||
|
export const oauthSignInActor = fromPromise<void, AuthProvider>(async ({ input }) => {
|
||||||
|
if (input === "google") {
|
||||||
|
await AuthPlatform.googleSignIn();
|
||||||
|
} else {
|
||||||
|
await AuthPlatform.facebookSignIn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== OAuth token → 后端业务 token sync actors ==========
|
||||||
|
// 由 <OAuthSessionSync /> 在 NextAuth 回调后派发(监听 useSession())。
|
||||||
|
// 用 OAuth provider 的 idToken / accessToken 调后端换业务 token + user。
|
||||||
|
// 后端返回的 LoginResponse 已经被 authRepository._saveLoginData 持久化。
|
||||||
|
|
||||||
|
export const syncGoogleBackendActor = fromPromise<LoginStatus, { idToken: string }>(
|
||||||
|
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;
|
||||||
|
return "facebook" as LoginStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 显式游客登录 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<LoginStatus, void>(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(只查不创) ==========
|
||||||
|
// 由 <AuthStatusChecker /> 在 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<LoginStatus, void>(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;
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Auth 状态机:Helpers
|
||||||
|
*
|
||||||
|
* 状态机内复用的小函数 —— 独立成文件便于测试 / 复用。
|
||||||
|
*/
|
||||||
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
|
|
||||||
|
/** 从本地存储读 guest deviceId;读不到 / 失败时返回 undefined(不抛)。 */
|
||||||
|
export async function readGuestId(): Promise<string | undefined> {
|
||||||
|
const r = await AuthStorage.getInstance().getDeviceId();
|
||||||
|
if (r.success && r.data != null) {
|
||||||
|
return r.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
+13
-168
@@ -7,185 +7,30 @@
|
|||||||
* 业务层只派发事件(`AuthEmailLoginSubmitted` / `AuthGoogleLoginSubmitted` / 等),
|
* 业务层只派发事件(`AuthEmailLoginSubmitted` / `AuthGoogleLoginSubmitted` / 等),
|
||||||
* 状态机通过 actors 调 `authRepository`(邮箱 / OAuth token sync)或
|
* 状态机通过 actors 调 `authRepository`(邮箱 / OAuth token sync)或
|
||||||
* `next-auth/react.signIn`(OAuth 跳转)。
|
* `next-auth/react.signIn`(OAuth 跳转)。
|
||||||
|
*
|
||||||
|
* 异步服务(actors)已抽到 `./auth-actors`,小工具抽到 `./auth-helpers`。
|
||||||
*/
|
*/
|
||||||
import { setup, fromPromise, assign } from "xstate";
|
import { setup, assign } from "xstate";
|
||||||
|
|
||||||
import { AuthPlatform, type AuthProvider } from "@/lib/auth/auth_platform";
|
import 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 { deviceIdentifier } from "@/utils/device_identifier";
|
|
||||||
import { Result } from "@/utils/result";
|
|
||||||
|
|
||||||
import { AuthState, initialState } from "./auth-state";
|
import { AuthState, initialState } from "./auth-state";
|
||||||
import type { AuthEvent } from "./auth-events";
|
import type { AuthEvent } from "./auth-events";
|
||||||
|
import {
|
||||||
|
emailLoginActor,
|
||||||
|
emailRegisterThenLoginActor,
|
||||||
|
oauthSignInActor,
|
||||||
|
syncGoogleBackendActor,
|
||||||
|
syncFacebookBackendActor,
|
||||||
|
guestLoginActor,
|
||||||
|
checkAuthStatusActor,
|
||||||
|
} from "./auth-actors";
|
||||||
|
|
||||||
// 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变
|
// 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变
|
||||||
export type { AuthState } from "./auth-state";
|
export type { AuthState } from "./auth-state";
|
||||||
export { initialState } from "./auth-state";
|
export { initialState } from "./auth-state";
|
||||||
export type { AuthEvent } from "./auth-events";
|
export type { AuthEvent } from "./auth-events";
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Helpers
|
|
||||||
// ============================================================
|
|
||||||
async function readGuestId(): Promise<string | undefined> {
|
|
||||||
const r = await AuthStorage.getInstance().getDeviceId();
|
|
||||||
if (r.success && r.data != null) {
|
|
||||||
return r.data;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Actors(异步服务)
|
|
||||||
// ============================================================
|
|
||||||
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
|
|
||||||
const authRepo: IAuthRepository = authRepository;
|
|
||||||
|
|
||||||
const emailLoginActor = fromPromise<LoginStatus, { email: string; password: string }>(
|
|
||||||
async ({ input }) => {
|
|
||||||
const guestId = await readGuestId();
|
|
||||||
const result = await authRepo.emailLogin({ ...input, guestId });
|
|
||||||
if (Result.isErr(result)) throw result.error;
|
|
||||||
return "email" as LoginStatus;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailRegisterThenLoginActor = fromPromise<
|
|
||||||
LoginStatus,
|
|
||||||
{ email: string; password: string; username: string; confirmPassword: string }
|
|
||||||
>(async ({ input }) => {
|
|
||||||
const guestId = await readGuestId();
|
|
||||||
|
|
||||||
const registerResult = await authRepo.register({ ...input, guestId });
|
|
||||||
if (Result.isErr(registerResult)) throw registerResult.error;
|
|
||||||
|
|
||||||
// 注册后自动登录(对齐 Dart 行为)
|
|
||||||
const loginResult = await authRepo.emailLogin({
|
|
||||||
email: input.email,
|
|
||||||
password: input.password,
|
|
||||||
guestId,
|
|
||||||
});
|
|
||||||
if (Result.isErr(loginResult)) throw loginResult.error;
|
|
||||||
return "email" as LoginStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
// OAuth 登录 actor:调 AuthPlatform 触发 OAuth 跳转。
|
|
||||||
// 成功路径下 NextAuth 会重定向离开本应用,actor 通常不会 resolve;
|
|
||||||
// onDone 仍保留以处理 SDK 同步返回(极少见)。
|
|
||||||
const oauthSignInActor = fromPromise<void, AuthProvider>(async ({ input }) => {
|
|
||||||
if (input === "google") {
|
|
||||||
await AuthPlatform.googleSignIn();
|
|
||||||
} else {
|
|
||||||
await AuthPlatform.facebookSignIn();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== OAuth token → 后端业务 token sync actors ==========
|
|
||||||
// 由 <OAuthSessionSync /> 在 NextAuth 回调后派发(监听 useSession())。
|
|
||||||
// 用 OAuth provider 的 idToken / accessToken 调后端换业务 token + user。
|
|
||||||
// 后端返回的 LoginResponse 已经被 authRepository._saveLoginData 持久化。
|
|
||||||
|
|
||||||
const syncGoogleBackendActor = fromPromise<LoginStatus, { idToken: string }>(
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
return "facebook" as LoginStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 显式游客登录 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,无需在此处主动校验。
|
|
||||||
|
|
||||||
const guestLoginActor = fromPromise<LoginStatus, void>(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(只查不创) ==========
|
|
||||||
// 由 <AuthStatusChecker /> 在 mount 时派发(一次性)。
|
|
||||||
// 流程(**纯只读**):
|
|
||||||
// 1. deviceIdentifier.getDeviceId() —— 无则通过 fingerprintjs 生成 + 落盘
|
|
||||||
// (给后续 OAuth/email 登录准备 deviceId,**不**在 splash 首次访问就 auto-create 游客)
|
|
||||||
// 2. AuthStorage.getLoginToken() —— 有 → "email"
|
|
||||||
// 3. AuthStorage.getGuestToken() —— 有 → "guest"
|
|
||||||
// 4. 都没有 → "notLoggedIn"(让用户**显式**选 OAuth / Email / Guest 入口)
|
|
||||||
|
|
||||||
const checkAuthStatusActor = fromPromise<LoginStatus, void>(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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Machine
|
// Machine
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user