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

9.7 KiB
Raw Permalink Blame History

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 字段。