fix: allow admin OTP password status
Staging Backend Quality Gate / validate (push) Successful in 12m11s
Staging Backend Quality Gate / publish (push) Successful in 12m43s

This commit is contained in:
Jesse_Chen
2026-08-10 11:52:14 +08:00
parent 65bcef3001
commit 64382c879d
3 changed files with 51 additions and 4 deletions
+16
View File
@@ -2632,3 +2632,19 @@
- 相关记录:BUG-126、BUG-153
- 复发自:无
- 修复版本:本地候选(待 staging push/gate
## BUG-155 | 后台邮箱 OTP 登录后密码状态接口误报未登录
- 状态:resolvedlocal 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 仍返回 401Cookie 继续保持 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 密码状态候选提交
@@ -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();
@@ -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<string> {
async function otpSignIn(
email: string,
host = userHost,
): Promise<string> {
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";