395 lines
13 KiB
TypeScript
395 lines
13 KiB
TypeScript
/**
|
||
* Auth 状态机(XState v5)
|
||
*
|
||
* 原始 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 { setup, fromPromise, assign } from "xstate";
|
||
import { signIn } from "next-auth/react";
|
||
|
||
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";
|
||
import type { AuthEvent } from "./auth-events";
|
||
|
||
// 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变
|
||
export type { AuthState } from "./auth-state";
|
||
export { initialState } from "./auth-state";
|
||
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 emailLoginActor = fromPromise<LoginStatus, { email: string; password: string }>(
|
||
async ({ input }) => {
|
||
const guestId = await readGuestId();
|
||
const result = await authRepository.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 authRepository.register({ ...input, guestId });
|
||
if (Result.isErr(registerResult)) throw registerResult.error;
|
||
|
||
// 注册后自动登录(对齐 Dart 行为)
|
||
const loginResult = await authRepository.emailLogin({
|
||
email: input.email,
|
||
password: input.password,
|
||
guestId,
|
||
});
|
||
if (Result.isErr(loginResult)) throw loginResult.error;
|
||
return "email" as LoginStatus;
|
||
});
|
||
|
||
// OAuth 登录 actor:调 next-auth/react 的 signIn(provider) 触发 OAuth 跳转。
|
||
// 成功路径下 NextAuth 会重定向离开本应用,actor 通常不会 resolve;
|
||
// onDone 仍保留以处理 SDK 同步返回(极少见)。
|
||
const oauthSignInActor = fromPromise<void, AuthProvider>(async ({ input }) => {
|
||
await signIn(input);
|
||
});
|
||
|
||
// ========== 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 authRepository.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 authRepository.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 都自动创建游客账号污染后端。
|
||
|
||
const guestLoginActor = fromPromise<LoginStatus, void>(async () => {
|
||
const deviceId = await deviceIdentifier.getDeviceId();
|
||
const result = await authRepository.guestLogin(deviceId);
|
||
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
|
||
//
|
||
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
||
// **不**再设 `success` state —— 之前曾用 "success" 当"刚成功"信号,
|
||
// 但 status check 流也会误入 success,触发 splash-button 误跳 /chat。
|
||
// 现在改用 `loginStatus !== "notLoggedIn"` 作为"已登录"信号(见 auth-context.tsx)。
|
||
// ============================================================
|
||
export const authMachine = setup({
|
||
types: {
|
||
context: {} as AuthState,
|
||
events: {} as AuthEvent,
|
||
},
|
||
actors: {
|
||
emailLogin: emailLoginActor,
|
||
emailRegisterThenLogin: emailRegisterThenLoginActor,
|
||
oauthSignIn: oauthSignInActor,
|
||
syncGoogleBackend: syncGoogleBackendActor,
|
||
syncFacebookBackend: syncFacebookBackendActor,
|
||
guestLogin: guestLoginActor,
|
||
checkAuthStatus: checkAuthStatusActor,
|
||
},
|
||
}).createMachine({
|
||
id: "auth",
|
||
initial: "idle",
|
||
context: initialState,
|
||
states: {
|
||
idle: {
|
||
on: {
|
||
AuthPanelModeChanged: {
|
||
actions: assign({
|
||
authPanelMode: ({ event }) => event.mode,
|
||
errorMessage: null,
|
||
}),
|
||
},
|
||
AuthModeChanged: {
|
||
actions: assign({
|
||
authMode: ({ event }) => event.mode,
|
||
errorMessage: null,
|
||
}),
|
||
},
|
||
AuthFormCleared: {
|
||
actions: assign({
|
||
email: "",
|
||
password: "",
|
||
username: "",
|
||
confirmPassword: "",
|
||
errorMessage: null,
|
||
}),
|
||
},
|
||
AuthReset: {
|
||
actions: assign(() => initialState),
|
||
},
|
||
// 启动 / 恢复时检查登录态 —— 由 <AuthStatusChecker /> 派发
|
||
AuthStatusCheckSubmitted: "checkingAuthStatus",
|
||
// 显式游客登录 —— 由 splash UI 派发(**不**自动)
|
||
AuthGuestLoginSubmitted: "loadingGuestLogin",
|
||
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
||
AuthEmailRegisterSubmitted: "loadingEmailRegister",
|
||
AuthGoogleLoginSubmitted: "loadingOAuth",
|
||
AuthFacebookLoginSubmitted: "loadingOAuth",
|
||
// OAuth 回调后 sync 入口 —— 由 <OAuthSessionSync /> 派发
|
||
AuthGoogleSyncSubmitted: "syncingGoogleBackend",
|
||
AuthFacebookSyncSubmitted: "syncingFacebookBackend",
|
||
AuthAppleLoginSubmitted: {
|
||
actions: assign({
|
||
errorMessage: "Apple login not implemented",
|
||
}),
|
||
},
|
||
},
|
||
},
|
||
|
||
checkingAuthStatus: {
|
||
entry: assign({ errorMessage: null }),
|
||
invoke: {
|
||
src: "checkAuthStatus",
|
||
onDone: {
|
||
target: "idle", // ← status check 完回 idle(**不**再假冒 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),
|
||
}),
|
||
},
|
||
},
|
||
},
|
||
|
||
loadingGuestLogin: {
|
||
entry: assign({ errorMessage: null }),
|
||
invoke: {
|
||
src: "guestLogin",
|
||
onDone: {
|
||
target: "idle", // ← 改自 "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",
|
||
input: ({ event }) => {
|
||
if (event.type !== "AuthEmailLoginSubmitted")
|
||
return { email: "", password: "" };
|
||
return { email: event.email, password: event.password };
|
||
},
|
||
onDone: {
|
||
target: "idle", // ← 改自 "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),
|
||
}),
|
||
},
|
||
},
|
||
},
|
||
|
||
loadingEmailRegister: {
|
||
invoke: {
|
||
src: "emailRegisterThenLogin",
|
||
input: ({ event }) => {
|
||
if (event.type !== "AuthEmailRegisterSubmitted")
|
||
return { email: "", password: "", username: "", confirmPassword: "" };
|
||
return {
|
||
email: event.email,
|
||
password: event.password,
|
||
username: event.username,
|
||
confirmPassword: event.confirmPassword,
|
||
};
|
||
},
|
||
onDone: {
|
||
target: "idle", // ← 改自 "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),
|
||
}),
|
||
},
|
||
},
|
||
},
|
||
|
||
loadingOAuth: {
|
||
entry: assign({ errorMessage: null }),
|
||
invoke: {
|
||
src: "oauthSignIn",
|
||
input: ({ event }) => {
|
||
if (event.type === "AuthGoogleLoginSubmitted") return event.provider;
|
||
if (event.type === "AuthFacebookLoginSubmitted") return event.provider;
|
||
return "google" as AuthProvider;
|
||
},
|
||
onDone: {
|
||
target: "idle",
|
||
},
|
||
onError: {
|
||
target: "idle",
|
||
actions: assign({
|
||
errorMessage: ({ event }) =>
|
||
event.error instanceof Error ? event.error.message : String(event.error),
|
||
}),
|
||
},
|
||
},
|
||
},
|
||
|
||
syncingGoogleBackend: {
|
||
entry: assign({ errorMessage: null }),
|
||
invoke: {
|
||
src: "syncGoogleBackend",
|
||
input: ({ event }) => {
|
||
if (event.type !== "AuthGoogleSyncSubmitted") return { idToken: "" };
|
||
return { idToken: event.idToken };
|
||
},
|
||
onDone: {
|
||
target: "idle", // ← 改自 "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),
|
||
}),
|
||
},
|
||
},
|
||
},
|
||
|
||
syncingFacebookBackend: {
|
||
entry: assign({ errorMessage: null }),
|
||
invoke: {
|
||
src: "syncFacebookBackend",
|
||
input: ({ event }) => {
|
||
if (event.type !== "AuthFacebookSyncSubmitted") return { accessToken: "" };
|
||
return { accessToken: event.accessToken };
|
||
},
|
||
onDone: {
|
||
target: "idle", // ← 改自 "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),
|
||
}),
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
export type AuthMachine = typeof authMachine;
|