Files
crearte-monorepo/docs/plans/2026-09-30-admin-console.md

113 lines
9.7 KiB
Markdown
Raw Permalink 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.
# P7 管理后台增强 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** crearte-server 0.12.0(audit_log 表 + 审计中间件 + admin 路由收敛组 + 用户列表/角色端点 + 最后 admin 护栏)与 crearte 0.18.0(AdminUsersView / AdminAuditView / apiRepo 三方法 / e2e 扩流),端到端验证后合主。deploy 零改动。
**Architecture:** 见 spec `docs/specs/2026-09-30-admin-console-design.md`。核心决策:D-A 只记管理员动作;D-B 中间件统一记录 + **维护者定稿:admin 路由收敛为 gin 组(空前缀 `engine.Group("", RequireAdmin, AuditMiddleware)`),组内继续用既有 `RouteAdmin*` 全路径常量注册,禁止逐条手挂**;D-C 列表=身份字段+角色操作;D-D 唯一 admin 不可降级(service 层,CLI/API 双覆盖)。SetUserRole 现成语义:`UpdateRoleByEmail` SQL 自带 `token_version+1`(作废被改者全部 token),**不要**另加 `BumpTokenVersion` 调用。
**Tech Stack:** Go 1.24(gin 1.11 / pgx 5.8;**禁新增第三方依赖**)、Vue 3 + vue-router + vitest + Playwright(前端既有栈)、migrations 0010。
## Global Constraints
- 工作目录:`/root/.openclaw/workspace/coder/crearte-monorepo/crearte-server/src`(Go 源码根)与 `…/crearte/src`(前端,package.json 在 src/ 下)。
- 分支:两仓各从 `master` 切 `feat/admin-console`;**master 上禁直接提交**(各仓 AGENTS.md);合回一律 `--no-ff` 由控制者执行。
- **Go 命令一律走容器**(AGENTS.md 本地约定,proxy.golang.org 不可达):
`docker run --rm -v /root/.openclaw/workspace/coder/crearte-monorepo/crearte-server:/work -v crearte_gomod:/go/pkg/mod -v crearte_gocache:/gocache -e GOCACHE=/gocache -e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn -w /work/src golang:1.24-alpine sh -c "<cmd>"`(首次冷 1–3 分钟,background+poll)。
- 集成层:`docker compose --profile debug up -d db-test`(宿主 5434,fresh volume),`TEST_DATABASE_URL=postgres://crearte:***@localhost:5434/crearte?sslmode=disable go test -p 1 ./...`;守卫语义=不许出现 "TEST_DATABASE_URL not set" skip。跑完 `down` 清场(controller 负责最终清场;子代理用独占端口自查后自清)。
- 审计中间件行为红线:actor 仅在 RequireAdmin 放行后记录(401/403 未遂不记);写失败 log-only 绝不影响响应;status 含 4xx/5xx(失败的 admin 尝试同样入史)。
- 分页钳制逐字照抄 `ListQueue` 语义(limit≤0 或 >200→50;offset<0→0)。
- 前端:admin 面两 GET 不缓存(无 ETag 依赖);错误经 `toContentMessage`;降级确认弹层文案必须含「对方登录态立即失效」与「最后一个 admin 会被拒绝」两点。
- CHANGELOG:server 追加 `## [0.12.0] - 2026-09-30`(顶版 0.11.1)、crearte 追加 `## [0.18.0] - 2026-09-30`(顶版 0.17.0);英文行+中文行连续。
- 完成定义:Task 3/4 各自测试绿 + 控制者验收(spec §7 四条)+ Task 5 合主。
---
### Task 1: server — 仓储与服务层(迁移 0010 + audit/user repo + memory parity + 护栏)
**Files:**
- Create: `internal/repository/migrations/0010_audit_log.sql`(spec §3.1 原样)
- Create: `internal/repository/audit.go` + `audit_test.go`(单测走 sqlmock? 否——本仓无 sqlmock 先例;纯逻辑测 scan/组装即可,真库覆盖交给集成层)
- Modify: `internal/repository/user.go`(+ `List(ctx,limit,offset,q) ([]model.User,int,error)`、`CountAdmins`)
- Modify: `internal/repository/memory.go`(同名 parity:List 过滤/排序/total、CountAdmins、audit Insert/List)
- Modify: store 聚合处(`UserStore` 接口或等价,看现码)接线新方法
- Modify: `internal/service/auth.go`(`ErrLastAdmin`;`SetUserRole` 加护栏;新 `SetUserRoleByID`)
- Create: `internal/service/audit.go`(`RecordAdminAction` / `ListAudit`)+ 表驱动单测
- Modify: `internal/service/auth_test.go`(护栏矩阵)
- Modify: `docs/CHANGELOG.md`(0.12.0 条目,Added+护栏)
**Interfaces(Produces):**
- `repository.AuditRepository`、`PostgresUserRepository.List/CountAdmins`
- `service.SetUserRole(ctx, store, email, role)`(签名不变+新错误)、`service.SetUserRoleByID(ctx, store, id, role)`
- `service.ListAudit(ctx, store, limit, offset, routeFilter)`
- `model.AuditEntry{ID, ActorID, ActorEmail, Method, Route, Path string; Status int; CreatedAt time.Time}`
**Steps:**
- [ ] 读 spec §3.1–§3.3 与现有 `internal/repository/user.go`、`memory.go`、`service/auth.go` 全文
- [ ] 迁移 0010;repo 三新方法;memory parity
- [ ] service 护栏:唯一 admin 降→`ErrLastAdmin`(判序:role 合法→找用户→当前 role=admin 且目标 user 且 CountAdmins==1→拒);`SetUserRoleByID` 复用同判
- [ ] `audit.go`:Record(吞错 log)/ List(钳制+倒序+route 过滤+total)
- [ ] 单测:护栏矩阵(唯一 admin 降拒 / 双 admin 降一 OK / 升 OK / 非 admin 目标降 OK no-op 合法)、ListAudit 钳制、SetUserRoleByID 404
- [ ] 容器四连 RC=0:`gofmt -l`、`go vet ./...`、`go build ./...`、`go test ./...`(无 TEST_DATABASE_URL,集成自动 skip 属预期)
- [ ] commit 到 `feat/admin-console`(本地,不 push)
### Task 2: server — API 层(admin 组收敛 + AuditMiddleware + 三端点)
**Depends:** Task 1。
**Files:**
- Modify: `internal/api/router.go`(admin 路由全部收敛进组:`adminGroup := engine.Group("", RequireAdmin(deps.AuthService), AuditMiddleware(deps.AuditStore))`,组内逐条 `adminGroup.GET/POST/PUT/PATCH(RouteAdmin*, handler)`;URL 常量与 `FullPath()` 输出不得变化)
- Create: `internal/api/audit.go`(AuditMiddleware + 三 handler:ListUsers/SetRole/ListAudit,或按现码 admin handler 布放)
- Modify: `internal/api/admin.go` / deps 结构(AuditStore 注入)
- Modify: `cmd/serve.go`(store 注入链)
- Modify: `internal/api/integration_test.go` 或新 `admin_users_test.go`(真库腿)
- Modify: `docs/CHANGELOG.md`(追加端点条目)
**Endpoints(spec §3.4):**
- `GET /api/admin/users?limit&offset&q` → `{users:[{id,email,username,display_name,role,created_at}], total}`
- `PATCH /api/admin/users/:id/role` `{role}` → 200 用户体;400 非法 role;404 无此人;409 `last_admin`
- `GET /api/admin/audit?limit&offset&route` → `{entries:[...], total}` 倒序
**Steps:**
- [ ] 组收敛改造,跑既有 admin 测试证明 401/403 行为零变化
- [ ] AuditMiddleware:`c.Next()` 后取 `handler.SetUser` 存的 user + `c.FullPath()` + `c.Writer.Status()`;user 非 admin 则跳过(理论不可达,防御)
- [ ] 三端点 + 错误映射(复用 WriteError code 体系,新增 `last_admin`)
- [ ] 集成测试(TEST_DATABASE_URL 空库):列表 q/分页/total;PATCH 升角色后**旧 token 打 /api/me 必 401**;409;一次 approve 后 audit 首行=该动作;404 的 revoke 也入史(status=404);401/403 未遂不入史
- [ ] 容器四连 + 集成 `-p 1` RC=0;commit
### Task 3: 前端 — apiRepo + 两 view + 路由入口 + vitest
**Files(`crearte/src`):**
- Modify: `app/data/apiRepo.ts` + `app/data/types.ts`(listAdminUsers / setUserRole / listAudit;`AdminUser`、`AuditEntry` 类型)
- Create: `app/views/AdminUsersView.vue`、`app/views/AdminAuditView.vue`
- Create: 对应 `*.test.ts`
- Modify: router 注册(找现 AdminView 注册处仿写,admin 守卫一致);`app/views/AdminView.vue` 顶部加两入口链接
- Modify: `docs/CHANGELOG.md`(0.18.0)
**Steps:**
- [ ] 读 AdminView.vue / AdminSubmissionView.vue / apiRepo.ts 现有模式(StatePanel、分页、toContentMessage)
- [ ] apiRepo 三方法 + 类型 + 单测(请求形状、错误映射、不缓存)
- [ ] AdminUsersView:表列 用户名/邮箱/显示名/角色/注册时间;搜索防抖;分页 50;升降按钮 + 降级确认弹层(双要点文案)+ 409 原文回显
- [ ] AdminAuditView:倒序流水(时间本地化/操作者/动作标签映射+未知回退原文/对象 path 提取/状态码着色)+ route 过滤下拉
- [ ] vitest + typecheck RC=0;主套件 e2e 不受影响自查(夹具模式不依赖新后端端点的 spec 别动)
- [ ] commit 到 `feat/admin-console`(本地,不 push)
### Task 4: 前端 — e2e 扩流(admin-flow.spec)
**Depends:** Task 2 端点形状冻结 + Task 3。**写测试所需夹具**:e2e 走 `e2e-stack` 真栈则直接打真端点;若 admin-flow 是夹具模式则补夹具路由。**先读 spec/e2e/admin-flow.spec.ts 现状再定**,与既有用例同构。
- [ ] 流①:admin 进 /admin/users → 搜索用户 → 升 admin → 列表即时更新 → 降回 → 确认弹层出现
- [ ] 流②:执行一个管理动作 → /admin/audit 首行即该动作(时间/对象/结果码断言)
- [ ] e2e 主套件 + 若走真栈则 CREARTE_STACK_REQUIRED=1 下 full-loop 同步绿;commit
### Task 5: 控制者 — 本机验收六腿 + 合主
- [ ] 双路审查子代理(server / fe)对照 spec 逐条 PASS/FAIL
- [ ] 验收=spec §7 四条(含手工冒烟 CLI/API 护栏一致性、废 token 端到端)
- [ ] 合主:两仓 `--no-ff` 合 `feat/admin-console` → push → 删分支;wrapper:ROADMAP P7 状态、CHANGELOG 0.2.6、文档索引登记 spec+plan → push
- [ ] 台账 `.superpowers/sdd/progress.md`
---
**风险注记:** ① 组收敛若与 gin 版本路由注册交互有意外(如 FullPath 变组前缀),以「测试零改动通过」为准绳修正注册写法,不许改常量值。② `List` 返回 `[]model.User` 含 `password_hash`?看 `userColumns` 现码——若模型带 hash,序列化前必须裁(新 `AdminUserView` 投影结构体或 JSON tag 控制),集成测试断言响应体无 hash 字段。