chore: initial commit of sso-mock SSO login service
Next.js + Mantine mock SSO service providing account/password, phone verify-code, and Feishu QR login. Issues a one-time sso_code that the external app exchanges for user info. Includes: - Feishu callback fix: guard against duplicate/concurrent /api/login submissions of the same single-use authorization code (feishuLogin once-guard + memoized onLoginSuccess to stop effect re-fire). - ADAPTATION_GUIDE.md: how to switch from the in-memory code store to the real backend auth/index + inner_get_user_info endpoints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
174
ADAPTATION_GUIDE.md
Normal file
174
ADAPTATION_GUIDE.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# SSO-Mock → 真实后端 适配指南
|
||||
|
||||
> 目标:当后端提供真实 SSO 接口后,把 sso-mock 从「自己伪造一次性 code」切换为「纯透传真实后端」。
|
||||
> 参考实现:生产前端 `ssofront`(`src/apis/login/index.ts`)已经在用这套契约,本指南以它为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论:需要几个接口?
|
||||
|
||||
核心是 **2 个**(你已想到),完全对齐生产还需 **1 个辅助 + 2 个已有代理**:
|
||||
|
||||
| # | 用途 | 后端接口(生产实测) | 必须 | 备注 |
|
||||
|---|------|----------------------|------|------|
|
||||
| ① | 登录 → 返回一次性 code | `POST /api/v1/auth/index` | ✅ | **单端点**,用 `login_type` 区分账号/手机/飞书 |
|
||||
| ② | 一次性 code → 用户信息 | `GET /api/v1/auth/inner_get_user_info?code=` | ✅ | 供 external-app 调用 |
|
||||
| ③ | app_id → 应用登录方式 | `GET /api/v1/application/by_client_id?client_id=` | ⭕ | 决定开放哪些登录 tab + 应用名;前端写死可省 |
|
||||
| ④ | 发送手机验证码 | `POST /api/v1/verify_code/send` | ⭕ | 手机登录才需要(sso-mock 现已代理为 basis 版) |
|
||||
| ⑤ | 租户简称列表 | `GET /api/v1/basis/tenant/base/listabbr` | ⭕ | 多租户选择(现已代理) |
|
||||
|
||||
> **一句话**:后端把 ①② 做出来,sso-mock 就能删掉内存 `codeStore` 与假 `/api/auth/verify`,变成纯代理。③ 是"完全对齐生产"的锦上添花。
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口契约(字段级)
|
||||
|
||||
### ① 登录换 code — `POST /api/v1/auth/index`
|
||||
|
||||
请求体(三种方式共用,按 `login_type` 带不同字段):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"login_type": "password | phone | feishu", // 判别登录方式
|
||||
"client_id": "<app_id>", // 外部应用标识
|
||||
"client_redirect_uri": "<app_url>", // 外部应用回调地址
|
||||
|
||||
// login_type=password
|
||||
"phone": "+86138...", "password": "***",
|
||||
// login_type=phone
|
||||
"phone": "+86138...", "verify_code": "123456",
|
||||
// login_type=feishu
|
||||
"code": "<飞书授权码>", "redirect_uri": "<sso-mock/login 回调>"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{ "code": "一次性 sso_code(短时效、单次使用)" }
|
||||
```
|
||||
|
||||
### ② code 换用户信息 — `GET /api/v1/auth/inner_get_user_info?code=<sso_code>`
|
||||
|
||||
响应(`Login.UserInfo`,节选):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"token": "...", "refresh_token": "...",
|
||||
"user": {
|
||||
"id": 1, "name": "张三", "phone": "...",
|
||||
"permissions": { "/dashboard": ["view"] },
|
||||
"feishu": { "avatar_url": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ③ 应用信息 — `GET /api/v1/application/by_client_id?client_id=<app_id>`
|
||||
|
||||
```json
|
||||
{ "data": { "providers": ["feishu", "password", "wechat"], "display_name": "XX 系统" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 标准流程(切到真实后端后)
|
||||
|
||||
```
|
||||
external-app
|
||||
→ 跳转 sso-mock: /login?app_id=<id>&app_url=<encoded url>
|
||||
↓ 用户在 sso-mock 登录(账号/手机/飞书)
|
||||
→ sso-mock ① POST /api/v1/auth/index ⇒ { code }
|
||||
→ sso-mock 重定向: <app_url>?sso_code=<code>
|
||||
↓
|
||||
→ external-app ② GET /api/v1/auth/inner_get_user_info?code=<code> ⇒ { user, token }
|
||||
→ external-app 建立本地会话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. sso-mock 需要改哪些文件
|
||||
|
||||
### 4.1 `app/api/login/route.ts` —— 从"伪造 code"改为"透传 auth/index"
|
||||
|
||||
**现状**:调 basis 原始登录端点拿到 `user+access_token`,再 `codeStore.set()` 自造 code。
|
||||
**改为**:直接调 `/api/v1/auth/index`,把返回的 `code` 原样透传,**删除 codeStore 相关代码**。
|
||||
|
||||
```ts
|
||||
// 三种方式统一映射到 auth/index
|
||||
const payloadMap = {
|
||||
account: { login_type: 'password', phone: body.phone, password: body.password },
|
||||
phone: { login_type: 'phone', phone: body.phone, verify_code: body.verify_code },
|
||||
feishu: { login_type: 'feishu', code: body.code, redirect_uri: body.redirect_uri },
|
||||
}
|
||||
const res = await callBasisApi('/api/v1/auth/index', {
|
||||
...payloadMap[type],
|
||||
client_id: body.client_id, // = app_id
|
||||
client_redirect_uri: body.client_redirect_uri, // = app_url
|
||||
})
|
||||
// 直接透传 code,不再 codeStore.set / generateCode
|
||||
return NextResponse.json({ code: res.code })
|
||||
```
|
||||
|
||||
> 注意:`client_id` / `client_redirect_uri` 现在是**必填**(一次性 code 必须绑定应用)。前端 `postLogin` / `qr-login.tsx` 已经在带这两个字段。
|
||||
|
||||
### 4.2 `app/api/auth/verify/route.ts` —— 从"读内存"改为"代理 inner_get_user_info"
|
||||
|
||||
**现状**:从 `codeStore` 取 user、自造 mock token。
|
||||
**改为**:
|
||||
|
||||
```ts
|
||||
export async function POST(req: Request) {
|
||||
const { code } = await req.json()
|
||||
if (!code) return NextResponse.json({ error: 'code 不能为空' }, { status: 400 })
|
||||
const res = await fetch(
|
||||
`${SHARED_API}/api/v1/auth/inner_get_user_info?code=${encodeURIComponent(code)}`
|
||||
)
|
||||
return NextResponse.json(await res.json(), { status: res.status })
|
||||
}
|
||||
```
|
||||
|
||||
> 或者让 external-app 直接调后端 ②,sso-mock 连这个代理都不用留。
|
||||
|
||||
### 4.3 `utils/code-store.ts` —— 删除
|
||||
|
||||
真实后端负责 code 的签发/校验/一次性/过期,内存 store 不再需要。同时移除 `route.ts` 里所有 `import { codeStore }`。
|
||||
|
||||
### 4.4 `app/api/getVerifyCode/route.ts` —— 换端点
|
||||
|
||||
`/api/v1/basis/user/phone/code` → `/api/v1/verify_code/send`(若后端统一到新契约)。
|
||||
|
||||
### 4.5(可选)应用信息驱动登录 tab
|
||||
|
||||
新增 `app/api/front/getAppInfo/route.ts` 代理 `③ by_client_id`,前端据 `providers` 动态渲染 tab(对齐 `ssofront/CloudLogin.tsx` 的 `asyncGetAppInfo`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 环境变量
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
NEXT_PUBLIC_SHARED_API_PATH=http://<真实后端>:<端口> # auth/index、inner_get_user_info 的宿主
|
||||
NEXT_PUBLIC_BASE_PATH= # 部署子路径(如有)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 迁移检查清单
|
||||
|
||||
- [ ] 后端就绪:`POST /api/v1/auth/index` 返回 `{ code }`
|
||||
- [ ] 后端就绪:`GET /api/v1/auth/inner_get_user_info` 返回用户信息
|
||||
- [ ] `login/route.ts` 改为调 auth/index 并透传 code,删 codeStore 逻辑
|
||||
- [ ] `auth/verify/route.ts` 改为代理 inner_get_user_info(或前端直连后端)
|
||||
- [ ] 删除 `utils/code-store.ts` 及其 import
|
||||
- [ ] `client_id` / `client_redirect_uri` 缺失时前端给出明确报错
|
||||
- [ ] (可选)接入 `by_client_id` 驱动登录 tab
|
||||
- [ ] 端到端验证:飞书回调只发 1 次 auth/index(已修复重复提交),换 code 成功,external-app 用 code 换到用户信息
|
||||
|
||||
---
|
||||
|
||||
## 7. 前端侧现状(无需改动)
|
||||
|
||||
登录组件已满足新契约的调用要求:
|
||||
|
||||
- `components/login/index.tsx` `postLogin()`:账号/手机登录已携带 `client_id` / `client_redirect_uri`。
|
||||
- `components/login/components/login/qr-login.tsx`:飞书回调已带 `code / redirect_uri / client_id / client_redirect_uri`,并已加**一次性提交守卫**(`feishuLoginTriggered` ref)防止同一飞书 code 被并发重复提交。
|
||||
Reference in New Issue
Block a user