liuyun 802851a69f chore(openspec): track close-phase-0 implementation
sysmgr-nest 收口:RBAC 权限落地 + 审计/登录日志 + seed 脚本。
openspec change close-phase-0 任务全部完成,待归档。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-10 23:20:02 +08:00

88 lines
6.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.

## Context
Phase 0 后端已具备:Prisma 6 + MySQL(14 张 `sys_` 表)、登录/刷新/登出(JWT access 2h + refresh 7d 旋转)、多租户/软删/data_scope 过滤(Prisma `$extends` + AsyncLocalStorage 上下文)、10 个域 CRUD、pino 日志、Swagger。`@RequirePermissions` / `@Public` 装饰器与 `PermissionsGuard` 已定义但未接线;`sys_audit_log` / `sys_login_log` 表已存在但无写入点;无 seed 脚本。动机见 proposal.md。
关键现状约束:
- 权限/角色/资源/组织/租户用 `code` 关联,用户用 `id`(见 shared/data-model)。
- `PrismaService` 对 create 不注入 `tenant_code`(避免 `$extends` 误注入 where),故写审计/登录日志需显式传 `tenantCode`。
- 无 Redis,仅 MySQL;权限集载入方案受此约束。
## Goals / Non-Goals
**Goals:**
- 三级鉴权落地:`@Public` / 无标记(仅登录)/ `@RequirePermissions`(登录+权限码)。
- 权限码数据驱动:登录时从 role_resource 聚合 `type ∈ {button, api}` 的 `resource.code`。
- `super_admin` 角色绕过权限码校验、data_scope 看全量。
- 审计日志、登录日志自动落库(异步、不阻塞业务)。
- 幂等 seed 脚本,一键初始化完整可登录的租户。
**Non-Goals:**
- 不做权限变更的即时生效(如每次请求查库、强制下线)。权限变更在下次登录/刷新后生效。
- 不做「在线会话管理 / 踢人下线」的 UI(SESSION 域后置)。
- 不改 DB schema、不改 shared 三层主 spec、不动前端。
## Decisions
### D1. 权限集与超管标志存 Redis(而非 JWT / 每请求查库)
登录成功时聚合权限码(`user_role → role_resource → resource`,取 `type ∈ {button, api}` 的 code)与 `super` 标志,写入 Redis;`PermissionsGuard` 每请求从 Redis 读取做集合判断。
- **key**:`{REDIS_KEY_PREFIX}perm:{tenantCode}:{userId}`;**value**:`{ super: boolean, permissions: string[] }`
- **TTL** 随会话(默认 7d,与 refresh 一致),刷新时续期,登出时删除。
- **备选**:JWT payload 承载(token 随资源数线性膨胀、权限变更延迟 ≤2h);每请求查库聚合(无缓存层、每请求多次 JOIN)。均放弃。
- **取舍**:引入 Redis 依赖,换来 token 最小、权限实时、多实例共享一致。
### D2. 权限实时失效(授权变更删缓存)
修改 `role_resource` / `user_role`(授权变更)后,删除该租户全部用户的权限缓存(SCAN `{prefix}perm:{tenantCode}:*` 后 DEL);下一次请求守卫重新聚合并回填。
- **简化**:租户级失效,不精确到受影响用户;脚手架规模可接受。
- 移除某人的 `super_admin` 角色同样经此路径即时生效(无需等 token 过期)。
### D3. 权限码语义:一个 code = 一个权限点,按钮与接口分用
`resource.code` 全局唯一,细粒度 `domain:action`。约定:
- **api 资源**:每个受控接口一个,code 即接口权限码(如 `user:create`),`@RequirePermissions` 引用它。
- **button 资源**:仅用于「纯前端按钮」(无独立受控接口的 UI 动作,如 `user:export`)。前端按钮若触发受控接口,直接复用该接口的 code 判定可见性,不重复登记 button 资源。
- **menu 资源**:导航菜单,code 不进入接口权限集。
- 避免「创建用户」按钮与「POST /users」接口争抢同一 code 的唯一键冲突。
### D4. 审计写入:`@AuditLog(module, action)` 装饰器 + 拦截器,异步快照
装饰器 `SetMetadata` 记录 `module`/`action`;拦截器在 `next.handle()` 的 `tap` 中:
- 请求进入时快照 `tenantCode`/`userId`/`username`/`ip`/`userAgent`/`method`/`path`/脱敏后的 `body` 与起始时间(避免异步阶段 AsyncLocalStorage 失效丢租户上下文);
- 完成后异步 fire-and-forget 写 `sys_audit_log`;失败仅记告警日志,不影响响应。
- 入参复用 BodyLoggingInterceptor 的递归脱敏(password/token/secret 等 → `***`)。
- 成功 `status=success`,异常 `status=fail` + `error_message`。
### D5. 登录日志写入:auth.service 内联记录
登录成功/失败在 `auth.service.login` 的对应分支直接写 `sys_login_log`(成功 status=success;失败 status=fail + message 原因:密码错误/账号停用/锁定/租户不存在)。同步写失败仅告警,不改变登录结果。
### D6. seed 数据建模与幂等
`prisma/seed.ts` 用裸 `PrismaClient`(不经 `$extends`),全部按唯一键 upsert,可重复执行:
- 租户 `touyun`;根组织 `touyun`。
- 角色 `super_admin`(`data_scope=all`)、可选 `admin`(`data_scope=all`)。
- 用户 `touyun`(`bcryptjs` 哈希 `Touyun@123`),绑定 `super_admin`。
- 资源树:菜单(工作台 + 各域导航)、api(每个受控接口一个,code=`domain:action`)、button(纯前端按钮)。
- `role_resource`:`super_admin` 关联全部资源 code。
- 密码哈希用 `bcryptjs`,与运行时代码同库。
### D7. 权限码域清单(domain:action)
`user` / `role` / `resource` / `org` / `dict` / `config` / `tenant` / `audit` / `login-log`;动作 `list`/`create`/`update`/`delete`/`reset-pwd`/`assign`/`assign-resource` 等,按各域实际接口对齐。
### D8. Redis 接入方式
客户端用 `ioredis`;连接与 key 前缀均来自 env:`REDIS_HOST`/`REDIS_PORT`/`REDIS_DB`/`REDIS_PASSWORD`(默认 127.0.0.1:6379)、`REDIS_KEY_PREFIX`(默认 `sysmgr:`)。多 fork 项目共用一个 Redis 实例时按前缀隔离,互不干扰。
## Risks / Trade-offs
- [新增 Redis 依赖] → Redis 已在 Docker 常驻,key 前缀隔离多项目;若某 fork 项目无 Redis,需自行提供回退(点查)能力,属后续可选。
- [权限缓存与 DB 短暂不一致(删 key 窗口)] → 删 key 后下一次请求即回填,窗口毫秒级。
- [异步审计可能丢日志(D4)] → 进程崩溃窗口极小;失败告警日志兜底,审计定位问题足够。
- [审计 params 含敏感数据] → 复用脱敏,落库前过滤 password/token/secret 等。
- [seed 覆盖既有同名数据] → upsert 语义「更新已有、补齐缺失」;生产慎跑,仅初始化用。
- [接口漏挂 `@RequirePermissions` 导致裸奔] → 默认仅登录(宽松),收口只覆盖核心管理域;后续可升级为「默认拒绝」需全量盘点。
## Migration Plan
- 无 DB schema 变更;表已存在。
- 部署步骤:① 合并代码 ② 配置 `REDIS_HOST` / `REDIS_KEY_PREFIX` 等 env(指向常驻 Redis)③ `pnpm prisma:seed`(幂等,可重复)④ 重启服务。
- 回滚:`git revert`;seed 数据无需回滚(幂等);Redis 缓存可整体清空(登录时自动回填)。
## Open Questions
- 权限变更后是否需强制失效既有 access token(属 SESSION 域「在线会话/踢人」,后置)。