Files
lingxi/docs/开发计划/灵犀_A1身份认证与用户管理_实施报告_V0.1.md
T
Stev_Wang 7824bc7b32
ci / 质量门禁 (push) Canceled after 0s
fix(dev): 明确启动不重置数据库并新增安全 db:init
背景:开发机每次重启后被反馈需要重新用默认密码登录并强改,排查确认仓库启动链路(dev.sh / pnpm dev / API / worker / db:seed)均不重置密码;只有 db:reset / migrate reset / 重建数据卷会恢复种子密码。A2 收尾时为干净截图执行过 db:reset,是最近一次重置的直接原因。

改动:
- 新增 pnpm db:init = prisma migrate deploy + 幂等 seed,作为首次/迁移后的安全初始化;不重置数据、不覆盖已有密码。
- README / dev.sh / AGENTS 命令契约 / SEED.md / A1·A2 实施报告明确:日常启动不需要 DB 命令,db:reset 仅用于清库。
- 实测:对已改密的 admin 执行 db:init 后 password_hash 与 must_change_password 均未变化。

验证:pnpm format:check / pnpm lint 通过;db:init 实际执行通过。
2026-11-03 13:37:34 +08:00

131 lines
9.8 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-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`、`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`、`POST/PATCH /departments`、`GET /roles`。
- 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 同源校验,部门两级联动、角色多选、超管二次确认。
- 样式:`--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 说明。
---
## 3 验证结果
| 门禁 | 命令 | 结果 |
| --- | --- | --- |
| 格式 | `pnpm format:check` | ✅ |
| Lint | `pnpm lint` | ✅ 0 warning |
| 类型 | `pnpm typecheck` | ✅ 10/10 tasks |
| 单测 | `pnpm test` | ✅ shared 95 / api 60 / web 55,共 210 |
| 集成 | `pnpm test:int` | ✅ 2 files / 7 tests(真实 PG + Redis) |
| OpenAPI | `pnpm openapi:export` | ✅ 新增 14 条 identity 路径,`apps/web/src/api/generated/openapi.d.ts` 已更新 |
| 浏览器 e2e | `A1_E2E=1 pnpm exec playwright test e2e/a1-identity.spec.ts` | ✅ 6/6(登录+强改、刷新旋转、登出、停用、角色变更、越权 403) |
联调截图:`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`,首次登录强制修改。
> 只有需要清空本地库时才执行 `pnpm db:reset`(会重建并恢复种子密码);`./dev.sh` / `pnpm dev` / `pnpm db:init` 不会重置已有账号密码。
---
## 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 之后应收紧为有客户时必填。 |
---
## 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 策略。