OpenWork Den Admin API 路由深度解析:从 requireAdminMiddleware 鉴权到平台级运营报表
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
导读
本文以 OpenWork 仓库中ee/apps/den-api/src/routes/admin/README.md为骨架,系统讲解 Den API 的 Admin 专属路由面:如何通过requireAdminMiddleware白名单中间件完成平台管理员鉴权、GET /v1/admin/overview的响应结构与分页协议、以及scale-performance.ts中分页参数的边界约束。文章还结合ee/apps/den-api/src/routes/admin/index.ts(约 2100 行)与ee/apps/den-api/src/middleware/admin.ts的源码实现,逐一拆解管理员增删、用户删除、推理用量重置、组织套餐/席位/能力开关等管理端点的请求体与错误语义,并给出仓库内测试用例路径,帮助你掌握这套「白名单鉴权 + 报表只读 + 变更审计」的 admin 路由设计范式。
目录结构:Admin 路由的归属与边界
Den API 的 admin 路由被刻意收敛在独立目录中,避免与 auth、org 等业务路由混在一起:
ee/apps/den-api/src/routes/admin/ ├── README.md # 目录说明(本文骨架) ├── index.ts # 全部 admin 路由注册与处理逻辑(约 2100 行) └── scale-performance.ts # 分页参数归一化与 LIKE 搜索转义工具在ee/apps/den-api/src/app.ts中,路由通过registerAdminRoutes(app)挂载到 Hono 应用上(见 app.ts)。目录说明明确强调三条工程约束(见 README.md):
- 所有路由必须由
requireAdminMiddleware门禁; - 管理报表逻辑留在此处,不混入 auth 或 org 路由;
- 报表标志(如
includeBilling)优先使用 query 校验器处理。
从实际代码看,这一设计已经远超 README 描述的最小形态:目录内不仅注册了 overview 端点,还生长出一整套用户/组织管理端点,并配套了scale-performance.ts来统一分页行为。
鉴权基石:requireAdminMiddleware 与平台管理员白名单
中间件实现
requireAdminMiddleware定义在 middleware/admin.ts,逻辑清晰:
- 从
c.get("user")取当前会话用户,无user.id直接返回401 { error: "unauthorized" }; - 规范化邮箱(
trim().toLowerCase()),为空返回403 { error: "admin_email_required" }; - 调用
isAdminEmailAllowed(email)查询AdminAllowlistTable白名单,不在白名单返回403 { error: "forbidden" }; - 通过后
await next()放行。
关键细节:鉴权依据是邮箱白名单而非角色字段。isAdminEmailAllowed每次调用前都会执行ensureAdminAllowlistSeeded(),把环境变量DEN_BOOTSTRAP_ADMIN_EMAILS中的邮箱幂等写入白名单表(见 admin-allowlist.ts),写入标记为Seeded bootstrap admin。这意味着首次部署时只要配置引导管理员邮箱,该邮箱即可立即具备 admin 权限,无需手动初始化数据库。
路由访问策略标记体系
requireAdminMiddleware同时被注册进 Den API 的「显式访问守卫」集合(见 middleware/route-access.ts)。Den API 路由是**默认拒绝(deny-by-default)**的:每个app.get/post/...注册都必须携带一个显式访问策略标记,requireAdminMiddleware、requireUserMiddleware等共享守卫会执行统一鉴权。adminRoute()工厂函数正是requireAdminMiddleware的别名封装,所有 admin 端点都通过adminRoute()挂载。
test/route-access-policy.test.ts(仓库 evals/specs 中有同名约束)会在 CI 中检查路由是否遗漏访问标记,从机制上杜绝「忘记加鉴权」的漏洞。
GET /v1/admin/overview:初始管理视图
响应结构
overview 是管理后台的首屏数据源,响应遵循adminOverviewResponseSchema(见 index.ts):
viewer:当前管理员自身信息(id、email、name);admins:当前白名单管理员列表(含note与createdAt);summary:全局汇总指标(见下文);users/organizations:受分页约束的首批用户/组织数据;userPage/organizationPage:分页元信息(total、limit、offset、returned、hasMore、search、durationMs);generatedAt:ISO 时间戳。
惰性汇总(deferred summary)设计
overview 故意不在首屏加载重指标。loadAdminInitialOverviewPayload只并行执行三项轻查询:白名单管理员列表、用户首页(includeBilling=false)、组织总数,汇总指标由buildDeferredOverviewSummary填充为null占位(见 index.ts)。真正的分析指标由独立的GET /v1/admin/metrics端点提供(loadAdminMetricsSummary),覆盖:
- 规模:
totalUsers、verifiedUsers、recentUsers7d/30d、totalOrganizations; - Worker:
totalWorkers、cloudWorkers(destination='cloud')、localWorkers、usersWithWorkers、usersWithoutWorkers; - 活跃度:
activeUsers1d/7d/30d(基于 AuthSession + TelemetryEvent 去重),realActiveUsers*(仅统计带session_id的task.started/completed/failed事件),recurringUsers(活跃天数 ≥ 2); - 增长漏斗:
inviters、medianHoursToFirstInvite(注册到首次邀请的间隔中位数); - 30 天趋势序列:
activitySeries每日activeUsers、realActiveUsers、signups三元组。
活跃度聚合窗口为 90 天,趋势序列回看 30 天;telemetry 相关查询都带.catch(() => []),说明遥测表被视为可选数据源,即使缺失也不影响 overview 可用性。
查询参数与分页约束
overview 与用户/组织分页端点共用adminPageQuerySchema(includeBilling、limit、offset、search均为可选字符串)。这些参数最终交给normalizeAdminPageRequest(见 scale-performance.ts)做防御性归一化:
| 参数 | 默认值 | 约束 |
|---|---|---|
limit | 50 | 钳制在[1, 100](ADMIN_MAX_PAGE_LIMIT) |
offset | 0 | 上限100000(ADMIN_MAX_PAGE_OFFSET) |
search | "" | trim后截断至 160 字符 |
非法值(非整数、负数)自动回退默认,因此客户端传limit=9999也不会拖垮数据库。buildAdminPageInfo中的hasMore判定为offset < 100000 && offset + returned < total,保证分页循环有明确的终止条件。
搜索语义:用户与组织
- 用户搜索(
userSearchCondition):若search命中邮箱正则则精确匹配AuthUserTable.email;否则对name、email、id、AuthAccount.providerId以及用户所属组织的名称/角色做LIKE模糊匹配(见 index.ts)。所有%、_、|都经sanitizeAdminSearchForLike转义(|作为转义符,见 scale-performance.ts),防止用户输入注入通配符导致全表扫描。 - 组织搜索(
organizationSearchCondition):对name、slug、id做同样的转义模糊匹配。
平台管理员管理端点
POST /v1/admin/admins —— 新增平台管理员
请求体经createAdminSchema校验:email必须合法且trim().toLowerCase()归一化,note可选(≤255 字符)。成功返回200 { ok, admin };若邮箱已存在,检测到ER_DUP_ENTRY(errno 1062)返回409 { error: "conflict" }(见 index.ts)。ID 由createDenTypeId("adminAllowlist")生成。
DELETE /v1/admin/admins/:adminId —— 移除平台管理员
删除在数据库事务内加FOR UPDATE行锁执行,并内置两条保护规则(见 index.ts):
- 不能删除自己(邮箱与当前 viewer 相同 →
400); - 不能删除最后一个管理员(白名单只剩 1 条 →
400)。
这两条规则从机制上防止管理员误操作把自己锁在系统外。
用户管理端点
GET /v1/admin/users/:userId/inference-usage —— 查询推理用量
返回目标用户在其所属每个组织内、每个计费窗口(window_type)的用量明细:窗口起止、limitAmount、组织总用量used_amount、用户分摊用量(coalesce(sum(bucket_charge.amount), 0))。数据来自InferenceOrgLimitPolicyTable/InferenceOrgUsageBucketTable/InferenceUsageLedgerBucketChargeTable的多表关联(见 index.ts),按组织名与窗口类型升序排列。
POST /v1/admin/users/:userId/inference-usage/reset —— 重置推理用量
在事务中按「策略 → 桶与账目」的顺序加锁(与 admission/settlement 的锁序一致,避免死锁),找出该用户在各组织当前窗口内的所有 charge,扣减桶的used_amount(greatest(used_amount - amount, 0)),并将 charge 的amount置 0 而非删除,从而保留记账身份、防止重试「复活」已豁免的用量(见 index.ts)。响应{ ok, resetAmount }返回释放的总量。
DELETE /v1/admin/users/:userId —— 删除用户
这是最重的一个端点,事务内级联清理:OAuth 令牌/同意/客户端、API Key、会话、账号、Desktop 移交授权、外部身份、SCIM 事件、连接账号、推理成员凭据,并将成员关系软删除(置removedAt)、Worker 归属置空、最后删除AuthUserTable记录(见 index.ts)。事务外还会:
- 调用
revokeGoogleCredentials撤销 Google 凭据; - 逐一失效会话缓存(token 与 session-id 两条缓存条目都要清)与 OAuth grant 缓存;
- 清除受影响组织的成员缓存,并重新同步席位订阅数量(
syncSeatSubscriptionQuantityAfterMemberChange)。
保护规则:管理员不能删除自己的账号(400)。
组织管理端点
PATCH /v1/admin/organizations/:organizationId/plan
请求体{ tier: "free" | "team" | "enterprise", seatLimit: 1..100000 }。写操作通过updateOrganizationMetadata原子合并元数据:写入{ tier, source: "manual" }(enterprise 额外记grantedAt),并把limits.members更新为seatLimit(见 index.ts)。响应回显parseOrganizationPlan的结果,便于客户端立即确认生效。
PATCH /v1/admin/organizations/:organizationId/free-seats
请求体{ totalFreeSeats },下限为calculateOrganizationSeatBillingCounts({ memberCount: 0 }).includedFree(即默认免费席位),上限 100000。实现上把「超出默认免费席位的部分」写入seatsFreeAdditional元数据,随后重算getOrganizationSeatBillingCounts并同步 Stripe 订阅数量(见 index.ts)。响应给出memberCount、freeSeatCount、seatsFreeAdditional、billableSeatCount四个计费口径。
PATCH /v1/admin/organizations/:organizationId/dpa
记录 DPA(数据处理协议)签署决策:事务内FOR UPDATE读组织元数据,readOrganizationMetadata解析失败返回503 managed_models_policy_unavailable;成功则更新dpaSigned并写入一条AuditEventTable审计事件(记录 actor、前后值与 reason),实现「决策 + 审计」的原子提交(见 index.ts)。变更经logOrganizationAuditEvent输出。
PUT /v1/admin/organizations/:organizationId/openwork-web-access
授予/撤销组织的 OpenWork Web 免费访问(complimentary access)。请求体{ enabled: boolean, reason: 3..500 字符 }。关键约束:存在进行中的付费 OpenWork Web 订阅时不能授予免费访问(返回409 openwork_web_subscription_exists),事务内二次检查订阅状态防竞态;操作同样写入审计事件(openWorkWebComplimentaryAccessGranted/Revoked,见 index.ts)。
GET/PUT /v1/admin/organizations/:organizationId/capabilities
管理组织的功能开关(capability overrides),当前可见四个维度:installLinks、mcpConnections、modelsAnalytics、gatewayDashboard。PUT支持true/false/null三种语义(null表示删除覆盖、回归默认,见 index.ts)。readUnmanagedCapabilityMetadata会过滤已退役的键(workflows、codemodeScripts、remoteMcpApps、cloud),避免过期覆盖项透传到下游。
分页与报表端点
GET /v1/admin/users 与 GET /v1/admin/organizations
两者共用queryValidator(adminPageQuerySchema)与normalizeAdminPageRequest,返回{ users/organizations, page, generatedAt }。用户页在includeBilling=true时逐页加载计费状态(paid/unpaid/unavailable),并通过mapWithConcurrency(subscriptionIds, 4, refreshOrgSubscriptionFromStripe)以 4 并发刷新 Stripe 订阅,保证页面规模的计费口径新鲜(见 index.ts)。组织页则计算plan、seatLimit、免费/可计费席位、能力开关与 OpenWork Web 访问状态(见 index.ts)。
GET /v1/admin/metrics
即上文「惰性汇总」的完整版:返回{ summary, generatedAt },是 overview 首屏之后按需加载的分析接口(见 index.ts)。
GET /v1/admin/overview
返回初始视图,queryValidator(overviewQuerySchema)校验includeBilling、limit、offset、search(见 index.ts)。
错误语义与 OpenAPI 文档化
所有 admin 端点通过hono-openapi的describeRoute声明,统一挂在tags: ["Admin"]下,并复用adminRouteErrors常量(见 index.ts):
| 状态码 | 语义 |
|---|---|
400 | 请求体/参数/ID 非法,{ error: "invalid_request", message } |
401 | 未认证,{ error: "unauthorized" } |
403 | 已认证但非白名单管理员,{ error: "forbidden" } |
404 | 目标用户/组织不存在,{ error: "not_found", message } |
409 | 冲突(重复 admin、存在付费订阅等),{ error, message } |
503 | 元数据策略不可读,managed_models_policy_unavailable |
文档化描述同时充当 API 契约:请求体字段、成功与失败响应结构都在describeRoute的responses中逐一声明,可作为生成 OpenAPI 文档与客户端类型的唯一事实来源。
测试佐证:仓库内验证路径
Admin 面在ee/apps/den-api/test/下有专门测试覆盖,可作为理解行为边界的补充材料:
admin-scale-performance.test.ts:验证分页参数归一化(limit/offset/search 边界);admin-management-inference-usage.test.ts:验证推理用量查询与重置的事务语义;admin-delete-user-id-validation.test.ts:验证删除用户时的 ID 校验与自我保护;admin-capabilities.test.ts、admin-organization-capabilities.test.ts:验证组织能力开关的读写与null清除语义;admin-mcp.test.ts、admin-mcp-routes.test.ts:验证 admin 面与 den-admin MCP 的联动。
从 README 到实现的演进小结
README 描述的是「小而美」的起点——一个 overview 端点、一个独立目录。而当前index.ts已扩展为完整的平台运营面:白名单管理员生命周期、用户删除与推理用量管理、组织套餐/席位/DPA/功能开关管理,以及带分页与惰性加载的报表体系。不变的是三条骨架约束依旧成立:
- 鉴权统一收敛:全部端点经
adminRoute()(即requireAdminMiddleware)门禁,白名单 + deny-by-default 策略标记双保险; - 职责隔离:管理逻辑保留在
routes/admin/,不侵入 auth/org 路由; - 参数防御:分页与搜索统一走
normalizeAdminPageRequest+sanitizeAdminSearchForLike,报表标志(如includeBilling)通过 query 校验器解析。
对开发者而言,这套设计可以直接借鉴:以中间件工厂 + 显式守卫标记 + 白名单表构建管理面鉴权,以「首屏轻量 + 重指标惰性加载」平衡管理后台性能,以审计事件表为每次管理变更留痕。如需在自部署 Den 环境中启用管理面,先在DEN_BOOTSTRAP_ADMIN_EMAILS配置引导管理员邮箱即可。
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考