From 2d620fb4ad204c88e32660f77a31c252edf7f94a Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 1 Oct 2026 22:43:34 +0800 Subject: [PATCH] fix(identity): staging fixed-code sign-in skips the first-password step (BUG-1152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the staging test-OTP channel, 验证码登录 with a fresh mailbox no longer stops at 设置登录密码; it records consent and enters the app. 注册账号 still sets a password, second factor still comes first, and production (no IDENTITY_TEST_OTP) is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- CHANGELOG.md | 5 ++++ docs/BUG_HISTORY.md | 16 ++++++++++++ ...RESS-staging-otp-skip-password-20261001.md | 23 ++++++++++++++++ docs/tasks/README.md | 2 +- .../staging-otp-skip-password-20261001.md | 14 ++++++++++ frontend/src/components/email-otp-login.tsx | 15 +++++++++-- .../tests/identity-login-provider.test.ts | 26 +++++++++++++++++++ 7 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 docs/tasks/PROGRESS-staging-otp-skip-password-20261001.md create mode 100644 docs/testing/staging-otp-skip-password-20261001.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 709bfe8a..64a9ccf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-10-01 — staging 用固定验证码登录新邮箱,不再要求设置密码 + +- 测试环境(staging)选「验证码登录」,输入固定验证码后直接进入首页,不再停在「设置登录密码」(BUG-1152)。「注册账号」仍会设置密码。 +- 生产环境不变。Skill 版本不变;不改数据库结构。 + ## 2026-10-01 — 交付时盘型结论不再连说两遍 - 修复:答完最后一道题出交付卡时,「D1 上升…用了 N 道选择题」连出两条。上一版修好收尾话的保存后带出的重复,现在已说过就不再补写(BUG-1153)。Skill 版本不变;不改数据库结构。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 114ad780..1c3c4ee3 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -15498,3 +15498,19 @@ - 复发自:BUG-596(同一症状:交付连出两条;当时的 60 秒守卫只管无用户消息的二次运行,不管补写轮次)。 - 修复版本:`codex/rectification-post-adopt-verify-20261001`。 + +## BUG-1152 | staging 新邮箱用固定验证码登录后被要求设置密码,测试者以为必须用密码登录 + +- 状态:resolved(2026-10-01 Claude 直接执行;产品 10-01 选「测试通道跳过设置密码」) +- 首次发现 / 最近更新:2026-10-01 / 2026-10-01 +- 影响面:`frontend/src/components/email-otp-login.tsx::verifyOtp`(新增 `offersFirstPasswordStep`)。只在服务端配置了 `IDENTITY_TEST_OTP` 的环境(staging)生效;生产不配置,行为不变。 +- 用户现象:打开 staging 登录页只看到「验证码登录 / 密码登录」,没有任何固定验证码的提示;用新邮箱输入固定码后又被拦在「设置登录密码」,没有跳过入口。产品以为 staging 改成要密码才能登录。 +- 触发条件:staging 测试通道 + 「验证码登录」+ 该邮箱账户没有密码(新邮箱必然如此)。 +- 根因:`verifyOtp` 在 `hasPassword()` 为 false 时一律进入 `set-password` 步骤(07-27 `ab2944f7` 起),不区分测试通道;验证码通过时会话已经建立,这一步只是界面步骤。BUG-1121/1122(`3e0c615f`)把登录卡上常驻的固定码提示改成点「发送验证码」后 10 秒的浮层,进页面时看不到固定码,问题更显眼。固定码通道本身一直开着(线上 `/login` 带 `testOtp`,发码接口 200)。 +- 决策记录:产品 10-01 只选「测试通道验证码登录跳过设置密码」;不恢复常驻提示(BUG-1121/1122 的浮层设计不变)。「注册账号」模式仍设置密码;二步验证仍优先。 +- 修复:`offersFirstPasswordStep(testMode, mode)` 在测试通道 + `otp` 模式下为 false,`verifyOtp` 此时不查 `hasPassword`、直接 `enterApp()`(记录协议同意后进入首页)。 +- 验证:`frontend/tests/identity-login-provider.test.ts`「staging fixed-code sign-in skips the first-password step」:四种组合(测试/真实邮件 × 验证码登录/注册),以及二步验证分支仍在密码判断之前。修复前该函数不存在,测试红。 +- 防复发:测试通道的便利只能挂在 `testMode` 上,不得改服务端鉴权或让生产受影响。 +- 相关记录:BUG-1121、BUG-1122。 +- 复发自:无 +- 修复版本:`codex/staging-otp-skip-password-20261001`。 diff --git a/docs/tasks/PROGRESS-staging-otp-skip-password-20261001.md b/docs/tasks/PROGRESS-staging-otp-skip-password-20261001.md new file mode 100644 index 00000000..4b713b19 --- /dev/null +++ b/docs/tasks/PROGRESS-staging-otp-skip-password-20261001.md @@ -0,0 +1,23 @@ +# PROGRESS · staging 固定验证码登录跳过「设置密码」(2026-10-01) + +执行方:Claude(产品 10-01「请你执行」,直接执行模式)。任务书:`TASK-staging-otp-skip-password-20261001.md`。基线 `origin/staging` = `279650a6`。 + +| 任务 | 结果 | 证据 | +| --- | --- | --- | +| T1 跳过设置密码 | 完成 | `email-otp-login.tsx` 新增导出 `offersFirstPasswordStep(testMode, mode)`;`verifyOtp` 在其为 false 时不调用 `hasPassword()`,落到原有的 `await enterApp()`。选了「省掉调用」:测试通道下这次请求没有用处。二步验证分支位置不变;`enterApp` 调用次数仍为 5,原有计数断言不改 | +| T2 回归测试 | 完成(纯函数行为 + 源码顺序断言) | `identity-login-provider.test.ts` 新增 1 条:四种组合 + 二步验证在前 + `set-password` 仍由该判断守住。全组件渲染需要模拟 `selfHostedAuthActions`/`next/image`,收益不抵成本,按让步顺序取纯函数 | +| T3 记录 | 完成 | `docs/BUG_HISTORY.md` BUG-1152;`CHANGELOG.md`;`docs/testing/staging-otp-skip-password-20261001.md` | + +## 验收数字(Node 22.x,本机无 Docker) + +| 项 | 结果 | +| --- | --- | +| `tsc --noEmit` | 0 错 | +| `npm run lint` | 0 error(126 warning,与改动文件无关;改动文件单独 eslint 无输出) | +| `npm test` | 基线 4867 / pass 4802 / fail 24 → 改后 4868 / 4803 / 24;失败清单与基线逐条 `diff` 一致(全是需 Docker/PostgreSQL 的套件) | +| `npm run build` | 成功;`/` 仍 `○ Static`,`/login` 仍 `ƒ` | +| 首屏 gzip | 未测:改动只在 `/login` 组件内加 3 行判断,`/` 首屏不加载该组件 | + +## 环境缺口 + +- 真机走查(新邮箱 → 固定码 → 直接进首页)留给产品,见 `docs/testing/` 清单。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index c987cd16..e58d61dd 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -402,4 +402,4 @@ | `TASK-consult-plain-answer-20261001.md` | `PROGRESS-consult-plain-answer-20261001.md` | 普通对话「还是废话」:09-17 四步开场形状(格局名 → 谁推谁修 → 扮演哪个象)逼出谜语,问父母时爸妈被揉成一段;产品授权推翻该形状,改成先答 + 按问题里的对象分段 + 人话自检 + 空宫不单独下结论 + 父母卡标 mother/father(BUG-1132~1134) | **已实现,待 Claude 验收**(fork 子代理直接执行);未推 staging、未部署;模型对比为环境缺口,真机清单 `docs/testing/consult-plain-answer-20261001.md` | 分支 `codex/consult-plain-answer-20261001` | | `TASK-consult-answer-clock-20261001.md` | `PROGRESS-consult-answer-clock-20261001.md` | 普通对话答题时钟 70 s 容不下推理模型(v4-pro 77 s),无出生分钟路线只受 110 s 工具钟管;产品定 5 分钟防卡死(BUG-1142) | **已实现,待 Claude 验收**(直接执行) | 分支 `codex/consult-answer-clock-20261001` | | `TASK-consult-timeline-no-plan-20261001.md` | `PROGRESS-consult-timeline-no-plan-20261001.md` | 普通对话进度栏提前打勾:回答提纲在算盘前就作为四行步骤显示并全部打勾(BUG-1145);产品选 B 去掉提纲行,只显示真实步骤 | **实现完成,待 Claude 验收**(直接执行) | 分支 `codex/consult-timeline-no-plan-20261001` | -| `TASK-staging-otp-skip-password-20261001.md` | `PROGRESS-staging-otp-skip-password-20261001.md` | staging 新邮箱用固定验证码登录后被强制设置密码,测试者误以为要密码登录;产品选「测试通道验证码登录跳过设置密码」,生产不变(BUG-1152) | **待领取** | 分支 `codex/staging-otp-skip-password-20261001` | +| `TASK-staging-otp-skip-password-20261001.md` | `PROGRESS-staging-otp-skip-password-20261001.md` | staging 新邮箱用固定验证码登录后被强制设置密码,测试者误以为要密码登录;产品选「测试通道验证码登录跳过设置密码」,生产不变(BUG-1152) | **已实现推 staging**(Claude 直接执行),真机清单 `docs/testing/staging-otp-skip-password-20261001.md` | 分支 `codex/staging-otp-skip-password-20261001` | diff --git a/docs/testing/staging-otp-skip-password-20261001.md b/docs/testing/staging-otp-skip-password-20261001.md new file mode 100644 index 00000000..b0922047 --- /dev/null +++ b/docs/testing/staging-otp-skip-password-20261001.md @@ -0,0 +1,14 @@ +# staging 固定验证码登录跳过设置密码 · 真机清单(2026-10-01) + +前提:staging 已部署含 BUG-1152 的提交(`/api/health` 的 `deployment.gitCommit` 与之一致)。用一个从没在 staging 登录过的邮箱(可以虚构,不会真的发邮件)。 + +1. 打开 `https://staging.jyotisha.chat/login`,停在「验证码登录」,勾选协议,填新邮箱,点「发送验证码」。 + - 预期:顶部浮层写「测试环境:验证码固定为 xxxxxx,不会发送真实邮件。」 +2. 输入浮层里的验证码,提交。 + - 预期:直接进入首页;**不出现**「设置登录密码」。 +3. 退出登录,再用同一邮箱走一遍第 1–2 步。 + - 预期:同样直接进入首页。 +4. 退出登录,点「注册账号」,换另一个新邮箱,发送验证码并输入固定码。 + - 预期:出现「设置密码 / 确认密码」,这是有意保留的。 +5. 生产 `https://jyotisha.chat/login`(只看不登录): + - 预期:发送验证码后的提示是「验证码已发送至 …」,不是固定码提示。 diff --git a/frontend/src/components/email-otp-login.tsx b/frontend/src/components/email-otp-login.tsx index 874929f9..b84f4aa6 100644 --- a/frontend/src/components/email-otp-login.tsx +++ b/frontend/src/components/email-otp-login.tsx @@ -20,6 +20,15 @@ const AUTH_ERROR = "auth-error"; /** The staging fixed code stays up long enough to type it. */ const TEST_OTP_NOTICE_MS = 10_000; +/** + * Whether a code sign-in still asks for a first password. The staging fixed-code + * channel lets testers in with a fresh mailbox straight away (BUG-1152); choosing + * 注册账号 is asking for a password, so that path keeps the step. + */ +export function offersFirstPasswordStep(testMode: boolean, mode: AuthMode): boolean { + return !(testMode && mode === "otp"); +} + function clearAuthNotices() { dismissNotice(AUTH_NOTICE); dismissNotice(AUTH_ERROR); @@ -171,8 +180,10 @@ export function EmailOtpLogin({ notifyInfo("请输入验证器动态码,或使用一枚未使用的恢复码", { id: AUTH_NOTICE, duration: NOTICE_DURATION_MS.error }); return; } - const hasPassword = await selfHostedAuthActions.hasPassword(); - if (!hasPassword) { + if ( + offersFirstPasswordStep(testMode, mode) + && !(await selfHostedAuthActions.hasPassword()) + ) { setStep("set-password"); notifySuccess("邮箱验证成功,请设置登录密码", { id: AUTH_NOTICE }); return; diff --git a/frontend/tests/identity-login-provider.test.ts b/frontend/tests/identity-login-provider.test.ts index 745c3422..453c3c9e 100644 --- a/frontend/tests/identity-login-provider.test.ts +++ b/frontend/tests/identity-login-provider.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; +import { offersFirstPasswordStep } from "../src/components/email-otp-login.tsx"; import { createSelfHostedAuthActions } from "../src/modules/identity/client.ts"; test("login page routes the two self-hosted surfaces explicitly", () => { @@ -284,3 +285,28 @@ test("login UI preserves accessible OTP, password, registration, and reset input assert.match(route, /provider_id = 'credential'/); assert.doesNotMatch(route, /update\s+identity\.accounts/i); }); + +test("staging fixed-code sign-in skips the first-password step (BUG-1152)", () => { + // Fixed-code channel + 验证码登录: a fresh mailbox goes straight in. + assert.equal(offersFirstPasswordStep(true, "otp"), false); + // 注册账号 asks for a password, fixed code or not. + assert.equal(offersFirstPasswordStep(true, "register"), true); + // Real mail (production): unchanged. + assert.equal(offersFirstPasswordStep(false, "otp"), true); + assert.equal(offersFirstPasswordStep(false, "register"), true); + + const component = readFileSync( + new URL("../src/components/email-otp-login.tsx", import.meta.url), + "utf8", + ); + const verifyOtp = component.slice( + component.indexOf("async function verifyOtp"), + component.indexOf("async function signInWithPassword"), + ); + // Second factor still comes first; the password lookup is only made when the step is offered. + assert.ok(verifyOtp.indexOf("result.twoFactorRequired") < verifyOtp.indexOf("offersFirstPasswordStep")); + assert.match( + verifyOtp, + /offersFirstPasswordStep\(testMode, mode\)\s*&& !\(await selfHostedAuthActions\.hasPassword\(\)\)\s*\) \{\s*setStep\("set-password"\)/, + ); +});