Merge branch 'dev' into test
This commit is contained in:
+2
-1
@@ -4,8 +4,9 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev:proxy": "HTTPS_PROXY=http://127.0.0.1:7891 next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start -p 9135",
|
||||
"build:start": "npm run build && npm run start",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
|
||||
+14
-14
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* Router 公共导出
|
||||
*
|
||||
* 集中管理:
|
||||
* - 路由常量(ROUTES、ROUTE_BUILDERS、AUTH_ONLY_ROUTES、PUBLIC_ROUTES、ALL_STATIC_ROUTES)
|
||||
* - Next.js 16 Proxy(CDN 端鉴权重定向)
|
||||
*
|
||||
* 业务层导入示例:
|
||||
* ```ts
|
||||
* import { ROUTES, AUTH_ONLY_ROUTES, type Route } from "@/router";
|
||||
* ```
|
||||
*/
|
||||
export * from "./routes";
|
||||
export { proxy, config as proxyConfig } from "./proxy";
|
||||
// /**
|
||||
// * Router 公共导出
|
||||
// *
|
||||
// * 集中管理:
|
||||
// * - 路由常量(ROUTES、ROUTE_BUILDERS、AUTH_ONLY_ROUTES、PUBLIC_ROUTES、ALL_STATIC_ROUTES)
|
||||
// * - Next.js 16 Proxy(CDN 端鉴权重定向)
|
||||
// *
|
||||
// * 业务层导入示例:
|
||||
// * ```ts
|
||||
// * import { ROUTES, AUTH_ONLY_ROUTES, type Route } from "@/router";
|
||||
// * ```
|
||||
// */
|
||||
// export * from "./routes";
|
||||
// export { proxy, config as proxyConfig } from "./proxy";
|
||||
|
||||
+88
-88
@@ -1,101 +1,101 @@
|
||||
/**
|
||||
* Next.js 16 Proxy(原 `middleware.ts`)
|
||||
*
|
||||
* 行为(鉴权路由统一在此处理,业务层组件不再做 auth-driven 跳转):
|
||||
* - 已登录(NextAuth `next-auth.session-token` cookie 存在)访问 `/splash` 或 `/auth`
|
||||
* → 308 重定向到 `/chat`
|
||||
* - 未登录(cookie 缺失)访问 `/chat` 或 `/sidebar`
|
||||
* → 308 重定向到 `/splash`(首界面)
|
||||
* - 其他情况透传(`NextResponse.next()`)
|
||||
*
|
||||
* 重要约束:
|
||||
* - **不**做自定义 cookie 持久化:统一用 NextAuth 内置 `next-auth.session-token` /
|
||||
* `__Secure-next-auth.session-token`(v5 默认 httpOnly + secure)作信号。
|
||||
* - 真正的鉴权门在 Client 侧 `useSession()`(next-auth/react),用于 UI 状态。
|
||||
* - 路由跳转(**唯一**鉴权跳转点)由本 proxy 集中处理。
|
||||
* - 副作用:邮箱登录用户(不走 NextAuth)不会被本 proxy 识别为已登录。
|
||||
* 邮箱登录在后续轮次若要恢复 proxy 行为,需引入 NextAuth EmailProvider 或独立
|
||||
* email-session 体系。
|
||||
*
|
||||
* Next 16 重要变更(参见 `node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/proxy.md`):
|
||||
* - 文件从 `middleware.ts` 重命名为 `proxy.ts`;`middleware` 仍可用但已 deprecated。
|
||||
* - 默认 Node.js runtime;**不**支持 `runtime` 配置(设置会抛错)。
|
||||
* - 函数导出名建议为 `proxy`(可默认导出)。
|
||||
*
|
||||
* 用 `src/` 时 proxy 必须放在 `src/` 内(参见 `src-folder.md`)。
|
||||
*/
|
||||
// /**
|
||||
// * Next.js 16 Proxy(原 `middleware.ts`)
|
||||
// *
|
||||
// * 行为(鉴权路由统一在此处理,业务层组件不再做 auth-driven 跳转):
|
||||
// * - 已登录(NextAuth `next-auth.session-token` cookie 存在)访问 `/splash` 或 `/auth`
|
||||
// * → 308 重定向到 `/chat`
|
||||
// * - 未登录(cookie 缺失)访问 `/chat` 或 `/sidebar`
|
||||
// * → 308 重定向到 `/splash`(首界面)
|
||||
// * - 其他情况透传(`NextResponse.next()`)
|
||||
// *
|
||||
// * 重要约束:
|
||||
// * - **不**做自定义 cookie 持久化:统一用 NextAuth 内置 `next-auth.session-token` /
|
||||
// * `__Secure-next-auth.session-token`(v5 默认 httpOnly + secure)作信号。
|
||||
// * - 真正的鉴权门在 Client 侧 `useSession()`(next-auth/react),用于 UI 状态。
|
||||
// * - 路由跳转(**唯一**鉴权跳转点)由本 proxy 集中处理。
|
||||
// * - 副作用:邮箱登录用户(不走 NextAuth)不会被本 proxy 识别为已登录。
|
||||
// * 邮箱登录在后续轮次若要恢复 proxy 行为,需引入 NextAuth EmailProvider 或独立
|
||||
// * email-session 体系。
|
||||
// *
|
||||
// * Next 16 重要变更(参见 `node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/proxy.md`):
|
||||
// * - 文件从 `middleware.ts` 重命名为 `proxy.ts`;`middleware` 仍可用但已 deprecated。
|
||||
// * - 默认 Node.js runtime;**不**支持 `runtime` 配置(设置会抛错)。
|
||||
// * - 函数导出名建议为 `proxy`(可默认导出)。
|
||||
// *
|
||||
// * 用 `src/` 时 proxy 必须放在 `src/` 内(参见 `src-folder.md`)。
|
||||
// */
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
// import { NextResponse } from "next/server";
|
||||
// import type { NextRequest } from "next/server";
|
||||
|
||||
import { Logger } from "@/utils/logger";
|
||||
// import { Logger } from "@/utils/logger";
|
||||
|
||||
import {
|
||||
AUTH_ONLY_ROUTES,
|
||||
PROTECTED_ROUTES,
|
||||
ROUTES,
|
||||
} from "@/router/routes";
|
||||
// import {
|
||||
// AUTH_ONLY_ROUTES,
|
||||
// PROTECTED_ROUTES,
|
||||
// ROUTES,
|
||||
// } from "@/router/routes";
|
||||
|
||||
/** NextAuth 内置 session cookie 名(v5 默认 httpOnly) */
|
||||
const SESSION_COOKIE_NAMES = [
|
||||
"next-auth.session-token",
|
||||
"__Secure-next-auth.session-token",
|
||||
];
|
||||
// /** NextAuth 内置 session cookie 名(v5 默认 httpOnly) */
|
||||
// const SESSION_COOKIE_NAMES = [
|
||||
// "next-auth.session-token",
|
||||
// "__Secure-next-auth.session-token",
|
||||
// ];
|
||||
|
||||
/** proxy 入口日志(Node.js runtime 跑,pino 兼容)—— 排查 / 监控路由用 */
|
||||
const log = new Logger("Proxy");
|
||||
// /** proxy 入口日志(Node.js runtime 跑,pino 兼容)—— 排查 / 监控路由用 */
|
||||
// const log = new Logger("Proxy");
|
||||
|
||||
function isAuthOnlyRoute(pathname: string): boolean {
|
||||
return (AUTH_ONLY_ROUTES as readonly string[]).includes(pathname);
|
||||
}
|
||||
// function isAuthOnlyRoute(pathname: string): boolean {
|
||||
// return (AUTH_ONLY_ROUTES as readonly string[]).includes(pathname);
|
||||
// }
|
||||
|
||||
function isProtectedRoute(pathname: string): boolean {
|
||||
return (PROTECTED_ROUTES as readonly string[]).includes(pathname);
|
||||
}
|
||||
// function isProtectedRoute(pathname: string): boolean {
|
||||
// return (PROTECTED_ROUTES as readonly string[]).includes(pathname);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Proxy 入口
|
||||
*/
|
||||
export function proxy(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
// /**
|
||||
// * Proxy 入口
|
||||
// */
|
||||
// export function proxy(request: NextRequest) {
|
||||
// const { pathname } = request.nextUrl;
|
||||
|
||||
// 检查 NextAuth session cookie 存在性
|
||||
const hasSession = SESSION_COOKIE_NAMES.some(
|
||||
(name) => Boolean(request.cookies.get(name)?.value),
|
||||
);
|
||||
// // 检查 NextAuth session cookie 存在性
|
||||
// const hasSession = SESSION_COOKIE_NAMES.some(
|
||||
// (name) => Boolean(request.cookies.get(name)?.value),
|
||||
// );
|
||||
|
||||
// 已登录访问未登录专属页 → /chat
|
||||
if (hasSession && isAuthOnlyRoute(pathname)) {
|
||||
log.info(
|
||||
{ pathname, target: ROUTES.chat, reason: "logged-in → auth-only" },
|
||||
"[proxy] redirect",
|
||||
);
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = ROUTES.chat;
|
||||
return NextResponse.redirect(url, 308);
|
||||
}
|
||||
// // 已登录访问未登录专属页 → /chat
|
||||
// if (hasSession && isAuthOnlyRoute(pathname)) {
|
||||
// log.info(
|
||||
// { pathname, target: ROUTES.chat, reason: "logged-in → auth-only" },
|
||||
// "[proxy] redirect",
|
||||
// );
|
||||
// const url = request.nextUrl.clone();
|
||||
// url.pathname = ROUTES.chat;
|
||||
// return NextResponse.redirect(url, 308);
|
||||
// }
|
||||
|
||||
// 未登录访问登录态专属页 → /splash(首界面)
|
||||
if (!hasSession && isProtectedRoute(pathname)) {
|
||||
log.info(
|
||||
{ pathname, target: ROUTES.splash, reason: "not-logged-in → protected" },
|
||||
"[proxy] redirect",
|
||||
);
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = ROUTES.splash;
|
||||
return NextResponse.redirect(url, 308);
|
||||
}
|
||||
// // 未登录访问登录态专属页 → /splash(首界面)
|
||||
// if (!hasSession && isProtectedRoute(pathname)) {
|
||||
// log.info(
|
||||
// { pathname, target: ROUTES.splash, reason: "not-logged-in → protected" },
|
||||
// "[proxy] redirect",
|
||||
// );
|
||||
// const url = request.nextUrl.clone();
|
||||
// url.pathname = ROUTES.splash;
|
||||
// return NextResponse.redirect(url, 308);
|
||||
// }
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
// return NextResponse.next();
|
||||
// }
|
||||
|
||||
/**
|
||||
* 匹配器:排除 API、Next 静态资源、图标与常见静态文件扩展名。
|
||||
* 不排除 `_next/data`,因为 proxy 文档明确说明"即使在排除列表中 proxy 仍会跑"
|
||||
* (安全兜底)。
|
||||
*/
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!api|_next/static|_next/image|_next/data|favicon.ico|icons|.*\\.(?:png|jpg|jpeg|gif|svg|webp|ico|css|js|woff2?|ttf|otf)$).*)",
|
||||
],
|
||||
};
|
||||
// /**
|
||||
// * 匹配器:排除 API、Next 静态资源、图标与常见静态文件扩展名。
|
||||
// * 不排除 `_next/data`,因为 proxy 文档明确说明"即使在排除列表中 proxy 仍会跑"
|
||||
// * (安全兜底)。
|
||||
// */
|
||||
// export const config = {
|
||||
// matcher: [
|
||||
// "/((?!api|_next/static|_next/image|_next/data|favicon.ico|icons|.*\\.(?:png|jpg|jpeg|gif|svg|webp|ico|css|js|woff2?|ttf|otf)$).*)",
|
||||
// ],
|
||||
// };
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
* - `as const` 让每个值都是字符串字面量类型,可用于 `PageProps<'/literal'>`。
|
||||
* - 不导出 Client-only 模块(`use-auth-gate` 走独立路径导入),保证 Server bundle 不被污染。
|
||||
* - 动态深链通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。
|
||||
*
|
||||
* 注意:proxy 路由跳转逻辑(2026-06-15 迁出),但本文件的**类型化常量**仍在
|
||||
* 被 9 个组件(auth-screen / chat-header / quota-dialog / DeepLinkPersist /
|
||||
* error / not-found / page / sidebar-screen / splash-screen)使用 —— **不能**整体注释。
|
||||
*/
|
||||
|
||||
/** 静态路由字面量 */
|
||||
|
||||
@@ -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` / 等),
|
||||
* 状态机通过 actors 调 `authRepository`(邮箱 / OAuth token sync)或
|
||||
* `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 { 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 type { AuthProvider } from "@/lib/auth/auth_platform";
|
||||
|
||||
import { AuthState, initialState } from "./auth-state";
|
||||
import type { AuthEvent } from "./auth-events";
|
||||
import {
|
||||
emailLoginActor,
|
||||
emailRegisterThenLoginActor,
|
||||
oauthSignInActor,
|
||||
syncGoogleBackendActor,
|
||||
syncFacebookBackendActor,
|
||||
guestLoginActor,
|
||||
checkAuthStatusActor,
|
||||
} from "./auth-actors";
|
||||
|
||||
// 重新导出 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 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
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user