团队管理 API
团队管理相关接口,包括邀请链接、API Key 管理和团队密钥管理。
路由前缀:/teams
源码:apps/backend/src/routes/teams.ts
认证
所有接口需要 JWT 认证:
Authorization: Bearer <JWT>端点
1. 生成团队邀请链接
POST /teams/generate-invite-link生成一个团队邀请 JWT Token,有效期 7 天。仅团队 owner 可操作。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
teamId | string | 是 | 团队 ID |
响应 200 OK:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2026-03-14T10:00:00.000Z"
}
}错误码:
| HTTP | 错误 | 说明 |
|---|---|---|
| 400 | teamId is required | 未提供 teamId |
| 401 | Unauthorized | 未认证 |
| 403 | Only team owner can generate invite links | 非团队 owner |
| 404 | Team not found | 团队不存在 |
| 500 | Internal server error | 服务端错误 |
2. 接受团队邀请
POST /teams/accept-invite使用邀请 Token 加入团队。如果用户已是成员(含 owner 自身),返回 alreadyMember: true(幂等)。
进入此端点会先调用 team-seats.service 计算席位状态:
- 已是 active 成员(含 owner)→ 直接返回,不受 isFull 影响
- 席位已满(
currentSeats >= max_seats,含 owner 自愈补偿)→ 409 拒绝 - 历史
(team_id, user_id)行(pending/rejected/软删)走复活分支:显式role='member'+ 清deleted_at,避免历史 admin 复活后越权
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
token | string | 是 | 邀请 JWT Token |
响应 200 OK:
{
"success": true,
"data": {
"team": {
"id": "uuid",
"slug": "my-team",
"name": "My Team"
},
"alreadyMember": false
}
}错误码:
| HTTP | 错误 | 说明 |
|---|---|---|
| 400 | token is required | 未提供 token |
| 400 | Invalid or expired invite token | Token 无效或过期 |
| 401 | Unauthorized | 未认证 |
| 404 | Team not found | 团队不存在 |
| 409 | team_seats_full | 席位已满;响应 data: { current, max },前端按 i18n 渲染 |
| 500 | team_plan_missing / team_query_failed / members_query_failed | 内部数据/查询异常(明确错误,不静默兜底) |
席位强 enforcement 限制:route 层为 best-effort(read-then-write 之间存在并发窗口),高并发下两个新成员同时接受可能瞬时超额。强语义需下沉 DB RPC + 行锁(暂未实现,已留 TODO)。
2.1 预览邀请详情
POST /teams/invite-preview只读端点,前端在 JoinTeam 视图调用,用于在用户点「加入」之前展示团队信息、邀请人、过期时间和当前席位用量。返回 server-verified 字段(避免依赖前端本地解析 JWT)。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
token | string | 是 | 邀请 JWT Token |
响应 200 OK:
{
"success": true,
"data": {
"team": { "id": "uuid", "slug": "my-team", "name": "My Team" },
"inviter": { "name": "Alice" },
"expiresAt": "2026-05-02T10:00:00.000Z",
"currentSeats": 3,
"maxSeats": 5,
"isFull": false,
"alreadyMember": false,
"canAccept": true,
"blockReason": null
}
}currentSeats已含 owner 自愈补偿(owner 不在team_members时 +1)canAccept = alreadyMember || !isFull:alreadyMember=true时即使 isFull 也允许进入(accept 会走幂等分支)blockReason:当前仅'team_seats_full' | null
错误码: 同 accept-invite。
3. 创建团队 API Key
POST /teams/:teamId/api-keys为团队创建 API Key。密钥明文仅在创建时返回一次,数据库只存储哈希值。仅团队 owner 可操作。
路径参数:
| 参数 | 说明 |
|---|---|
teamId | 团队 ID |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | Key 名称 |
description | string | 否 | 描述 |
limitCredits | number | null | 否 | 积分限额 |
响应 201 Created:
{
"success": true,
"data": {
"id": "uuid",
"name": "My API Key",
"prefix": "wn-",
"suffix": "abc...defg",
"apiKey": "wn-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-03-07T10:00:00.000Z"
},
"message": "请立即保存此 API Key,它不会再次显示!"
}注意:
apiKey字段包含完整明文密钥,仅在此次响应中返回,请立即保存。
错误码:
| HTTP | 错误 | 说明 |
|---|---|---|
| 400 | name is required | 未提供 name |
| 401 | Unauthorized | 未认证 |
| 403 | Only team owner can create API keys | 非团队 owner |
| 404 | Team not found | 团队不存在 |
| 500 | Internal server error | 服务端错误 |
4. 列出团队密钥
GET /teams/:teamId/secrets获取团队的所有密钥列表。团队成员(owner 和 member)均可查看。
路径参数:
| 参数 | 说明 |
|---|---|
teamId | 团队 ID |
响应 200 OK:
{
"success": true,
"data": [
{
"id": "uuid",
"key": "MY_SECRET_KEY",
"description": "第三方 API 密钥",
"created_at": "2026-03-07T10:00:00.000Z",
"updated_at": "2026-03-07T10:00:00.000Z"
}
]
}错误码:
| HTTP | 错误 | 说明 |
|---|---|---|
| 401 | Unauthorized | 未认证 |
| 403 | Permission denied | 非团队成员 |
| 500 | Internal server error | 服务端错误 |
5. 创建团队密钥
POST /teams/:teamId/secrets创建团队密钥。仅团队 owner 可操作。
路径参数:
| 参数 | 说明 |
|---|---|
teamId | 团队 ID |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | string | 是 | 密钥名(大写字母开头,仅大写字母/数字/下划线,最长 64 字符) |
value | string | 是 | 密钥值 |
description | string | 否 | 描述 |
key 格式要求:匹配 ^[A-Z][A-Z0-9_]*$,最大 64 字符。
响应 201 Created:
{
"success": true,
"data": {
"id": "uuid",
"key": "MY_SECRET_KEY",
"description": "描述",
"created_at": "2026-03-07T10:00:00.000Z"
}
}错误码:
| HTTP | 错误 | 说明 |
|---|---|---|
| 400 | key and value are required | 缺少必填字段 |
| 400 | key must match ... | key 格式不合法 |
| 401 | Unauthorized | 未认证 |
| 403 | Only team owner can create secrets | 非团队 owner |
| 409 | Key already exists in this team | key 重复 |
| 500 | Internal server error | 服务端错误 |
6. 更新团队密钥
PUT /teams/:teamId/secrets/:id更新团队密钥的值或描述。仅团队 owner 可操作。
路径参数:
| 参数 | 说明 |
|---|---|
teamId | 团队 ID |
id | 密钥 ID |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
value | string | 否 | 新密钥值 |
description | string | 否 | 新描述 |
value和description至少提供一个。
响应 200 OK:
{
"success": true,
"data": {
"id": "uuid",
"key": "MY_SECRET_KEY",
"description": "新描述",
"updated_at": "2026-03-07T12:00:00.000Z"
}
}错误码:
| HTTP | 错误 | 说明 |
|---|---|---|
| 400 | At least one of value or description is required | 未提供任何更新字段 |
| 401 | Unauthorized | 未认证 |
| 403 | Only team owner can update secrets | 非团队 owner |
| 500 | Internal server error | 服务端错误 |
7. 删除团队密钥
DELETE /teams/:teamId/secrets/:id删除团队密钥。仅团队 owner 可操作。
路径参数:
| 参数 | 说明 |
|---|---|
teamId | 团队 ID |
id | 密钥 ID |
响应 200 OK:
{
"success": true
}错误码:
| HTTP | 错误 | 说明 |
|---|---|---|
| 401 | Unauthorized | 未认证 |
| 403 | Only team owner can delete secrets | 非团队 owner |
| 500 | Internal server error | 服务端错误 |
团队成员管理
三档角色语义:
- owner:以
teams.owner_user_id为权威来源;owner 同时存在于team_members表(由teams_auto_add_owner_trgtrigger 保证) - admin:
team_members.role = 'admin',可帮 owner 管理 member;不能互相管理(由admin_cannot_manage_non_member分支保护) - member:
team_members.role = 'member',默认新成员
8. 列出团队成员
GET /teams/:teamId/members返回团队内所有 status='accepted' 且未软删的成员。调用方必须是团队成员。
响应 200 OK:
{
"success": true,
"data": [
{
"user_id": "uuid",
"role": "member",
"is_owner": false,
"display_name": "KongKang",
"avatar_url": null,
"created_at": "2026-04-21T10:00:00.000Z"
}
],
"can_manage": true
}is_owner以teams.owner_user_id为权威来源(非 role 推导)- 若 owner 因历史脏数据不在
team_members中,会补一行虚拟记录(后端 warn 日志),保证 UI 不丢 owner can_manage表示调用方是 owner 或 admin
9. 修改成员角色
PATCH /teams/:teamId/members/:userId/role切换成员 member ↔ admin。仅 owner 可调。
请求体: { "role": "member" | "admin" }
错误码(节选): not_owner (403), cannot_change_owner_role (400), cannot_change_self_role (400), target_not_member (404), invalid_role (400)
10. 查询成员的项目权限足迹
GET /teams/:teamId/members/:userId/project-permissions列出该成员在团队所有项目里的显式角色和直接权限。
权限:owner / admin 可查任意 userId;member 只能查自己(userId = auth.uid()),否则 403 insufficient_privilege。
响应 200 OK:
{
"success": true,
"data": {
"explicit_projects": [
{
"project_id": "uuid",
"project_name": "My Project",
"project_slug": "my-project",
"roles": [{ "id": "uuid", "code": "project_editor", "name": "项目编辑者" }],
"direct_permissions": [{ "code": "read" }, { "code": "write" }]
}
]
}
}隐式团队访问(团队成员身份 → 团队共享资源)由前端自行提示,不在响应体中返回。
11. 移除团队成员
DELETE /teams/:teamId/members/:userId从团队移除成员:单事务原子级联软删 user_roles / user_permissions / team_members。底层调用 remove_team_member RPC。
鉴权与保护(RPC 内部):
- caller 必须是 owner 或 admin
- admin 仅能移除 member(不能管理 admin)
- 不能移除 owner / 不能移除自己
- Ownership preflight:目标在本团队下不能拥有活跃
projects.owner_user_id或活跃documents.owner_user_id
成功响应 200 OK:
{ "success": true, "cleanup": { "roles": 3, "permissions": 2 } }member_owns_resources 错误 409 Conflict:
{
"success": false,
"error": "member_owns_resources",
"owned_projects": [{ "id": "uuid", "name": "...", "slug": "..." }],
"owned_documents": [
{ "id": "uuid", "title": "...", "project_id": "uuid", "project_name": "..." }
]
}其他错误码: cannot_remove_self (400), cannot_remove_owner (403), target_not_member (404), admin_cannot_manage_non_member → insufficient_privilege (403)
12. 转让团队所有权
POST /teams/:teamId/transfer-ownership将团队所有权从当前 owner 转让给另一位成员。底层调用 transfer_team_ownership RPC。
请求体:
{
"new_owner_id": "uuid",
"team_name_confirmation": "My Team"
}team_name_confirmation必须与teams.name匹配(忽略空白/大小写)作为二次确认new_owner必须是status='accepted'的团队成员
RPC 副作用:UPDATE teams.owner_user_id;原 owner 的 team_members 行通过 ON CONFLICT DO UPDATE 自愈为 role='admin', status='accepted', deleted_at=NULL(兼容脏数据)。
成功响应 200 OK:
{ "success": true, "new_owner_id": "uuid" }错误码: not_owner (403), target_is_self (400), team_name_mismatch (400), target_not_accepted_member (404)
审计
以下权限边界变更在成功路径上异步写入 audit_logs(fire-and-forget,失败仅 warn):
| Action | 触发端点 | 关键 details |
|---|---|---|
team_member_role_change | PATCH role | {teamId, targetUserId, oldRole, newRole} |
team_member_remove | DELETE member | {teamId, targetUserId, cleanup: {roles, permissions}} |
team_ownership_transfer | POST transfer-ownership | {teamId, oldOwnerId, newOwnerId} |
底层 RPC
本次新增两个 SECURITY DEFINER RPC(supabase/sql/20260421_team_member_rpcs.sql),要求调用方为 authenticated 角色(持用户 JWT),以便 auth.uid() 可用。后端路由使用 createUserClient(serverMode, rawJwt) 按请求创建 user-scoped 客户端调用,不在共享 supabaseAdmin 上做 setSession。
remove_team_member(team_id, user_id) → jsonbtransfer_team_ownership(team_id, new_owner_id, team_name_confirmation) → jsonb