Files
cozsweet-frontend-nextjs/implementation_plan.md
T
Claude b362945195 feat(auth): migrate Flutter IAuthPlatform to Next.js 16 SDK service layer
Adds the third-party social login SDK integration layer (Google + Facebook),
mirroring the Flutter `IAuthPlatform` interface from
`lib/data/services/auth/`. The platform produces identity tokens only; callers
use the existing `authRepository.googleLogin / facebookLogin /
facebookIdLogin` (already migrated) to complete the login.

Architecture (event-bridge pattern):
  - `IAuthPlatform` is plain TypeScript (no React dependency)
  - `WebAuthPlatform` dispatches DOM `CustomEvent`s; `AuthPlatformBridge`
    ("use client" component) hosts the React SDK hooks and resolves promises
  - `MockAuthPlatform` returns hardcoded tokens when
    `NEXT_PUBLIC_USE_MOCK_AUTH=true` (mirrors Dart `kDebugMode` branch)

SDK choices:
  - `@react-oauth/google@0.13.5` for Google
  - `@greatsumini/react-facebook-login@3.4.0` (maintained fork of
    archived `react-facebook`, React 19 compatible) for Facebook
  - For Google: uses raw GIS `google.accounts.id.initialize` + `prompt` API
    since `@react-oauth/google` does not expose an `id_token` flow variant
    (existing `GoogleLoginRequest.idToken` field expects JWT id_token)

Files added:
  - src/data/services/auth/{auth_platform,web_auth_platform,
    mock_auth_platform,auth_event_bridge,auth_bridge_component,
    initialize_google_auth,initialize_facebook_auth,global.d.ts,index}.ts(x)
  - src/data/schemas/auth/facebook_user_data.ts (Zod schema)
  - src/data/dto/auth/facebook_user_data.ts (DTO class)

Files modified:
  - package.json, pnpm-lock.yaml: added 2 deps
  - .env.example: 4 new NEXT_PUBLIC_* env vars
  - barrelsby.json: registered src/data/services[/auth] directories
  - implementation_plan.md: appended third-party auth section
  - src/data/dto/{auth,chat,metrics,user}/index.ts: regenerated barrels
  - src/data/schemas/{auth,chat,metrics,user}/index.ts: regenerated barrels

Files deleted:
  - src/data/storage/index.ts: pre-existing TS2308 conflict (sync + async
    `AuthStorage` both re-exported from auto-generated barrel; no consumer
    uses the barrel — all imports use direct paths)

Scope (per user direction):
  - Client SDK integration only; no React hook/context, no UI, no Apple
  - Platform produces tokens; caller drives `authRepository` / `authApi`
  - v1 single-popup semantics (concurrent popups overwrite)
  - Bridge component is code-ready but NOT mounted in
    `src/app/layout.tsx`; next-step UI migration wires the provider chain

Verified: `npx tsc --noEmit` (0 errors), `pnpm lint` (0 errors),
`pnpm build` (Next.js 16.2.7 + Turbopack compiled successfully).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-09 11:13:14 +08:00

