Files
lingxi/docs/开发计划/灵犀_A1身份认证与用户管理_实施报告_V0.1.md
T
Stev_Wang 3e384febf3
ci / 质量门禁 (push) Canceled after 0s
fix(tooling): 给 db:reset 增加显式确认,避免误清本地密码
背景:
- 本轮部门管理联调后执行了 pnpm db:reset 清理测试数据,清空本地库,
  导致用户手工改过的登录密码被重置回 lingxi2026;
- 之前只保证了 ./dev.sh / pnpm dev / pnpm db:init 不会重置数据,
  db:reset 本身仍可被脚本或助手无提示执行,问题没有真正兜住。

改动:
- 新增 scripts/db-reset.mjs:默认拒绝执行并打印影响;只有显式
  ALLOW_DB_RESET=1 才调用 prisma migrate reset --force;
- package.json 的 db:reset 指向该脚本;
- README / AGENTS §6 §11 / e2e README / A1 实施报告同步:
  日常迁移补种子只用 pnpm db:init,验证/联调中禁止 db:reset;
  e2e 会改写 actor 密码,需使用专用账号或独立测试库。

影响面:
- 确需清库时多一步显式确认;迁移、启动、单测、int 测试不受影响。

回滚方式:
- 回滚本提交即恢复 db:reset 直接执行 prisma migrate reset --force。

验证证据:
- pnpm db:reset 无 ALLOW_DB_RESET 时退出码 1,并打印拦截说明;
- pnpm format:check / lint 通过;node --check scripts/db-reset.mjs 通过。
2026-11-03 15:16:52 +08:00

