认证端点 API
包含两类端点(频道权限按页面入口最低门槛取 public,便于未登录用户查看 OTP 接口; 具体子端点权限以下面标注为准):
- 公开:OTP 验证码代理转发(带频率限制和审计记录)
- 登录用户:注册兜底建团(issue #523)
基础路径
/auth认证方式
/auth/send-otp— 公开,无需 JWT 认证,由后端统一做频率限制后代理转发到 Supabase Auth/auth/initialize— 登录用户,需 Bearer JWT;单独挂jwtAuth中间件,不影响 send-otp 公开访问
限流说明
- 全局限流:继承全局 CORS 中间件
- Auth 专用限流:额外的频率限制(由
site_settings动态配置) - IP 维度限流:数据库级每小时限制
- 邮箱维度限流:原子化 advisory lock 限流(消除 TOCTOU 竞态)
端点列表
POST /auth/send-otp
发送 OTP 验证码邮件。支持首次发送和重发(通过 resend_token)。
请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
email | string | 是 | 目标邮箱地址 |
resend_token | string | 否 | 上次返回的一次性重发令牌,用于绕过冷却期重发 |
lang | string | 否 | 语言偏好(如 zh-CN / en-US),透传到 Supabase Auth user metadata,供 DB handle_new_user trigger 自动建首个团队时选择团队名(zh* → "我的团队",其余 → "My Team") |
成功响应 200
json
{
"success": true,
"message": "验证码已发送",
"resend_token": "uuid-token",
"token_expires_at": "2026-01-01T00:05:00Z",
"retry_after": 60,
"next_allowed_at": "2026-01-01T00:01:00Z"
}| 字段 | 类型 | 说明 |
|---|---|---|
resend_token | string | 新的一次性重发令牌(用于下次重发) |
token_expires_at | string | 令牌过期时间(ISO 8601) |
retry_after | number | 距下次可发送的秒数 |
next_allowed_at | string | 下次可发送的 ISO 时间 |
错误响应
| 状态码 | error 值 | 说明 |
|---|---|---|
| 400 | invalid_body | 请求体格式无效 |
| 400 | invalid_email | 邮箱格式无效 |
| 400 | token_email_mismatch | 重发令牌与邮箱不匹配 |
| 429 | rate_limited | 请求过于频繁,返回 retry_after 和 next_allowed_at |
| 500 | internal_error | 服务暂时不可用 |
| 500 | send_failed | 验证码发送失败 |
限流响应 429
json
{
"success": false,
"error": "rate_limited",
"message": "请求过于频繁,请稍后再试",
"retry_after": 60,
"next_allowed_at": "2026-01-01T00:01:00Z"
}POST /auth/initialize
权限:登录用户(需 Bearer JWT)
兜底接口:确保当前登录用户拥有一个 active team。通常情况下注册时 DB handle_new_user trigger 已自动建好首个团队,本接口仅在以下场景生效:
- DB trigger 创建失败(异常被 trigger 内部 WARN 吞掉,profile 已落地但 team 缺失)
- 老用户首次登录(trigger 上线前注册的账号)
- 团队被误删后用户访问 dashboard
调用 DB helper public.create_default_team_for_user(p_user_id, p_lang)。函数本身幂等:
- 用户已有 active team 直接返回
- 用户是某 team 的 owner → UPSERT 自己的
admin/acceptedmembership - 用户仅是某 team 的 active member(不是 owner) → 仅返回 team_id,不改 membership role(防止误提升)
请求体(可选)
json
{ "lang": "zh-CN" }| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
lang | string | 否 | 语言偏好,优先级最高 |
语言来源链(自上而下,首个非空生效):
body.langAccept-Languageheader 首段serverMode兜底(selfhosted →zh-CN,cloud →en-US)
成功响应 200
json
{
"success": true,
"data": {
"team_id": "uuid",
"slug": "team-a1b2c3d4e5",
"name": "我的团队"
}
}错误响应
| 状态码 | error | 说明 |
|---|---|---|
| 401 | unauthorized | 缺少或无效的 Authorization header |
| 500 | internal_error | RPC 调用失败或 team 查询失败 |
幂等性:连续调用对同一用户返回同一个 team_id;DB 层使用 pg_advisory_xact_lock 串行化同一用户的并发调用,避免双插。
安全机制
- IP 获取:优先使用
x-real-ip(反向代理设置),fallback 到x-forwarded-for首个 IP - 日志脱敏:邮箱和 IP 在日志中自动脱敏(SHA-256 hash)
- 多层限流:内存限流中间件 + IP 数据库限流 + 邮箱原子限流
- 发送失败回滚:Supabase OTP 发送失败时自动回滚占位记录,避免冷却期误拦截
- 请求体校验:所有端点拒绝/归一化非对象 body(防止 JSON
null触发 TypeError) - /auth/initialize 权限边界:DB helper SECURITY DEFINER + 仅
service_roleGRANT,不暴露给前端直调;前端必须通过本路由的 jwtAuth + 后端 service-role 通道