From 64382c879d9a29fad9e9258987bd5b1e6dca1ed0 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 10 Aug 2026 11:52:14 +0800 Subject: [PATCH] fix: allow admin OTP password status --- docs/BUG_HISTORY.md | 16 ++++++++ .../src/app/api/account/password/route.ts | 2 +- .../tests/identity-auth-integration.test.ts | 37 +++++++++++++++++-- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 014602c7..132d898f 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2632,3 +2632,19 @@ - 相关记录:BUG-126、BUG-153 - 复发自:无 - 修复版本:本地候选(待 staging push/gate) + +## BUG-155 | 后台邮箱 OTP 登录后密码状态接口误报未登录 + +- 状态:resolved(local candidate,待 staging gate/deployment) +- 首次发现:2026-08-10 +- 最近更新:2026-08-10 +- 影响面:独立后台域名的邮箱 OTP 登录、首次密码设置引导与 `GET/POST /api/account/password`;普通用户域名、未知 Host 拒绝和后台 RBAC 不放宽。 +- 用户现象:邮箱 OTP 校验成功并已创建 Better Auth session,但页面随后提示“暂时无法确认密码状态,请稍后再试”,密码状态接口返回 401“请先登录”。 +- 触发条件:浏览器在已配置的 admin origin 完成邮箱 OTP 登录后,用同一 host-only session Cookie 请求 `/api/account/password`。 +- 根因:密码状态 route 在读取 session 前把身份 surface 硬限制为 `user`;admin host 虽然是已识别身份域名且持有有效 user session,仍被提前拒绝。前端把该非 2xx 响应映射成密码状态暂不可用。 +- 修复:密码状态 route 继续要求 self-hosted identity、已识别 Host 和有效 user session,但允许 `user` 与 `admin` 两个已配置 surface;未知 Host 仍返回 401,Cookie 继续保持 host-only,不引入跨域会话共享。 +- 验证:身份集成回归新增 admin host `OTP -> session -> GET /api/account/password`,无密码账户必须返回 200 与 `hasPassword=false`;同一 Cookie 改投未知 Host 仍必须返回 401。 +- 防复发:共享 user identity session 的账户自助接口应校验“已识别身份 surface”,只有明确属于普通站的业务接口才限制 `surface=user`;任何 admin OTP 登录回归都必须继续检查登录后密码状态探测。 +- 相关记录:BUG-123、BUG-139 +- 复发自:无 +- 修复版本:本次后台 OTP 密码状态候选提交 diff --git a/frontend/src/app/api/account/password/route.ts b/frontend/src/app/api/account/password/route.ts index 6e8febf8..8a6c9d13 100644 --- a/frontend/src/app/api/account/password/route.ts +++ b/frontend/src/app/api/account/password/route.ts @@ -10,7 +10,7 @@ export const dynamic = "force-dynamic"; async function userSession(request: Request) { if (!isSelfHostedIdentityEnabled(process.env)) return null; const config = readSelfHostedIdentityConfig(process.env); - if (resolveIdentitySurface(request.headers.get("host"), config) !== "user") { + if (!resolveIdentitySurface(request.headers.get("host"), config)) { return null; } const services = getIdentityAuthServices(); diff --git a/frontend/tests/identity-auth-integration.test.ts b/frontend/tests/identity-auth-integration.test.ts index 1be745e8..cebeb0d2 100644 --- a/frontend/tests/identity-auth-integration.test.ts +++ b/frontend/tests/identity-auth-integration.test.ts @@ -163,9 +163,12 @@ test("Better Auth supports shared user OTP/password sessions for admins", async user: toNextJsHandler(services.user), }); - async function otpSignIn(email: string): Promise { + async function otpSignIn( + email: string, + host = userHost, + ): Promise { const send = await handlers.POST( - request(userHost, "/api/auth/email-otp/send-verification-otp", { + request(host, "/api/auth/email-otp/send-verification-otp", { email, type: "sign-in", }), @@ -176,7 +179,7 @@ test("Better Auth supports shared user OTP/password sessions for admins", async assert.equal(message?.type, "sign-in"); const signIn = await handlers.POST( - request(userHost, "/api/auth/sign-in/email-otp", { + request(host, "/api/auth/sign-in/email-otp", { email, otp: message?.otp, }), @@ -235,6 +238,34 @@ test("Better Auth supports shared user OTP/password sessions for admins", async 200, ); + const adminOtpCookie = await otpSignIn( + "admin-otp@example.com", + adminHost, + ); + const adminOtpStatus = await getPasswordStatus( + request( + adminHost, + "/api/account/password", + undefined, + adminOtpCookie, + ), + ); + assert.equal(adminOtpStatus.status, 200); + assert.deepEqual(await adminOtpStatus.json(), { hasPassword: false }); + assert.equal( + ( + await getPasswordStatus( + request( + "unrecognized.example.com", + "/api/account/password", + undefined, + adminOtpCookie, + ), + ) + ).status, + 401, + ); + const newEmail = "new-user@example.com"; const firstPassword = "first-password"; const resetPassword = "reset-password";