fix: allow admin OTP password status
This commit is contained in:
@@ -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 密码状态候选提交
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user