diff --git a/.task/feature-svc-login-recovery-flow.md b/.task/feature-svc-login-recovery-flow.md index 9a76a7c9..3d5ea194 100644 --- a/.task/feature-svc-login-recovery-flow.md +++ b/.task/feature-svc-login-recovery-flow.md @@ -7,7 +7,11 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 3fb8210f6495a78e70a67af3661bdfe9c00fb9de - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-login-recovery-flow(worktree);分支:feature/svc-login-recovery-flow -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:用户 2026-10-13 提交《登录流程方案》全文并指示「按此方案实施」—— + 该指示即方案人工批准与实施授权。终点 = Backend 事务补强与管理员签发接口、Frontend + 登录恢复接线、Dev 合同文档与 gitlink 组合、PR 合入(智能体 Review 与合入按 + policy/delivery.md 执行)。未授权:目标环境部署与 live 账户操作(验收矩阵中 + 浏览器 E2E、SSE 断流等遗留项另行任务)。 - 交付模式:manual(前序人工决策,智能体 Review/合入);其他阶段明确授权后可记为 authorized-continuous - 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入) - 自动推进授权:无;授权范围:无;授权来源:无;授权终点:无;停止条件:无(未授权) @@ -28,13 +32,25 @@ | ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 | |---|---|---|---|---|---|---| -| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 | +| L1 | 密码登录为默认入口;登录建会话绑定认证版本 | 符合;既有 login + 新增 auth_version(迁移 v28) | P0 | 竞态测试:重置后旧版本建会话被拒 | 迁移 v28 | 纳入本增量 | +| L2 | 管理员网页签发/状态/撤销一次性登录 Key | 需调整;原为直接重置密码,裁定改为签发 Key | P0 | 非管理员/受限会话 403;明文仅签发响应一次 | 后端 admin 路由 | 纳入本增量 | +| L3 | 兑换只输 Key;受限会话只开身份/设密/退出 | 符合;既有 redeem + allowlist,补 IP 退避 | P0 | 100 并发兑换单胜;业务/管理入口 403 | — | 纳入本增量 | +| L4 | 恢复设密免当前密码;完成回登录页 | 需调整;前端门去当前密码框,onDone 不再直放行 | P0 | reloginRequired=true → 登录页;刷新按 sessionScope 恢复门 | L1 | 纳入本增量 | +| L5 | 主动改密要求当前密码且单事务 | 需调整;原三语句非事务 | P0 | 缺/错 current 401;当前会话保留其他吊销 | — | 纳入本增量 | +| L6 | 补发/重置使旧 Key 与受限会话失效 | 需补齐;签发原不动已兑换会话 | P0 | 补发后旧会话 401、旧 Key 不可兑换 | — | 纳入本增量 | +| L7 | 登录页移除 ask_ 日常入口;管理员 UI 改签发 | 需调整(用户裁定) | P1 | 无 ask_ tab;用户管理为签发 Key 面板 | L2 | 纳入本增量 | +| L8 | 账户事件审计与密码变更同事务 | 需补齐;新增 account_events(v28) | P1 | issued/redeemed/password_set 等事件入库 | 迁移 v28 | 纳入本增量 | +| L9 | 浏览器兑换专用 E2E、SSE 断流、PG 双库 | 部分;PG 经 CI test-pg,E2E/SSE 遗留 | P2 | 合同文档遗留项清单 | CI | 后续处理 | ## 方案与计划 -- 方案引用:待填写(含对象版本) -- 实施计划引用:待填写(含增量与需求 ID) -- 测试计划引用:待填写(含需求 ID 与验收条件) +- 方案引用:用户 2026-10-13《登录流程方案》(会话全文)=需求与方案主记录; + 合同定稿 docs/svc-deploy/2026-10-13-login-recovery-contract.md(本任务产出,含三裁定与验收矩阵) +- 实施计划引用:方案第八节 A0–A5;本增量执行 A0(合同文档)+ A1(后端事务)+ + A2(管理员接口)+ A3(前端接线);A4 组合验证 = Dev make test + CI;A5 = PR 合入 +- 测试计划引用:Backend internal/store/login_keys_test.go(8 用例,openTestStore 双库)、 + internal/server/login_recovery_test.go(4 HTTP 用例);Frontend test/loginpage.test.ts + (兑换文案)、e2e/changepw-form.playwright.ts(门形态更新);GATE 以 PR HEAD CI 为准 ## 阶段评审 diff --git a/docs/svc-deploy/2026-10-13-login-recovery-contract.md b/docs/svc-deploy/2026-10-13-login-recovery-contract.md new file mode 100644 index 00000000..c72dd4a4 --- /dev/null +++ b/docs/svc-deploy/2026-10-13-login-recovery-contract.md @@ -0,0 +1,76 @@ +# 登录恢复流程合同(2026-10-13) + +状态:**已实施**(Dev 任务 `feature/svc-login-recovery-flow`;用户 2026-10-13「按此方案实施」授权)。 +上游方案:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md`(r2); +本文是该方案登录部分的合同定稿与三项裁定记录,U4/U5 规格后续以本文为准回写。 + +## 登录初衷(冻结) + +1. 日常使用**用户名 + 密码**登录。 +2. 忘记密码时,由**管理员签发绑定该用户的一次性登录 Key**(`alk_`)。 +3. 用户兑换 Key 后进入**受限设密会话**,必须设置新密码;恢复设密**不要求旧密码**。 +4. 个人设置中的**主动改密仍要求当前密码**。 +5. 受限会话只开放身份查询、设密、退出;完成前不能进入工作台或访问业务接口。 + +## 三项裁定(对 r2 的合同分歧,用户按新方案裁定) + +| 分歧 | r2 表述 | 裁定(本合同) | +|---|---|---| +| 登录页长期 API 密钥入口 | 保留并标注集成用途 | **移除日常登录入口**;程序化客户端直接带 Authorization 头。浏览器不再持久保存 `ask_` | +| 管理员网页恢复方式 | 直接设置新密码(CLI 保留) | 网页改为**签发一次性登录 Key**;旧 `POST /api/admin/users/{owner}/password` 接口兼容保留(调用盘点:仅前端 UsersPanel 使用,已下线) | +| must-change 首登会话 | full 会话 + 中间件动态拦截 | **登录即建 `set_password_only` 受限会话**,与恢复会话同形;`me.sessionScope` 统一驱动前端设密门 | + +## 接口合同 + +| 接口 | 状态 | 行为 | +|---|---|---| +| `POST /api/auth/login` | 已有(修订) | 密码验证绑定认证版本;must-change/未设密建受限会话;响应带 `sessionScope` | +| `POST /api/auth/login-key/redeem` | 已有 | 请求只有 `{key}`;身份由服务端绑定;按来源 IP 退避(10 次/30s) | +| `GET /api/me` | 已有 | 返回 `sessionScope`/`authVia`/`mustChangePassword`/`passwordSet` | +| `POST /api/auth/password` | 已有(修订) | 受限会话免当前密码(`CompleteSetPasswordChecked`,事务内复验);主动改密要求当前(单事务) | +| `POST /api/auth/logout` | 已有 | 注销当前会话 | +| `POST /api/admin/users/{owner}/login-keys` | **新增** | 管理员签发;明文仅响应一次;补发同事务撤销旧 Key 与受限会话 | +| `GET /api/admin/users/{owner}/login-keys` | **新增** | 状态/有效期列表,无明文 | +| `DELETE /api/admin/users/{owner}/login-keys/{id}` | **新增** | 撤销 Key 并作废该用户受限会话 | + +统一字段:`sessionScope`(`full`|`set_password_only`)、`mustChangePassword`、`passwordSet`、`reloginRequired`。 +恢复设密成功固定返回登录页(`reloginRequired=true`),用新密码重新登录。 + +## 事务与安全要求(对应方案第六节) + +1. 签发不改密码、不吊销正常会话、不动业务数据;补发使旧 Key 与其受限会话立即失效。 +2. 兑换与建会话原子提交;会话绑定签发时点的 `users.auth_version`。 +3. 设密事务内复验会话(存在/未过期/scope/版本),统一写密码、清标志、bump 版本、 + 撤 Key、删会话、写审计(`account_events`,v28 迁移)。 +4. 密码登录会话创建校验认证版本,堵「旧密码验证在途 → 恢复完成 → 旧请求建会话」。 +5. 主动改密单事务(校验当前密码 → 更新 → 吊销其他会话保留当前)。 +6. 每次密码写入 bump `auth_version` 并作废全部旧会话;管理员/CLI 重置同事务。 +7. 会话 cookie 在 TLS(直连或 `X-Forwarded-Proto: https`)下置 `Secure`。 +8. SSE 长连接:恢复完成后旧会话行已删除,新请求即失效;存量连接的主动断流 + 依赖后续会话失效推送(本合同未实现,遗留项见下)。 + +## 验收矩阵与现状 + +| 项 | 结果 | 依据 | +|---|---|---| +| 正常登录/错误凭据 | PASS(SQLite) | `login_recovery_test.go`、既有 e2e | +| 只输 Key 识别账户、不能指定身份 | PASS | `TestAdminLoginKeyRecoveryFlow` | +| 受限会话拒业务/管理员/签发入口 | PASS | 同上 + 中间件 allowlist | +| 100 并发兑换恰好一次成功 | PASS | `TestLoginKeyConcurrentRedeem` | +| 补发/撤销使旧 Key 与受限会话失效 | PASS | `TestIssueLoginKeyRevokesDerivedSessions`、`TestAdminLoginKeyReissueAndRevoke` | +| 改密事务复验(被撤销会话不能改密) | PASS | `TestCompleteSetPasswordRevalidatesSession` | +| 登录竞态(旧版本建会话被拒) | PASS | `TestLoginRaceBlockedByAuthVersion` | +| 主动改密缺/错当前密码拒绝 | PASS | `TestActiveChangePasswordRequiresCurrent` | +| must-change 首登受限会话 | PASS | `TestLoginMustChangeCreatesRestrictedSession` | +| PostgreSQL 双库 | GATE(CI test-pg job) | store 测试走 `openTestStore`,`AGENT_PG_TEST_DSN` 下自动切真库 | +| 浏览器 E2E(兑换→设密→重登) | NOT_RUN(本轮) | `e2e/changepw-form.playwright.ts` 已更新门形态;专用兑换 E2E 遗留 | +| SSE 存量连接主动断流 | NOT_RUN(遗留) | 见「遗留项」 | + +以上本地结果均为 SELF;GATE 以 PR HEAD 的 CI(含 test-pg)为准。 + +## 遗留项(不阻塞本合同) + +- 登录页专用兑换 E2E(login-key.playwright.ts,U5 计划文件)。 +- SSE 长连接的认证版本主动复验/断流通知。 +- Documents 主方案 r2 的 U4/U5 规格按本文回写(与 PR #87 合入后再推进,避免文档冲突)。 +- 兑换入口跨实例限流(当前进程内退避,与 `loginGuard` 同档)。