news 2026/9/13 5:34:23

OpenWork Den API 的 /v1/me 路由:当前认证用户与活跃组织的实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenWork Den API 的 /v1/me 路由:当前认证用户与活跃组织的实现指南

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.tsme/devices.ts之类的额外文件,保持单一职责。

二、路由清单与三项核心职责

2.1 README 声明的职责

README 将模块职责归纳为三点:

  1. 返回当前认证的用户/会话负载(return the current authenticated user/session payload);
  2. 解析当前用户所属的组织(resolve the orgs the current user belongs to);
  3. 暴露当前会话的活跃组织选择数据(expose active org selection data for the current session)。

2.2 实际实现:五个端点

对照 index.ts 的实际注册代码,registerMeRoutes共挂载了五个路由(README 只点名了前两个,后三个是模块演化后的实际扩展,全部归属同一主题域):

方法路径中间件职责
GET/v1/meauthenticatedRoute()返回当前用户与活跃会话详情
GET/v1/me/orgsorgMemberRoute({ useUserOrganizations: true })列出当前用户可见的组织并标记活跃组织
POST/v1/me/send-download-linkauthenticatedRoute()向当前用户邮箱发送桌面端下载链接
PATCH/v1/me/profileauthenticatedRoute()+jsonValidator(updateProfileSchema)更新当前用户的显示姓名
POST/v1/me/active-organizationuserSessionRoute()+jsonValidator(setActiveOrganizationSchema)为当前会话切换活跃组织(hide: true,仅内部使用)
GET/v1/me/desktop-configorgMemberRoute()返回当前用户活跃组织的桌面端限制配置

所有路由在 Hono OpenAPI 描述中都被标记为tags: ["Users"]。注册入口位于 app.ts 的 registerMeRoutes(app),与registerOrgRoutesregisterVersionRoutesregisterWebhookRoutes等并列挂载到同一 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。它的执行流程:

  1. 校验用户已认证(无 user 直接 401);
  2. 解析组织作用域来源,优先级为:API Key 绑定的组织(getApiKeyScopedOrganizationId)→ 请求头x-openwork-org-idORG_SCOPE_HEADER)→ 遗留头x-openwork-legacy-org-idLEGACY_ORG_PROXY_HEADER);
  3. 调用resolveUserOrganizations解析用户可见组织与活跃组织;
  4. 若存在作用域组织,将组织列表过滤为仅该组织
  5. 若会话尚未持久化活跃组织且可解析出活跃组织,则调用hydrateSessionActiveOrganization回写会话(保证首次进入组织时会话状态自愈);
  6. 在 Hono context 中写入userOrganizationsactiveOrganizationIdactiveOrganizationSlug三个变量。

/v1/me/orgs使用orgMemberRoute({ useUserOrganizations: true })正是为了获得上述三个 context 变量来组装响应;/v1/me/desktop-config则使用不带参数的orgMemberRoute()(即resolveOrganizationContextMiddleware),因为桌面配置需要的是"当前组织 + 当前成员"的完整上下文(organizationContext.organizationorganizationContext.currentMember)。

3.4 路由守卫的显式注册检查

值得注意的是 route-access.ts 的 explicitAuthGuardHandlers 用WeakSet显式登记了requireUserMiddlewarerequireUserSessionMiddlewareresolveOrganizationContextMiddlewareresolveUserOrganizationsMiddleware等守卫,并配合hasExplicitAuthGuardHandler做审计,防止路由悄悄绕过鉴权。这说明 me 模块采用的中间件不仅是行为约定,还处于 Den API 的鉴权静态检查体系之内。

四、GET /v1/me:当前用户与登录方式汇总

该端点(index.ts 第 150-183 行)在authenticatedRoute()保护下,返回:

{ "user": { "...用户字段...", "authProviders": ["email", "sso"] }, "session": { "...当前会话字段..." } }

实现细节中值得注意的两点:

  1. authProviders 的推导:路由查询AuthAccountTable中该用户的所有账号,取providerId去重排序,再经 normalizeAuthProvider 归一化:credential/email-passwordemailopenwork-sso-*ssoopenwork-scim-*scim,其余原样返回。客户端据此可判断该用户是通过邮箱密码、企业 SSO 还是 SCIM 开通的账号。
  2. 响应 SchemameResponseSchemaCurrentUserResponse)对usersession采用z.object({}).passthrough(),即验证结构但放行额外字段,避免未来新增字段时破坏兼容。

五、GET /v1/me/orgs:组织列表与活跃标记

该端点(index.ts 第 185-209 行)返回当前用户可见的组织,并标注哪个组织处于活跃状态:

{ "orgs": [ { "id": "...", "name": "...", "slug": "...", "...": "...", "isActive": true } ], "activeOrgId": "org_...", "activeOrgSlug": "acme" }

