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>
This commit is contained in:
@@ -229,3 +229,162 @@ pnpm add ofetch
|
||||
- `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
|
||||
- 跳过 Apple(Flutter 端本来就是 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`
|
||||
|
||||
Reference in New Issue
Block a user