Add multi-admin account management

Schema:
- New AdminUser model with bcrypt-hashed password (cost 10)
- Roles: admin (full) / operator (read-only ops)
- Status: active / disabled
- MustChangePassword flag forces first-login password change

Backend:
- store: Add admins [] + CRUD methods (ListAdmins strips PasswordHash)
- service: SeedDefaultAdminIfEmpty (uses env credentials on first run),
  CreateAdmin, ChangePassword, ResetPassword, SetAdminStatus, DeleteAdmin
- middleware: JWT now carries user_id (UUID)
- api: login() uses bcrypt + updates last_login_at/ip, blocks disabled
- api: me() returns role + must_change_password
- api: new endpoints:
    POST /api/me/password       (self password change)
    GET  /api/admins
    POST /api/admins             (create)
    POST /api/admins/:id/password (reset by admin)
    POST /api/admins/:id/status   (enable/disable)
    DELETE /api/admins/:id        (with self/last-admin guard)

Frontend:
- Login: must_change_password=true triggers forced change-password dialog
- Layout: admin dropdown shows role tag + 修改密码 / 退出登录
- New /admins page (admin only) with table + create/reset/status/delete
- Router guard hides /admins from non-admin accounts
- API client: Auth.changePassword, Admins.{list,create,resetPassword,setStatus,delete}

Security:
- PasswordHash stored as bcrypt $2a$10$... in db.json
- ListAdmins always returns PasswordHash=''; never leaks via API
- Login returns 403 for disabled accounts

Verified: 21/21 API tests + browser E2E (first-login forced change,
restart persistence, admin list without hash, role-based menu)
This commit is contained in:
cnbugs
2026-08-09 21:45:41 +08:00
parent 4c8b7b5188
commit 09f6918aeb
12 changed files with 778 additions and 23 deletions
+78 -5
View File
@@ -48,13 +48,45 @@ GET /api/health
→ 200 {"ok": true}
```
### 当前登录用户
### 登录
```
GET /api/me
→ 200 {"username": "admin"}
POST /api/login
Content-Type: application/json
{"username": "admin", "password": "..."}
→ 200 {
"token": "eyJhbG...",
"username": "admin",
"role": "admin",
"must_change_password": false
}
→ 401 {"error": "用户名或密码错误"}
→ 403 {"error": "账号已被禁用"}
```
### 登出(仅前端清理 token,服务端无状态)
> 密码以 bcrypt 哈希存储在 `data/db.json` 中。
> `must_change_password=true` 表示首次登录(默认账号)需要立刻改密。
### 当前登录账号
```
GET /api/me
→ 200 {
"user_id": "uuid",
"username": "admin",
"role": "admin",
"must_change_password": false
}
```
### 修改自己密码
```
POST /api/me/password
{"old_password": "...", "new_password": "..."}
→ 200 {"ok": true}
→ 400 {"error": "原密码错误"} | {"error": "新密码至少 6 个字符"}
```
### 登出(前端清理 token)
```
POST /api/logout
→ 200 {"ok": true}
@@ -62,7 +94,48 @@ POST /api/logout
---
## 2. 仪表盘
## 2. 管理员账号管理
> 仅 admin role 可访问。operator 访问会拒绝(由前端路由守卫 + 后端 handler 校验)。
### 列出账号(不含 hash)
```
GET /api/admins
→ 200 [AdminUser, ...] // PasswordHash 字段固定为空
```
### 新建账号
```
POST /api/admins
{"username": "alice", "password": "StrongPass123", "role": "operator"}
→ 200 AdminUser
```
### 重置密码(无需知道旧密码)
```
POST /api/admins/:id/password
{"new_password": "NewPass456"}
→ 200 {"ok": true}
```
### 启用/禁用
```
POST /api/admins/:id/status
{"status": "active"} | {"status": "disabled"}
→ 200 {"ok": true}
```
### 删除(不能删自己、不能删最后一个 admin)
```
DELETE /api/admins/:id
→ 200 {"ok": true}
→ 400 {"error": "不能删除自己"}
→ 400 {"error": "不能删除最后一个 admin 账号"}
```
---
## 3. 仪表盘
### 汇总
```