145 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 灵犀 A1 身份认证与用户管理 · 实施报告 V0.1
| 项 | 内容 |
| --- | --- |
| 实施日期 | 2026-11-02 |
| 批次 | A1(身份认证与用户管理) |
| 状态 | 已完成,门禁通过;**未提交** |
| 依据 | 开发计划 V0.1 §6.2 / §7.1、技术架构 V0.4 §5.4.1 / §6.3、工程规范 01–10 |
| 视觉基线 | A0「方向 A · 云白蓝」+ 选定方案 B(主从分栏 + 分屏登录) |
---
## 1 范围与完成度
| 任务 | 内容 | 状态 |
| --- | --- | --- |
| A1-BE-01 | 部门 / 用户 CRUD、停用启用、角色绑定、客户转交占位 | ✅ |
| A1-BE-02 | Argon2id 登录、JWT access + refresh 旋转、jti 吊销、登出、失败锁定、密码策略、SSO 接口预留 | ✅(SSO 仅预留) |
| A1-BE-03 | Jwt → Permission → SpecialPermission → Scope 守卫链、`@Audit` 与请求上下文 | ✅(Scope 骨架) |
| A1-BE-04 | `/me` 用户 / 角色 / 权限 / 数据范围 / 可见客户;权限即时生效 | ✅(dataScope 仅 all / 本部门及下级,六类留 A2) |
| A1-FE-01 | 登录页、token 存储刷新、登出、过期跳转、路由回跳 | ✅ |
| A1-FE-02 | 用户列表(游标分页 / 筛选 / 排序)、新建 / 编辑、停用确认、部门筛选、角色多选、客户转交入口 | ✅ |
| A1-FE-03 | `/me` 缓存、`usePermission` / `<Can>`、路由与按钮级控制、403 页 | ✅ |
| A1-BE-05 | 部门管理补强:树(含停用)、新建 / 改名 / 负责人 / 排序、停用 / 启用、停用拦截 | ✅(2026-11-03 补强) |
| A1-FE-04 | 部门管理页 `/system/departments`:两级树 + 详情/编辑 + 停用启用确认 | ✅(2026-11-03 补强) |
| A1-QA-01 | 登录 / 刷新 / 登出 / 停用 / 角色变更 / 越权 6 类用例 | ✅(接口 int + 浏览器 e2e) |
口径确认(用户拍板):初始密码由管理员设定、首次登录强制修改;手机号列表掩码、详情全号仅 `user:manage` 可见;路由按开发计划附录 C 使用 `/system/users`(2026-11-03 路由规范调整后)。
---
## 2 交付物
### 2.1 契约(`packages/shared`)
- 新增 `src/schemas/identity.ts`(+ spec):`UserSummary`(`phoneMasked`,永不含全号)、`UserDetail`(`phone?` 仅授权时返回)、`UserListQuery`(`cursor/limit/keyword/department_id/role_id/status/sort/order`)、`CreateUserInput`、`UpdateUserInput`、`DisableUserInput`、`TransferCustomersInput`、`LoginInput`、`RefreshInput`、`LoginResponse = tokenPair + me`、`MeResponse`、`DepartmentNode`(`status / sortOrder / memberCount / directMemberCount`)、`CreateDepartmentInput`、`UpdateDepartmentInput`、`DepartmentTreeQuery`、`RoleSummary`。
- `permissions.ts` 新增 `USER_VIEW = 'user:view'`;保留 `USER_MANAGE`。
- 写 body camelCase、query snake_case;写接口 `.strict()`;错误码沿用 A0 统一表。
### 2.2 后端(`apps/api` + `prisma`)
- 依赖:`@nestjs/jwt@12.0.3`、`argon2@0.45.1`(计划 A1-BE-02 已批准,版本精确锁定)。
- 迁移:`prisma/migrations/20261102143519_a1_identity`(expand):`departments`、`users`、`roles`、`user_roles`、`role_permissions`、`refresh_tokens`。
- 种子(幂等):8 角色 + 角色权限映射、3 个一级部门 + 6 个小组、11 个用户(`admin` 超管 + 10 位示例成员),统一初始密码 `lingxi2026`、`must_change_password=true`。
- identity 模块(`apps/api/src/modules/identity`):
- `POST /auth/login|refresh|logout|change-password`、`GET /me`、`GET/POST /users`、`GET/PATCH /users/{id}`、`POST /users/{id}/disable|enable|transfer-customers`、`GET /departments/tree?include_disabled=`、`POST/PATCH /departments`、`POST /departments/{id}/disable|enable`、`GET /roles`。
- 部门:两级组织树;新建 / 改名 / 负责人 / 排序;只停用不删除;有在职成员、启用中的下级小组或下级在职成员时禁止停用(42201 带数量);启用时上级必须启用;负责人必须是启用用户。
- refresh token 旋转:jti + token_hash + family,旧 token 原子吊销;重放整族吊销。
- 登录失败 Redis 计数,达阈值锁定 15 分钟(42901)。
- 权限每请求从 DB 解析,角色变更下一次请求生效。
- 未修改初始密码时除白名单接口外统一 40903。
- 手机号:列表只 `phoneMasked`;详情仅调用者具 `user:manage` 时附带 `phone`。
- 安全与上下文(`apps/api/src/common/security`):`@Public`、`@RequirePermissions`、`@CurrentUser`、`@Audit`、AsyncLocalStorage 请求上下文;`@Audit` 先写 Pino 结构化日志(A5 落库预留)。
- env:`JWT_ACCESS_SECRET` / `JWT_REFRESH_SECRET`(≥32 位)/ `JWT_ACCESS_TTL_SECONDS=900` / `JWT_REFRESH_TTL_SECONDS=604800` / `AUTH_MAX_LOGIN_FAILURES=5` / `AUTH_LOCK_MINUTES=15`;`.env.example` 已登记,本地 `.env.local` 已配置。
### 2.3 前端(`apps/web`)
- 路由:`/system/users`、`/system/users/new`、`/system/users/:id`、`/403`(旧 `/users*` 已加重定向);`RequireAuth` + `RequirePermission`;未登录跳 `/login?redirect=`(含 `sanitizeRedirect` 防开放重定向);无权限进 403;停用账号回登录并给出停用说明。
- 权限:`usePermission` / `usePermissions` + `<Can>`,无权限按钮不渲染;`/me` 走 TanStack Query 缓存。
- 登录:方案 B 左右分屏(左 560 品牌 + 能力 + CSS 规格缩略,右表单卡);真实 `POST /auth/login`;错误 / loading / 锁定态;首次登录强改在登录页内完成,成功后清会话要求用新密码重登。
- 用户列表 / 详情:左 400 列表 + 右 760 详情(基本资料 / 角色权限 / 负责客户 / 登录与安全 / 操作记录);搜索、部门、角色、状态、排序、游标分页;停用 / 启用 / 客户转交 / 调整角色 / 超管授予均「对象卡 → 后果 → 勾选」二次确认;编辑资料弹窗;空 / 加载 / 错误三态;1024 / 1280 适配。
- 新建用户:单栏三段(账号信息 / 组织与角色 / 初始设置),Semi Form + 共享 Zod 同源校验,部门两级联动、角色多选、超管二次确认。
- 部门管理(补强):`/system/departments`,左两级组织树(人数 / 负责人 / 状态、显示已停用、`?id=` 选中)+ 右详情/内联编辑(名称、上级只读、负责人、排序、直属/含下级人数、下级小组);新建部门/小组、停用/启用二次确认;无 `user:manage` 只读;树/表单/接口封装均有单测。
- 样式:`--lx-*` token + Semi 组件 + `styles/users.css`;未引入其他组件库/图标库。
### 2.4 测试(`e2e`)
- `e2e/a1-identity.spec.ts`:6 条浏览器端到端用例(`A1_E2E=1` 门控,CI 无依赖时 skip),真实 API + 真实 PG/Redis;测试数据自建自清。
- `e2e/README.md`:运行前置、环境变量、CORS 说明。
### 2.5 A1 补强:部门管理(2026-11-03)
- **口径**:独立页面 `/system/departments`;只停用不删除;有在职成员 / 启用中的下级小组 / 下级在职成员时禁止停用;负责人/排序可改;上级关系创建后不可移动;权限复用 `user:view` / `user:manage`,不新增权限点。
- **后端**:`GET /departments/tree?include_disabled`、`POST/PATCH /departments`、`POST /departments/{id}/disable|enable`;无 DELETE;`@Audit` 覆盖写操作;成员统计只算在职。
- **前端**:管理中心新增「部门管理」;两级树 + 详情/编辑 + 新建/停用启用确认;停用前本地预判 + 服务端文案兜底。
- **测试**:shared 契约(节点字段/新建/更新 strict);api 单测 + int 覆盖两级/重名/负责人/三类停用拦截/启用联动/DELETE 404;web 单测覆盖树、约束、表单、接口封装;真实账号 + 真实 API 手工联调通过(新建部门/小组、改名、停用空小组、重名提示、截图 `70-departments.png`)。
---
## 3 验证结果
| 门禁 | 命令 | 结果 |
| --- | --- | --- |
| 格式 | `pnpm format:check` | ✅ |
| Lint | `pnpm lint` | ✅ 0 warning |
| 类型 | `pnpm typecheck` | ✅ 10/10 tasks |
| 单测 | `pnpm test` | ✅ shared 121 / api 90 / web 120,共 331 |
| 集成 | `pnpm test:int` | ✅ 4 files / 33 tests(真实 PG + Redis,含部门 6 例) |
| OpenAPI | `pnpm openapi:export` | ✅ identity 路径(含部门 5 条)导出,`apps/web/src/api/generated/openapi.d.ts` 已更新 |
| 浏览器 e2e | `A1_E2E=1 pnpm exec playwright test e2e/a1-identity.spec.ts` | ✅ 6/6(登录+强改、刷新旋转、登出、停用、角色变更、越权 403) |
| 部门管理联调 | 真实 API + 浏览器(jiang.lan) | ✅ 新建一级/小组、改名、停用空小组、重名 40903、树/详情/停用确认均正常 |
联调截图:`design/a1-ui-v1/shots/impl/`(登录、强改、用户列表、详情、角色 Tab、停用弹窗、命中真实接口);e2e 截图:`design/a1-ui-v1/shots/impl/e2e/`。
---
## 4 本地运行
```bash
pnpm check:dev # 确认 PG 5432 / Redis 6379
pnpm db:init # 首次/迁移后:migrate deploy + 幂等种子(不重置数据、不改已有密码)
pnpm dev # API :3000 + Web :5173(日常启动不需要任何 DB 命令)
# 浏览器打开 http://127.0.0.1:5173/login
```
种子账号:`admin`(超级管理员);示例成员 `chen.mo` / `lin.qing` / `zhou.an` / `zhao.yinuo` / `gao.cen` / `su.he` / `shen.zhiyuan` / `jiang.lan`;停用示例 `tang.jia` / `he.an`。初始密码统一 `lingxi2026`,首次登录强制修改。
> 只有需要清空本地库时才执行 `ALLOW_DB_RESET=1 pnpm db:reset`(默认被拦截;会重建并恢复种子密码)。`./dev.sh` / `pnpm dev` / `pnpm db:init` 不会重置已有账号密码;e2e 会改写 actor 密码,请使用专用账号/测试库。
---
## 5 已知差异与遗留
| 编号 | 项 | 说明 / 建议 |
| --- | --- | --- |
| 1 | 数据范围六类 | 当前 `/me` 只区分 `all` 与 `department_and_children`;六类范围与 `ScopeContext` 注入按计划由 A2 完成,`ScopeGuard` 骨架已就位。 |
| 2 | 客户转交 | `POST /users/{id}/transfer-customers` 现返回 42201「客户模块(A3)提供」;负责客户 Tab 暂为空态。A3 落地后替换。 |
| 3 | 审计落库 | `@Audit` 目前写结构化 Pino 日志;A5 建审计表后接入持久化与检索/导出。 |
| 4 | OpenAPI schema | 路径已导出,但请求/响应 schema 仍缺(Zod 入参无 DTO 类);建议后续用 shared Zod 生成 JSON Schema 并补 `@ApiBody/@ApiOkResponse`。前端当前直接用 shared 类型。 |
| 5 | 记住我 | 契约暂无 `rememberMe`,登录页保留交互;如需真正 7 天会话需扩展 refresh TTL 策略。 |
| 6 | SSO | 仅预留适配器接口,未实现(计划 M3-3)。 |
| 7 | 对比度 | 辅助文字 `#8F959E` 白底约 2.8:1 属 A0 全局 token 问题,本轮未改;建议作为一次全局 token 调整统一处理。 |
| 8 | CORS / e2e | API `WEB_ORIGIN` 仅允许 `http://127.0.0.1:5173`;e2e 跑在 dev 5173。若改用 preview 4173,需把该 origin 加进 `WEB_ORIGIN`。 |
| 9 | 本地账号状态 | 联调过程中曾修改本地 admin/jiang.lan 口令并触发锁定;交付前已 `pnpm db:reset` 恢复种子(`lingxi2026`)。 |
| 10 | 客户转交必填 | PRD 要求离职停用前必须转交名下客户;当前因 A3 未落地,停用弹窗的转交为可选项,A3 之后应收紧为有客户时必填。 |
| 11 | 部门管理 e2e | 部门管理暂无独立浏览器 e2e,由 api int(6 例)+ web 单测 + 真实账号手工联调覆盖;后续补 e2e 时建议覆盖「有成员禁止停用」与「停用后不可再选」。 |
| 12 | 部门删除 | 按口径不提供删除,仅停用;如后续确需删除空部门,需先补 PRD/接口口径。 |
---
## 6 回滚方式
- 代码:整体回滚本批 commit(未提交前可直接 `git restore` 上述路径)。
- 数据库:本批为 expand 迁移,回滚代码不需要删表;如需彻底回退,用 `prisma migrate resolve` 标记后手工 drop 新增表(仅本地)。
- 配置:删除 `.env.local` 中新增的 JWT / 认证变量,API 启动会因校验失败而阻止启动(有意的 fail-fast)。
---
## 7 下一步
1. **A2 角色权限与数据范围**:角色 CRUD、权限点树、六类数据范围、SoD 校验、`ScopeContext` 全链路注入(当前 identity 已留接口与守卫骨架)。
2. **A3 客户与行业目录**:替换客户转交占位、负责客户 Tab 接真实数据、停用前转交必填。
3. **A5 审计**:`@Audit` 落库、检索与导出,A1 e2e 增加审计断言。
4. **工程补强**:OpenAPI schema 由 shared Zod 生成;全局对比度 token 决策;`rememberMe` 与 refresh TTL 策略。