OpenWork Den API 的 /v1/me 路由:当前认证用户与活跃组织的实现指南
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
本指南以 ee/apps/den-api/src/routes/me/README.md 为骨架,深入剖析 Den API 中负责"当前登录用户"(current actor)的me路由模块:它注册了/v1/me与/v1/me/orgs两个端点,并围绕"返回当前用户/会话负载、解析当前用户所属组织、暴露当前会话的活跃组织选择数据"三项职责展开。读完本文,你将掌握 me 模块的路由清单、鉴权中间件选型、活跃组织解析逻辑、限流与错误处理细节,以及如何在现有代码结构上继续扩展"当前用户"相关子域。
一、模块定位:只服务"当前用户"这一角色
me文件夹是 Den API 中对"当前认证用户"(the currently authenticated user)相关路由的归属地。它的边界非常清晰:只面向当前 actor(调用者自己),不做任何任意的用户管理操作(如管理员查看/修改他人资料)。这一边界被明确记录在 README 的 Notes for future work 中:
- 保持该文件夹聚焦于当前 actor,而非任意的用户管理操作;
- 如果后续出现更多"当前用户"子区域,应在本文件夹内拆分为独立文件(而不是塞进
index.ts或别处)。
从源码结构看,目前该文件夹只有两个文件:README.md 与 index.ts,其中index.ts是唯一的实现文件,负责注册/v1/me与/v1/me/orgs。按 README 的规划,未来若出现新的当前用户子域(例如"我的通知设置"、"我的设备"),应拆成me/profile.ts、me/devices.ts之类的额外文件,保持单一职责。
二、路由清单与三项核心职责
2.1 README 声明的职责
README 将模块职责归纳为三点:
- 返回当前认证的用户/会话负载(return the current authenticated user/session payload);
- 解析当前用户所属的组织(resolve the orgs the current user belongs to);
- 暴露当前会话的活跃组织选择数据(expose active org selection data for the current session)。
2.2 实际实现:五个端点
对照 index.ts 的实际注册代码,registerMeRoutes共挂载了五个路由(README 只点名了前两个,后三个是模块演化后的实际扩展,全部归属同一主题域):
| 方法 | 路径 | 中间件 | 职责 |
|---|---|---|---|
GET | /v1/me | authenticatedRoute() | 返回当前用户与活跃会话详情 |
GET | /v1/me/orgs | orgMemberRoute({ useUserOrganizations: true }) | 列出当前用户可见的组织并标记活跃组织 |
POST | /v1/me/send-download-link | authenticatedRoute() | 向当前用户邮箱发送桌面端下载链接 |
PATCH | /v1/me/profile | authenticatedRoute()+jsonValidator(updateProfileSchema) | 更新当前用户的显示姓名 |
POST | /v1/me/active-organization | userSessionRoute()+jsonValidator(setActiveOrganizationSchema) | 为当前会话切换活跃组织(hide: true,仅内部使用) |
GET | /v1/me/desktop-config | orgMemberRoute() | 返回当前用户活跃组织的桌面端限制配置 |
所有路由在 Hono OpenAPI 描述中都被标记为tags: ["Users"]。注册入口位于 app.ts 的 registerMeRoutes(app),与registerOrgRoutes、registerVersionRoutes、registerWebhookRoutes等并列挂载到同一 Hono 应用上。
三、中间件选型:authenticatedRoute 与 orgMemberRoute
README 的"Middleware expectations"一节给出了两条铁律:
- 路由需要已认证用户时,使用
requireUserMiddleware; - 路由需要组织成员上下文时,使用
resolveUserOrganizationsMiddleware。
在 Den API 中,这些底层中间件被封装成了更语义化的路由守卫,定义于 middleware/route-access.ts:
export function authenticatedRoute(): MiddlewareHandler<{ Variables: AuthContextVariables }> { return requireUserMiddleware } export function userSessionRoute(): MiddlewareHandler<{ Variables: AuthContextVariables }> { return requireUserSessionMiddleware } export function orgMemberRoute(options: { useUserOrganizations: true }): typeof resolveUserOrganizationsMiddleware export function orgMemberRoute(): typeof resolveOrganizationContextMiddleware export function orgMemberRoute(options?: { useUserOrganizations: true }) { if (options?.useUserOrganizations) { return resolveUserOrganizationsMiddleware } return resolveOrganizationContextMiddleware }3.1 requireUserMiddleware:最基础的登录校验
实现位于 middleware/current-user.ts:只检查c.get("user")?.id是否存在,不存在则直接返回401 { error: "unauthorized" }。这是 me 模块大多数端点的"最小鉴权",因为它只需要"这个人登录了",不需要组织上下文。
3.2 requireUserSessionMiddleware:要求真实的用户会话
与requireUserMiddleware不同,userSessionRoute()背后的 requireUserSessionMiddleware 在用户已认证的基础上,还额外要求:
- 请求不能是 API Key 调用(
c.get("apiKey")为空); - 会话必须存在且携带 token;
- 会话 ID 必须能通过
normalizeDenTypeId("session", ...)的类型校验。
任一条件不满足,返回403。这正是/v1/me/active-organization使用它的原因:切换活跃组织是"会话级"操作,只允许带登录会话的客户端执行,API Key 与无 token 的调用一律拒绝(详见 index.ts 中的路由描述:"This is used by desktop bearer-token sessions that cannot call Better Auth's cookie-backed organization endpoint.")。
3.3 resolveUserOrganizationsMiddleware:组织成员上下文
这是 me 模块中信息量最大的中间件,实现在 middleware/user-organizations.ts。它的执行流程:
- 校验用户已认证(无 user 直接 401);
- 解析组织作用域来源,优先级为:API Key 绑定的组织(
getApiKeyScopedOrganizationId)→ 请求头x-openwork-org-id(ORG_SCOPE_HEADER)→ 遗留头x-openwork-legacy-org-id(LEGACY_ORG_PROXY_HEADER); - 调用
resolveUserOrganizations解析用户可见组织与活跃组织; - 若存在作用域组织,将组织列表过滤为仅该组织;
- 若会话尚未持久化活跃组织且可解析出活跃组织,则调用
hydrateSessionActiveOrganization回写会话(保证首次进入组织时会话状态自愈); - 在 Hono context 中写入
userOrganizations、activeOrganizationId、activeOrganizationSlug三个变量。
/v1/me/orgs使用orgMemberRoute({ useUserOrganizations: true })正是为了获得上述三个 context 变量来组装响应;/v1/me/desktop-config则使用不带参数的orgMemberRoute()(即resolveOrganizationContextMiddleware),因为桌面配置需要的是"当前组织 + 当前成员"的完整上下文(organizationContext.organization与organizationContext.currentMember)。
3.4 路由守卫的显式注册检查
值得注意的是 route-access.ts 的 explicitAuthGuardHandlers 用WeakSet显式登记了requireUserMiddleware、requireUserSessionMiddleware、resolveOrganizationContextMiddleware、resolveUserOrganizationsMiddleware等守卫,并配合hasExplicitAuthGuardHandler做审计,防止路由悄悄绕过鉴权。这说明 me 模块采用的中间件不仅是行为约定,还处于 Den API 的鉴权静态检查体系之内。
四、GET /v1/me:当前用户与登录方式汇总
该端点(index.ts 第 150-183 行)在authenticatedRoute()保护下,返回:
{ "user": { "...用户字段...", "authProviders": ["email", "sso"] }, "session": { "...当前会话字段..." } }实现细节中值得注意的两点:
- authProviders 的推导:路由查询
AuthAccountTable中该用户的所有账号,取providerId去重排序,再经 normalizeAuthProvider 归一化:credential/email-password→email,openwork-sso-*→sso,openwork-scim-*→scim,其余原样返回。客户端据此可判断该用户是通过邮箱密码、企业 SSO 还是 SCIM 开通的账号。 - 响应 Schema:
meResponseSchema(CurrentUserResponse)对user与session采用z.object({}).passthrough(),即验证结构但放行额外字段,避免未来新增字段时破坏兼容。
五、GET /v1/me/orgs:组织列表与活跃标记
该端点(index.ts 第 185-209 行)返回当前用户可见的组织,并标注哪个组织处于活跃状态:
{ "orgs": [ { "id": "...", "name": "...", "slug": "...", "...": "...", "isActive": true } ], "activeOrgId": "org_...", "activeOrgSlug": "acme" }响应中每个组织的isActive由org.id === c.get("activeOrganizationId")计算得出;activeOrgId/activeOrgSlug直接来自resolveUserOrganizationsMiddleware写入的 context 变量,未解析到时为null。底层组织解析逻辑在 orgs.ts 的 resolveUserOrganizations:
- 先调用
ensureUserOrgAccess确保用户具备组织访问资格; - 若部署为
single_org模式(env.orgMode === "single_org"),只保留 slug 等于env.singleOrg.slug的那个组织,其他一律过滤; - 活跃组织的判定优先级:请求携带的
activeOrganizationId(须属于可见组织)→ 若可见组织恰好只有一个,则自动以该组织为活跃组织。
多组织模式(multi_org)下,活跃组织来自会话的activeOrganizationId(或请求作用域头)。DEN_ORG_MODE环境变量的解析规则见 env.ts 的 parseDenOrgMode:未设置时默认single_org,只接受single_org与multi_org两个取值,其他值直接抛错。
六、PATCH /v1/me/profile:更新显示姓名
该端点(index.ts 第 268-304 行)用于更新当前用户的显示姓名,请求体 Schema 为 updateProfileSchema:
const updateProfileSchema = z.object({ firstName: z.string().trim().max(120), lastName: z.string().trim().max(120), }).refine((value) => value.firstName.length > 0 || value.lastName.length > 0, { message: "Enter a first or last name.", })校验要点:
- 两个字段均可选,但至少填一个(
refine兜底); - 每个字段最长 120 字符,自动
trim(); - 服务端通过
normalizeNamePart将连续空白折叠为单个空格,再以[firstName, lastName].filter(Boolean).join(" ")拼接成完整name写入AuthUserTable; - 更新后调用
cache.auth.deleteSessionsForUser使该用户的会话缓存失效,保证后续请求立刻读到新名字; - 响应直接回显新的
name与updatedAt。
七、POST /v1/me/send-download-link:桌面端下载链接发送
该端点(index.ts 第 211-266 行)向当前用户邮箱发送桌面端下载链接,用于桌面端登录后自助获取安装包。它集中体现了 me 模块的错误处理设计,值得完整列出:
400— 账号缺少邮箱:{ error: "user_email_required" }(user.email?.trim()为空时返回)。
429— 限流:核心参数定义在文件顶部:
const DOWNLOAD_LINK_RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000 // 1 小时窗口 const DOWNLOAD_LINK_RATE_LIMIT_MAX = 5 // 最多 5 次限流实现为 checkDownloadLinkRateLimit:以me:send-download-link:${userId}为 key 写入RateLimitTable,窗口内计数达到 5 次即拒绝,并在响应头返回Retry-After(秒数)。窗口滚动逻辑:距上次请求超过 1 小时则计数重置为 1,否则累加。
502— 邮件发送失败:捕获DenEmailSendError,按error.reason细分错误信息:
| reason | 含义 |
|---|---|
email_not_configured | 部署未配置邮件服务 |
resend_rejected | Resend 拒绝投递(含error.detail详情) |
resend_network | 无法连接 Resend |
nodemailer_rejected | Nodemailer 拒绝投递 |
邮件走sendEmail工具,使用downloadLink模板并注入downloadUrl: OPENWORK_DOWNLOAD_URL。非DenEmailSendError的异常会被重新抛出,交由全局错误处理。
八、POST /v1/me/active-organization:会话级活跃组织切换
该端点(index.ts 第 306-352 行)被describeRoute标记为hide: true(不出现在公开 API 文档中),专门服务于无法调用 Better Auth cookie 组织端点的桌面 bearer-token 会话。请求体支持二选一:
const setActiveOrganizationSchema = z.object({ organizationId: denTypeIdSchema("organization").optional(), organizationSlug: z.string().trim().min(1).max(255).optional(), }).refine((value) => value.organizationId !== undefined || value.organizationSlug !== undefined, { message: "Provide an organization id or slug.", })切换逻辑的关键在于归属校验与单组织模式的收紧:
- 先调用
resolveUserOrganizations解析目标组织; single_org模式下走 getAllowedSingleOrgActiveOrganization:若用户只有一个组织,且请求的组织 ID/Slug 与该组织一致(或未指定),才允许激活;否则返回null触发 403;multi_org模式下按请求的organizationId或organizationSlug在用户可见组织中查找;- 未找到匹配组织 →
403 { error: "forbidden", message: "You do not have access to this organization." }; - 匹配成功 → 调用 setSessionActiveOrganization 将
activeOrganizationId持久化到AuthSessionTable,并同步更新当前 Hono context 中的 session,响应返回{ activeOrgId, activeOrgSlug }。
相关行为在 test/bearer-session.test.ts 中有测试覆盖:使用x-api-key调用/v1/me/active-organization时,由于userSessionRoute()拒绝 API Key,期望得到 403。
九、GET /v1/me/desktop-config:活跃组织的桌面端策略
该端点(index.ts 第 354-399 行)返回桌面端应用在当前活跃组织下应遵守的配置,响应 Schema 为desktopConfigSchema(CurrentUserDesktopConfigResponse)。其组装逻辑:
- 从
organizationContext取出organization与currentMember; - 调用
normalizeOrganizationMetadata规范化组织元数据; - 调用 calculateDesktopPolicyForOrgMember 计算该成员生效的桌面策略(
desktop-policies.ts同目录)——例如策略版本与升级控制; - 叠加运行时的能力开关:
automationsEnabled: env.automations.enableddashboardEnabled: env.dashboardsEnabledconnectEnabled: memberFacingMcpConnectionsEnabled(...)(受env.mcpConnectionsGatingEnabled门控)
- 从组织元数据中按需透传品牌化字段(存在才返回):
allowedDesktopVersions(允许的桌面版本白名单)brandAppName、brandLogoUrl、brandIconUrl、brandAccentColor(自定义品牌名/Logo/图标/强调色)
桌面端首次启动时可用该端点一次性拉齐"我的组织下能用哪些能力、必须用哪个版本、显示什么品牌",避免客户端各自猜测策略。
十、鉴权路径速查:三种客户端如何调用 me 端点
从 app.ts 的根文档路由 可以确认 Den API 支持三种调用方式:
- 用户会话:
Authorization: Bearer <session-token>,适用于所有 me 端点; - 组织 API Key:
x-api-key: den_...,API Key 会解析到创建它的用户及其绑定的组织成员身份,因此可以调用普通用户/组织路由——但注意/v1/me/active-organization与/v1/me/orgs这类需要真实会话或组织上下文的端点有额外限制(前者直接拒绝 API Key,后者会按 Key 绑定的组织过滤列表); - 公开路由:健康检查与文档等无需认证。
# 获取当前用户与登录方式 curl https://<den-host>/v1/me -H "Authorization: Bearer <session-token>" # 获取当前用户的组织列表(含活跃标记) curl https://<den-host>/v1/me/orgs -H "Authorization: Bearer <session-token>" # 通过组织 API Key 调用(按 Key 作用域过滤组织) curl https://<den-host>/v1/me/orgs -H "x-api-key: den_..." # 更新显示姓名 curl -X PATCH https://<den-host>/v1/me/profile \ -H "Authorization: Bearer <session-token>" \ -H "Content-Type: application/json" \ -d '{"firstName": "Ada", "lastName": "Lovelace"}'十一、扩展指南:如何在 me 文件夹下添加新端点
遵循 README 的边界与 Den API 的既有惯例,新增"当前用户"子域端点时应:
- 判断归属:只处理"调用者自己的资源"(我的资料、我的设备、我的会话、我的通知偏好等);凡涉及操作其他用户或全局管理,一律放到
routes/org、routes/admin等目录。 - 拆文件:不要继续把端点堆进 index.ts,按子域新建
me/xxx.ts,每个文件导出自己的registerXxxRoutes(app),在index.ts或 app.ts 中组合调用。 - 选中间件:只需要登录 →
authenticatedRoute()(requireUserMiddleware);需要组织成员上下文 →orgMemberRoute({ useUserOrganizations: true })或orgMemberRoute();需要"真实会话"(拒绝 API Key)→userSessionRoute()。 - 写 OpenAPI 描述:所有端点都通过
describeRoute提供tags: ["Users"]、summary、description与响应 Schema(复用denTypeIdSchema、unauthorizedSchema、invalidRequestSchema、jsonResponse等 openapi.js 工具)。 - 校验入参:请求体用
jsonValidator(schema)配合 Zod,trim()与长度上限是 me 模块的一致风格。
十二、总结
me路由模块是 Den API 中"当前用户"语义的唯一入口:它用最小的中间件组合(requireUserMiddleware/resolveUserOrganizationsMiddleware/requireUserSessionMiddleware)区分"已登录"、"组织成员"与"真实会话"三种信任级别,在/v1/me/orgs与/v1/me/active-organization中完整实现了从组织解析 → 活跃组织判定 → 会话持久化的闭环,并通过/v1/me/desktop-config把组织级策略(桌面版本、能力开关、品牌定制)下发给桌面端。理解这个模块,是理解 Den API 面向用户侧 API 设计的起点——尤其是组织作用域解析与 single_org 模式下的收紧逻辑,它们决定了多租户与单租户部署下"我属于哪些组织、我当前在哪个组织"这两个问题的答案。
进一步阅读:路由注册入口 app.ts、组织解析与活跃组织持久化 orgs.ts、鉴权中间件 current-user.ts、组织上下文中间件 user-organizations.ts、会话/API Key 鉴权测试 bearer-session.test.ts。
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考