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:
339
docs/sub2api_api.md
Normal file
339
docs/sub2api_api.md
Normal 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, // USD,0 = 无限
|
||||
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 也 401(token 被撤销)→ 返回前端 `401 sub2api_reauth_required`,前端弹窗让用户重新输密码,后端重调 `/auth/login` 重新拿到 token
|
||||
|
||||
### 6.3 各业务同步矩阵
|
||||
|
||||
| 动作 | 本地 | sub2api(admin token) | sub2api(用户 token) |
|
||||
|---|---|---|---|
|
||||
| 注册 | ①写 user(pending) | ②`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 key(44 字节 urlsafe base64)
|
||||
```
|
||||
|
||||
生成 Fernet key:
|
||||
```bash
|
||||
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
```
|
||||
|
||||
`requirements.txt` 增加:`httpx>=0.27` `cryptography>=42`
|
||||
Reference in New Issue
Block a user