响应中每个组织的isActiveorg.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_orgmulti_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使该用户的会话缓存失效,保证后续请求立刻读到新名字;
  • 响应直接回显新的nameupdatedAt

七、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_rejectedResend 拒绝投递(含error.detail详情)
resend_network无法连接 Resend
nodemailer_rejectedNodemailer 拒绝投递

邮件走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模式下按请求的organizationIdorganizationSlug在用户可见组织中查找;
  • 未找到匹配组织 →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 为desktopConfigSchemaCurrentUserDesktopConfigResponse)。其组装逻辑:

  1. organizationContext取出organizationcurrentMember
  2. 调用normalizeOrganizationMetadata规范化组织元数据;
  3. 调用 calculateDesktopPolicyForOrgMember 计算该成员生效的桌面策略(desktop-policies.ts同目录)——例如策略版本与升级控制;
  4. 叠加运行时的能力开关
    • automationsEnabled: env.automations.enabled
    • dashboardEnabled: env.dashboardsEnabled
    • connectEnabled: memberFacingMcpConnectionsEnabled(...)(受env.mcpConnectionsGatingEnabled门控)
  5. 从组织元数据中按需透传品牌化字段(存在才返回):
    • allowedDesktopVersions(允许的桌面版本白名单)
    • brandAppNamebrandLogoUrlbrandIconUrlbrandAccentColor(自定义品牌名/Logo/图标/强调色)

桌面端首次启动时可用该端点一次性拉齐"我的组织下能用哪些能力、必须用哪个版本、显示什么品牌",避免客户端各自猜测策略。

十、鉴权路径速查:三种客户端如何调用 me 端点

从 app.ts 的根文档路由 可以确认 Den API 支持三种调用方式:

  1. 用户会话Authorization: Bearer <session-token>,适用于所有 me 端点;
  2. 组织 API Keyx-api-key: den_...,API Key 会解析到创建它的用户及其绑定的组织成员身份,因此可以调用普通用户/组织路由——但注意/v1/me/active-organization/v1/me/orgs这类需要真实会话组织上下文的端点有额外限制(前者直接拒绝 API Key,后者会按 Key 绑定的组织过滤列表);
  3. 公开路由:健康检查与文档等无需认证。
# 获取当前用户与登录方式 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 的既有惯例,新增"当前用户"子域端点时应:

  1. 判断归属:只处理"调用者自己的资源"(我的资料、我的设备、我的会话、我的通知偏好等);凡涉及操作其他用户或全局管理,一律放到routes/orgroutes/admin等目录。
  2. 拆文件:不要继续把端点堆进 index.ts,按子域新建me/xxx.ts,每个文件导出自己的registerXxxRoutes(app),在index.ts或 app.ts 中组合调用。
  3. 选中间件:只需要登录 →authenticatedRoute()requireUserMiddleware);需要组织成员上下文 →orgMemberRoute({ useUserOrganizations: true })orgMemberRoute();需要"真实会话"(拒绝 API Key)→userSessionRoute()
  4. 写 OpenAPI 描述:所有端点都通过describeRoute提供tags: ["Users"]summarydescription与响应 Schema(复用denTypeIdSchemaunauthorizedSchemainvalidRequestSchemajsonResponse等 openapi.js 工具)。
  5. 校验入参:请求体用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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 5:33:01

Spring注解开发核心原理与最佳实践

1. Spring注解开发概述Spring框架自2003年诞生以来&#xff0c;已经成为Java企业级开发的事实标准。而注解(Annotation)作为Java 5引入的重要特性&#xff0c;在Spring 3.0版本后逐渐成为配置的主流方式。注解开发模式通过将配置信息直接嵌入到代码中&#xff0c;极大地简化了传…

作者头像 李华
网站建设 2026/9/13 5:30:02

15分钟跑通DataHub元数据管理:3个由浅入深的定制配方

15分钟跑通DataHub元数据管理&#xff1a;3个由浅入深的定制配方 【免费下载链接】datahub The Context Platform for your Data and AI Stack 项目地址: https://gitcode.com/GitHub_Trending/da/datahub 周四下午产品来催&#xff1a;周五前要把 Snowflake 里所有表的…

作者头像 李华
网站建设 2026/9/13 5:28:33

Budibase 开发环境在平台更新后出现不兼容问题时如何重置恢复

Budibase 开发环境在平台更新后出现不兼容问题时如何重置恢复 【免费下载链接】budibase AI agents, automations and apps that run your operations. Model agnostic. 项目地址: https://gitcode.com/GitHub_Trending/bu/budibase 如果你在本地开发 Budibase&#xff…

作者头像 李华