integrate sub2api as upstream for auth/keys/usage via FastAPI BFF

Preserve local user table for superDream-specific features while syncing
user lifecycle, API key CRUD and usage queries through sub2api. Admin token
handles reads and user lifecycle; per-user tokens (Fernet-encrypted in DB)
handle key writes that admin endpoints do not expose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xuyong
2026-04-17 21:23:08 +08:00
parent 20e842a60a
commit 35c0b7de16
30 changed files with 1707 additions and 803 deletions

339
docs/sub2api_api.md Normal file
View File

@@ -0,0 +1,339 @@
# sub2api 接口对接规范
本文档记录 superDreamFront 与 sub2api 对接所使用的全部 HTTP 接口,以及 BFF 层superDreamFront FastAPI各服务的调用策略。
> 对应的 sub2api 源码位置:
> - 路由:`sub2api/backend/internal/server/routes/{auth,user,admin}.go`
> - 用户侧 handler`sub2api/backend/internal/handler/{auth_handler,api_key_handler,usage_handler,user_handler}.go`
> - 管理员 handler`sub2api/backend/internal/handler/admin/{user_handler,apikey_handler}.go`
> - DTO 类型:`sub2api/backend/internal/handler/dto/types.go`
---
## 1. 通用约定
### 1.1 Base URL
所有接口以 `/api/v1` 为前缀,完整地址由环境变量 `SD_SUB2API_BASE_URL` 决定,例如:
```
SD_SUB2API_BASE_URL = http://127.0.0.1:8080
→ 实际请求http://127.0.0.1:8080/api/v1/...
```
### 1.2 响应 envelope
所有响应统一包一层(来源 `internal/pkg/response/response.go`
```json
{
"code": 0, // 0 表示成功;非 0 为 HTTP 状态码
"message": "success",
"reason": "", // 仅错误时出现
"metadata": null, // 错误附加信息
"data": { ... } // 业务载荷
}
```
分页响应的 `data` 形状固定为:
```json
{
"items": [...],
"total": 1234,
"page": 1,
"page_size": 20,
"pages": 62
}
```
BFF 层建议在 `sub2api.client.request()` 中统一解 envelope
- `code == 0` → 返回 `data`
- `code != 0` → 抛自定义异常 `Sub2APIError(code, message, reason, metadata)`
### 1.3 认证方式
sub2api 有三类鉴权:
| 鉴权类型 | 携带方式 | 用途 |
|---|---|---|
| 公开 | 无 | 注册、登录、忘记密码、验证码 |
| 用户 JWT | `Authorization: Bearer <access_token>` | `/keys``/usage``/user/*` |
| 管理员 | `x-api-key: <admin_api_key>``Authorization: Bearer <admin_jwt>` | `/admin/*` |
> 管理员 API Key 在 sub2api 后台 `Admin 设置 → Admin API Key → 生成` 后拿到,写入 `SD_SUB2API_ADMIN_TOKEN`。
### 1.4 Turnstile
部分公开接口会校验 Cloudflare Turnstile。sub2api 管理员可在设置里关闭;本 BFF 暂不处理 Turnstile若 sub2api 启用会 400。
---
## 2. 认证(公开接口)
### 2.1 发送邮箱验证码
`POST /api/v1/auth/send-verify-code`
请求:
```json
{ "email": "user@example.com", "turnstile_token": "" }
```
响应:
```json
{ "message": "Verification code sent successfully", "countdown": 60 }
```
### 2.2 注册
`POST /api/v1/auth/register`
请求:
```json
{
"email": "user@example.com",
"password": "******", // min 6
"verify_code": "123456", // 若 sub2api 开启邮箱验证码注册
"turnstile_token": "",
"promo_code": "", // 可选注册优惠码
"invitation_code": "" // 可选邀请码
}
```
响应(同 `AuthResponse`
```json
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 3600, // access 有效期(秒)
"token_type": "Bearer",
"user": { ...dto.User }
}
```
### 2.3 登录
`POST /api/v1/auth/login`
请求:
```json
{ "email": "user@example.com", "password": "******", "turnstile_token": "" }
```
若用户未开 2FA响应同注册。
若开启 2FA响应
```json
{ "requires_2fa": true, "temp_token": "...", "user_email_masked": "u**@e**.com" }
```
此时前端需再调 `POST /api/v1/auth/login/2fa`
```json
{ "temp_token": "...", "totp_code": "123456" }
```
### 2.4 刷新 Token
`POST /api/v1/auth/refresh`
```json
// 请求
{ "refresh_token": "..." }
// 响应
{ "access_token": "...", "refresh_token": "...", "expires_in": 3600, "token_type": "Bearer" }
```
注意sub2api 的 refresh_token **一次性使用、每次刷新会 rotate**BFF 取到新 refresh_token 要覆盖旧值。
### 2.5 登出
`POST /api/v1/auth/logout` body 可选 `{"refresh_token": "..."}`,服务端会撤销该 token。
### 2.6 忘记 / 重置密码
- `POST /api/v1/auth/forgot-password` `{"email", "turnstile_token"}`
- `POST /api/v1/auth/reset-password` `{"email", "token", "new_password"}`
### 2.7 当前用户
`GET /api/v1/auth/me` 需要用户 JWT响应 `dto.User + {run_mode}`
---
## 3. API Key用户 JWT
路由前缀 **`/api/v1/keys`**(注意不是 `/api-keys`handler 注释里旧路径是误导)。
### 3.1 列表
`GET /api/v1/keys?page=1&page_size=20&sort_by=created_at&sort_order=desc&search=&status=&group_id=`
响应 `data`(分页 envelope
```json
{
"items": [ { ...dto.APIKey } ],
"total": 5, "page": 1, "page_size": 20, "pages": 1
}
```
`dto.APIKey` 关键字段(完整字段见 `types.go`
```ts
{
id: number,
user_id: number,
key: string, // 完整 key注意sub2api 确实返回明文完整 key不做截断
name: string,
group_id: number | null,
status: "active" | "inactive",
ip_whitelist: string[], ip_blacklist: string[],
last_used_at: string | null,
quota: number, // USD0 = 无限
quota_used: number,
expires_at: string | null,
rate_limit_5h: number, rate_limit_1d: number, rate_limit_7d: number, // 0=不限
usage_5h: number, usage_1d: number, usage_7d: number,
reset_5h_at, reset_1d_at, reset_7d_at: string | null,
created_at, updated_at: string,
group?: dto.Group
}
```
### 3.2 单个查询
`GET /api/v1/keys/:id``dto.APIKey`
### 3.3 新建
`POST /api/v1/keys`
```json
{
"name": "my-key",
"group_id": 1, // 可选
"custom_key": null, // 可选自定义 key
"ip_whitelist": [], "ip_blacklist": [],
"quota": 0, // 0=无限
"expires_in_days": null, // null=不过期
"rate_limit_5h": 0, "rate_limit_1d": 0, "rate_limit_7d": 0
}
```
响应:`dto.APIKey`
### 3.4 更新
`PUT /api/v1/keys/:id`(字段同上,全部可选;特殊:`expires_at=""` 清除过期,`reset_quota=true` 重置已用,`reset_rate_limit_usage=true` 重置限速计数)
### 3.5 删除
`DELETE /api/v1/keys/:id``{ "message": "API key deleted successfully" }`
### 3.6 用户可用分组 / 专属倍率
- `GET /api/v1/groups/available``dto.Group[]`
- `GET /api/v1/groups/rates``{ [groupID: number]: number }`
---
## 4. 用量(用户 JWT
### 4.1 列表
`GET /api/v1/usage?page=&page_size=&api_key_id=&model=&request_type=&stream=&billing_type=&start_date=YYYY-MM-DD&end_date=YYYY-MM-DD&timezone=Asia/Shanghai`
响应 `items: dto.UsageLog[]`(分页)。
`dto.UsageLog` 关键字段:
```ts
{
id, user_id, api_key_id, account_id: number,
request_id, model: string,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost,
request_type: string, // "chat"/"responses"/"messages"/"image" 等
stream: boolean,
duration_ms, first_token_ms: number | null,
image_count: number,
created_at: string
}
```
### 4.2 详情
`GET /api/v1/usage/:id``dto.UsageLog`
### 4.3 统计
`GET /api/v1/usage/stats?period=today|week|month``start_date&end_date`
→ 返回 `service.UsageStats`(求和 + tokens/requests/cost 各纬度汇总;字段见 sub2api `UsageStats` 定义)。
### 4.4 Dashboard
- `GET /api/v1/usage/dashboard/stats`
- `GET /api/v1/usage/dashboard/trend?granularity=day&start_date&end_date`
- `GET /api/v1/usage/dashboard/models?start_date&end_date`
- `POST /api/v1/usage/dashboard/api-keys-usage` body `{ "api_key_ids": [1,2,3] }`
---
## 5. 管理员接口x-api-key
superDream BFF 仅用到以下 admin 接口:
### 5.1 用户
- `POST /api/v1/admin/users` — 注册同步
```json
{ "email", "password", "username", "notes": "from superDream",
"balance": 0, "concurrency": 0, "allowed_groups": [] }
```
→ `dto.AdminUser`
- `PUT /api/v1/admin/users/:id` — 改密/改状态同步(字段均为可选 PATCH 语义)
- `DELETE /api/v1/admin/users/:id` — 删除同步
- `GET /api/v1/admin/users?search=email@x.com` — 查找用户(拿 `id` 用于后续调用)
- `GET /api/v1/admin/users/:id` — 详情
### 5.2 查用户 API Keys / 用量
- `GET /api/v1/admin/users/:id/api-keys?page=&page_size=&sort_by=&sort_order=` → `dto.APIKey[]`(分页)
- `GET /api/v1/admin/users/:id/usage?period=today|week|month` → `UsageStats`
### 5.3 能力缺口
sub2api **admin 路径不提供** 代用户创建/删除/修改 Key 的接口(`registerAdminAPIKeyRoutes` 仅 `PUT /admin/api-keys/:id` 改分组)。
→ Key 的写操作必须使用用户 JWT。
---
## 6. BFF 对接策略
### 6.1 本地 `User` 表扩展字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `sub2api_user_id` | `BIGINT` | 对应 sub2api 的 `dto.User.id` |
| `sub2api_refresh_token_enc` | `TEXT` | Fernet 加密后的 refresh_token |
| `sub2api_access_token` | `TEXT` | 当前有效的 access_token内存级缓存即可但写库便于重启复用 |
| `sub2api_access_expires_at` | `DATETIME` | access_token 过期时间UTC |
### 6.2 鉴权 → Token 获取顺序
每次 BFF 需要代理到 sub2api **用户接口** 时:
1. 如本地缓存 access_token 未过期(预留 60s 余量),直接用
2. 否则用 Fernet 解密 refresh_token调 `/auth/refresh` 换新rotate 后写回)
3. 若 refresh 也 401token 被撤销)→ 返回前端 `401 sub2api_reauth_required`,前端弹窗让用户重新输密码,后端重调 `/auth/login` 重新拿到 token
### 6.3 各业务同步矩阵
| 动作 | 本地 | sub2apiadmin token | sub2api用户 token |
|---|---|---|---|
| 注册 | ①写 userpending | ②`POST /admin/users` 取回 `id` | ③`POST /auth/login` 拿首对 tokens<br>④成功后 commit 本地;任一失败整体回滚 |
| 登录 | ①校验本地密码 → 签本地 JWT | — | ②同步调 `/auth/login` 刷新存储 refresh_token |
| 改密 | ①更新本地 hash | ②`PUT /admin/users/:id` | ③用新密码调 `/auth/login` 替换 token |
| 删用户 | ①级联删 | ②`DELETE /admin/users/:id` | — |
| `GET /keys` | — | `GET /admin/users/:id/api-keys` | — |
| `POST/PUT/DELETE /keys` | — | — | `/keys` 系列 |
| `GET /usage/*` | — | `GET /admin/users/:id/usage` | 兜底:若 admin 接口粒度不够,回退到用户 token 调 `/usage` |
| Dashboard 统计 | 本地聚合 sub2api 返回 | `/admin/users/:id/usage` | `/usage/dashboard/*` |
### 6.4 前端字段替换
替换 `frontend/src/services/` 下的 TS interface使其与 `dto.APIKey` / `dto.UsageLog` / `dto.User` 完全一致(字段名用 snake_case 保留,不转 camelCase避免映射层。具体列在实现阶段列出 diff。
### 6.5 错误映射
| sub2api 返回 | BFF 抛出 | 前端显示 |
|---|---|---|
| `code=400` | `HTTPException 400` + `reason` | 表单校验提示 |
| `code=401 INVALID_TOKEN/TOKEN_EXPIRED` | 触发 refresh 流;失败抛 `401 sub2api_reauth_required` | 弹框"请重新登录" |
| `code=403` | `403` | "没有权限" |
| `code=404` | `404` | "资源不存在" |
| `code>=500` | `502 upstream_error` + 日志 | "上游服务异常" |
| 网络异常 | `504 upstream_timeout` | "请求超时" |
---
## 7. 新增配置项
写入 `app/config/settings.py`
```python
sub2api_base_url: str = "http://127.0.0.1:8080"
sub2api_admin_token: str = "" # sub2api Admin API Key
sub2api_request_timeout: float = 10.0 # 秒
sub2api_turnstile_bypass: bool = True # 是否绕过 turnstile需 sub2api 端也关)
token_encryption_key: str = "" # Fernet key44 字节 urlsafe base64
```
生成 Fernet key
```bash
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```
`requirements.txt` 增加:`httpx>=0.27` `cryptography>=42`