391 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Plan: HTTP API 层迁移
## [Overview]
将 Flutter 项目 `/lib/core/net/``/lib/data/services/api/` 下的 HTTP 网络层迁移至 Next.js 16,使用 **ofetch** 作为 HTTP 客户端(轻量、同构、拦截器钩子丰富),通过 `.env.local` 配置环境变量,复用已迁移的 Zod 数据模型进行响应验证。
## [Types]
### ApiResult 统一响应包装
```typescript
// src/data/api/api_result.ts
export type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: ApiError };
export class ApiError extends Error {
readonly code: string;
readonly status?: number;
readonly details?: unknown;
constructor(code: string, message: string, status?: number, details?: unknown);
}
```
### 环境配置类型
```typescript
// src/data/api/config/api_config.ts
export type AppEnv = "development" | "test" | "production";
export interface ApiConfig {
baseUrl: string;
wsUrl: string;
connectTimeout: number; // ms
receiveTimeout: number; // ms
sendTimeout: number; // ms
}
```
### 拦截器钩子签名
```typescript
// ofetch 原生 hooks
onRequest?: (ctx: { request: Request; options: FetchOptions }) => void | Promise<void>;
onResponse?: (ctx: { request: Request; response: Response; options: FetchOptions }) => void | Promise<void>;
onResponseError?: (ctx: { request: Request; response: Response; options: FetchOptions; error: FetchError }) => void | Promise<void>;
onError?: (ctx: { request: Request; error: Error; options: FetchOptions }) => void | Promise<void>;
```
## [Files]
### 新增
| 路径 | 用途 |
|---|---|
| `src/data/api/api_path.ts` | 路径常量(40+ endpoints |
| `src/data/api/api_result.ts` | 统一响应包装 `ApiResult<T>` + `ApiError` |
| `src/data/api/config/api_config.ts` | 环境 → baseUrl/超时 配置 |
| `src/data/api/storage/auth_storage.ts` | localStorage 包装(token 持久化) |
| `src/data/api/storage/device_storage.ts` | 设备 ID 持久化 |
| `src/data/api/http_client.ts` | ofetch 实例 + 拦截器装配 |
| `src/data/api/interceptor/token_interceptor.ts` | 注入 `Authorization: Bearer` |
| `src/data/api/interceptor/auth_refresh_interceptor.ts` | 401 → 自动刷新 token |
| `src/data/api/interceptor/logging_interceptor.ts` | 请求/响应日志(开发模式) |
| `src/data/api/auth_api.ts` | 11 个 auth 接口 |
| `src/data/api/chat_api.ts` | 5 个 chat 接口(含 multipart |
| `src/data/api/user_api.ts` | 5 个 user 接口 |
| `src/data/api/metrics_api.ts` | 2 个 metrics 接口 |
| `src/data/api/index.ts` | barrel 导出 |
| `.env.local` | 本地环境变量(不提交) |
| `.env.example` | 环境变量模板(提交) |
### 修改
| 路径 | 修改 |
|---|---|
| `package.json` | 添加 `ofetch` 依赖 |
| `pnpm-lock.yaml` | `pnpm install` 自动更新 |
| `.gitignore` | 确保 `.env.local` 已被忽略(Next.js 默认) |
### 不迁移
-`websocket_handler.dart` / `websocket_manager.dart` — WebSocket 独立项
-`message_queue.dart` — 离线消息队列
-`service_manager.dart` 的 dio 特定部分(已被 ofetch 实例替代)
-`payment_api_client.dart` — 原始 Dart 也不存在客户端类
## [Functions]
### 新增
| 函数 | 签名 | 文件 | 用途 |
|---|---|---|---|
| `getAuthToken()` | `() => Promise<string \| null>` | `storage/auth_storage.ts` | 优先登录 token,回退游客 token |
| `setLoginToken(t)` | `(token: string) => Promise<void>` | 同上 | 持久化登录 token |
| `setGuestToken(t)` | `(token: string) => Promise<void>` | 同上 | 持久化游客 token |
| `clearAuthData()` | `() => Promise<void>` | 同上 | 清除所有 auth 数据 |
| `getDeviceId()` | `() => string` | `storage/device_storage.ts` | 读取/生成设备 ID |
| `getApiConfig()` | `() => ApiConfig` | `config/api_config.ts` | 当前环境配置 |
| `createHttpClient()` | `() => typeof ofetch` | `http_client.ts` | ofetch 实例工厂 |
### 删除/重写
| 函数 | 旧实现 | 新实现 |
|---|---|---|
| `BackendService` 单例 | Dio + 4 ApiClient 字段 | ofetch + 4 api 类模块级单例 |
| `getBaseUrl()` | ServiceType.getBaseUrl() | api_config.ts 静态方法 |
| `getServiceConfig()` | ServiceType 扩展 | api_config.ts 静态方法 |
## [Classes]
### 新增
| 类 | 文件 | 关键方法 |
|---|---|---|
| `ApiError` | `api_result.ts` | 构造、`toString()` |
| `AuthStorage` | `storage/auth_storage.ts` | `getLoginToken`/`setLoginToken`/`getRefreshToken`/`getGuestToken`/`setGuestToken`/`hasLoginToken`/`clearAuthData` |
| `DeviceStorage` | `storage/device_storage.ts` | `getDeviceId()` |
| `AuthApi` | `auth_api.ts` | `sendCode`/`emailLogin`/`register`/`appleLogin`/`googleLogin`/`facebookLogin`/`facebookIdLogin`/`logout`/`guestLogin`/`refreshToken`/`getCurrentUser` |
| `ChatApi` | `chat_api.ts` | `sendMessage`/`getHistory`/`speechToText(multipart)`/`syncMessages`/`uploadImage(multipart)` |
| `UserApi` | `user_api.ts` | `getUserStats`/`getCurrentUser`/`updateProfile`/`getCredits`/`getCreditsHistory` |
| `MetricsApi` | `metrics_api.ts` | `reportPwaEvent`/`reportUserInfo` |
### 修改
无(无现有 class 需要修改)
### 拦截器实现(函数式,非 class)
由于 ofetch 钩子是函数式 API,拦截器以函数形式实现:
```typescript
// interceptor/token_interceptor.ts
export const tokenInterceptor: FetchHook = async (ctx) => {
const path = new URL(ctx.request.url).pathname;
if (!needsToken(path)) return;
const token = await getAuthToken();
if (token) {
ctx.request.headers.set("Authorization", `Bearer ${token}`);
}
};
// interceptor/auth_refresh_interceptor.ts
export const authRefreshInterceptor: FetchHook = async (ctx) => {
if (ctx.response?.status !== 401) return;
// 401 处理 + token 刷新 + 请求重试
};
// http_client.ts
const client = ofetch.create({
baseURL: getApiConfig().baseUrl,
timeout: getApiConfig().receiveTimeout,
hooks: {
onRequest: [loggingInterceptor, tokenInterceptor],
onResponse: [loggingInterceptor],
onResponseError: [authRefreshInterceptor, loggingInterceptor],
},
});
```
## [Dependencies]
### 新增
| 包 | 版本 | 用途 |
|---|---|---|
| `ofetch` | `^1.5.0` | 同构 HTTP 客户端(浏览器 + Node.js |
### 安装
```bash
pnpm add ofetch
```
### 版本选择
选择 ofetch v1.5+,原因:
- 完整同构支持(unjs/h3 生态)
- 内置拦截器钩子
- 体积小(~5KB gzipped
- TypeScript 优先
## [Testing]
### 验证策略
每个批次完成后:
- `npx tsc --noEmit` — 0 错误
- `pnpm lint` — 通过
最终验证:
- 模块导入测试(`import { authApi, chatApi } from '@/data/api'`
- 拦截器单元测试(mock localStorage 与 fetch
- 端到端测试(启动 dev server,触发实际请求)
### 测试要点
1. **环境变量加载**`process.env.NEXT_PUBLIC_API_BASE_URL` 正确解析
2. **Token 注入**`getCurrentUser` 自动携带 Authorization 头
3. **401 刷新**:模拟 401 响应,验证 token 刷新队列与重试
4. **Multipart 上传**`uploadImage` 正确发送 FormData
5. **超时控制**30s/60s 配置生效
## [Implementation Order]
按依赖关系,**9 个步骤**
1. **安装 ofetch**`pnpm add ofetch`(修改 `package.json` + `pnpm-lock.yaml`
2. **创建 `.env.example` + `.env.local`**:环境变量模板
3. **批次 A:基础设施**5 个文件)
- `api_path.ts`(路径常量)
- `api_result.ts`(响应包装)
- `config/api_config.ts`(环境配置)
- `storage/auth_storage.ts`localStorage 包装)
- `storage/device_storage.ts`(设备 ID
4. **批次 B:拦截器 + http_client**4 个文件)
- `interceptor/token_interceptor.ts`
- `interceptor/auth_refresh_interceptor.ts`
- `interceptor/logging_interceptor.ts`
- `http_client.ts`ofetch 实例装配)
5. **批次 CAPI clients**4 个文件,独立)
- `auth_api.ts`11 个方法)
- `chat_api.ts`5 个方法)
- `user_api.ts`5 个方法)
- `metrics_api.ts`2 个方法)
6. **批次 Dbarrel 导出**1 个文件)
- `index.ts`
7. **最终验证**
- `npx tsc --noEmit`
- `pnpm lint`
- 端到端 smoke test
---
# 后续迁移:第三方登录平台(IAuthPlatform
## [Overview]
将 Flutter `IAuthPlatform``/Users/chase/Documents/cozsweet/lib/data/services/auth/`)迁移为 Next.js 16 项目的纯 TypeScript 服务层。SDK 选用 `@react-oauth/google`Google Identity Services React 包装器)+ `@greatsumini/react-facebook-login``react-facebook` 的活跃 fork,兼容 React 19)。
**Flutter 端的 `AuthRepository` 已经完整迁移**`src/data/repositories/auth_repository.ts`,含 `googleLogin`/`facebookLogin`/`facebookIdLogin`/`appleLogin`),本步只补齐"生产第三方 identity token"那一段——平台层产 token,调用方拿 token 去调 `authRepository`(或 `authApi`)完成登录。
**范围限定**
- 客户端 SDK 接入层,**不**写 React Context/useAuth**不**写 UI
- 跳过 AppleFlutter 端本来就是 stub
- 平台**只产 token**,不调 `authRepository` / `authApi`
- 保留 Mock`NEXT_PUBLIC_USE_MOCK_AUTH=true` 切换,对齐 Dart `kDebugMode`
## [Architecture: event-bridge 模式]
`IAuthPlatform` 必须是 plain TypeScript(不依赖 React),但 React 包装器 SDK`useGoogleOAuth``FacebookLoginClient`)只能在 React 组件里用。解决方案是 **plain-TS 平台类 ↔ React Bridge 组件通过 DOM `CustomEvent` 通信**
```
WebAuthPlatform (plain TS) AuthPlatformBridge ("use client")
┌──────────────────────┐ ┌──────────────────────────┐
│ googleSignIn() │ │ │
│ ├─ new Promise │ dispatchEvent │ useGoogleOAuth() │
│ │ store requestId │ ────────────▶ │ window.google.accounts │
│ │ → resolver │ │ .id.initialize({...}) │
│ └─ return Promise │ │ on auth event: │
│ │ resolveGoogle │ window.google.accounts│
│ resolveGoogle( │ ◀──────────── │ .id.prompt() │
│ requestId, result)│ │ GIS callback: │
└──────────────────────┘ │ resolve the promise │
└──────────────────────────┘
```
- `WebAuthPlatform` 暴露公开方法 `resolveGoogle(requestId, result)` / `resolveFacebook(...)` 给 Bridge 回写 Promise
- `AuthPlatformBridge` 监听 `cozsweet:auth:google:start` / `cozsweet:auth:facebook:start` 事件,触发对应 SDK 流程
- **v1 限制**:单 popup 语义——一次只追踪一个待处理 Google / Facebook 请求;并发调用会被覆盖
## [Files]
### 新建(9 个文件)
| 路径 | 作用 |
|---|---|
| `src/data/services/auth/auth_platform.ts` | `IAuthPlatform` 接口 + `AuthPlatformFactory` + `getAuthPlatform()` 单例 + 命名类型 `GoogleCredential` / `FacebookLoginResult` |
| `src/data/services/auth/web_auth_platform.ts` | 真机平台类(plain TS),通过 DOM 事件与 Bridge 通信;含 `resolveGoogle()` / `resolveFacebook()` 公开方法 |
| `src/data/services/auth/mock_auth_platform.ts` | 调试用 Mock(返回固定 token + 250ms 模拟延迟) |
| `src/data/services/auth/auth_event_bridge.ts` | 跨 plain-TS / React 边界的 DOM 事件通道(无 React 依赖;导出事件名常量 + dispatch / subscribe 助手) |
| `src/data/services/auth/auth_bridge_component.tsx` | `"use client"` 组件,承载 `useGoogleOAuth` + `FacebookLoginClient`,监听事件并 resolve Promise |
| `src/data/services/auth/initialize_google_auth.ts` | Google GIS 预热辅助(占位) |
| `src/data/services/auth/initialize_facebook_auth.ts` | Facebook SDK 预热辅助(占位) |
| `src/data/schemas/auth/facebook_user_data.ts` | Zod schema + `Input` / `Data` 类型 |
| `src/data/dto/auth/facebook_user_data.ts` | `FacebookUserData` DTO 类(`from` / `fromJson` / `toJson` |
| `src/data/services/auth/global.d.ts` | `window.google.accounts.id.*` 的全局类型声明 |
### 修改
| 路径 | 变更 |
|---|---|
| `package.json` | 新增 `@react-oauth/google@0.13.5``@greatsumini/react-facebook-login@3.4.0` |
| `.env.example` | 新增 `NEXT_PUBLIC_GOOGLE_CLIENT_ID``NEXT_PUBLIC_FACEBOOK_APP_ID``NEXT_PUBLIC_FACEBOOK_API_VERSION=v25.0``NEXT_PUBLIC_USE_MOCK_AUTH=false` |
| `barrelsby.json` | 修整:删除已不存在的 `./src/data/models` 残留;新增 `./src/data/services` + `./src/data/services/auth` |
### 删除
| 路径 | 原因 |
|---|---|
| `src/data/storage/index.ts` | barrelsby 自动生成的 barrel,把同步 `AuthStorage``./auth_storage`)和异步 `AuthStorage``./auth/auth_storage`)同时 re-export,触发 TS2308 类型冲突。该 barrel 在仓库内**无消费者**(所有 import 都走直接路径 `@/data/storage/auth_storage``@/data/storage/auth/auth_storage`),可安全删除;后续若需要 barrel 须先解决命名冲突 |
## [Google Flow 决策(重要)]
`@react-oauth/google@0.13.5``useGoogleLogin(options)` 只支持两种 flow
- `flow: 'implicit'` → 回调 `TokenResponse`,含 `access_token`**不是** id_token
- `flow: 'auth-code'` → 回调 `CodeResponse`,含 `code`(授权码,需后端交换)
**但** 后端 `GoogleLoginRequest.idToken: z.string()` 期望的是 JWT id_token(与 Flutter `GoogleSignInAccount.authentication.idToken` 字段同名同义),所以 `useGoogleLogin` 两种 flow 都不适配。
**采用方案**:直接调用 GIS 全局 API(在 Bridge 的 `useEffect` 中),由 `GoogleOAuthProvider` 加载 GIS 脚本后:
- `window.google.accounts.id.initialize({ client_id, callback })` —— 注册 callback
- `window.google.accounts.id.prompt()` —— 触发弹窗
- callback 收到 `{ credential: '<JWT>' }``credential` 即 id_token
JWT payload 通过 `atob(parts[1])` 解析拿到 `email` / `name` / `picture`,填充 `GoogleCredential` 接口字段。
## [调用方契约(参考实现,**不在本步交付**)]
```ts
import { getAuthPlatform } from "@/data/services/auth";
import { authRepository } from "@/data/repositories";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
const platform = getAuthPlatform({
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
facebookAppId: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID!,
});
async function googleLogin() {
const r = await platform.googleSignIn();
if (r.kind === "failure") return handleError(r.error);
const guestId = (await AuthStorage.getInstance().getDeviceId()).value ?? "";
await authRepository.googleLogin({ idToken: r.value.idToken, guestId });
// token 持久化由 authRepository._saveLoginData 自动完成
}
async function facebookLogin() {
const r = await platform.facebookSignIn();
if (r.kind === "failure") return handleError(r.error);
await authRepository.facebookLogin({ accessToken: r.value.accessToken, guestId: "" });
// 拉头像 + by-id 登录(与 Flutter AuthBloc._uploadFacebookAvatar 一致)
const ud = await platform.getFacebookUserData();
if (ud.kind === "success" && ud.value.id && ud.value.pictureUrl) {
await AuthStorage.getInstance().setFacebookId(ud.value.id);
await authRepository.facebookIdLogin({ fbId: ud.value.id, avatarUrl: ud.value.pictureUrl });
}
}
```
## [下一步:UI 挂载方式(留给下一轮迁移)]
本步**不**挂载到 `src/app/layout.tsx`。下一轮 UI 接入时按如下方式挂载:
```tsx
// src/app/layout.tsx
import { GoogleOAuthProvider } from "@react-oauth/google";
import { getAuthPlatform, AuthPlatformBridge } from "@/data/services/auth";
const platform = getAuthPlatform({
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
facebookAppId: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID!,
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}>
<AuthPlatformBridge platform={platform}>
{children}
</AuthPlatformBridge>
</GoogleOAuthProvider>
</body>
</html>
);
}
```
## [Implementation Order]
1. **安装**`pnpm add @react-oauth/google @greatsumini/react-facebook-login`
2. **追加 env**`.env.example`
3. **修整 `barrelsby.json`**:删除 `./src/data/models` 残留、新增 `./src/data/services``./src/data/services/auth`
4. **删除** `src/data/storage/index.ts`(命名冲突)
5. **新建 Zod schema** + **DTO 类** for `FacebookUserData`
6. **生成 barrel**`pnpm generate-barrels`
7. **新建 global.d.ts**:声明 `window.google.accounts.id.*` 类型
8. **依次新建** `auth_event_bridge.ts``initialize_*.ts``auth_platform.ts``web_auth_platform.ts``mock_auth_platform.ts``auth_bridge_component.tsx`
9. **验证**`npx tsc --noEmit``pnpm lint``pnpm build`