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:
@@ -64,6 +64,30 @@ type AuditLog struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// AdminUser 管理控制台账号。
|
||||
// 密码以 bcrypt 哈希存储 (cost=10)。
|
||||
// - Role: "admin" = 全部权限 (含账号管理)
|
||||
// "operator" = 仅运维操作(实例/用户/备份等),无账号管理
|
||||
// - Status: "active" | "disabled"
|
||||
// - MustChangePassword: 首次 seed 的默认账号(密码 = admin123)需要首次登录后改密
|
||||
//
|
||||
// 安全:
|
||||
// * PasswordHash 字段在 db.json 中以 password_hash 持久化(必须!)
|
||||
// * 但 store.ListAdmins 在返回前会清空 PasswordHash,
|
||||
// 因此所有 API 响应里 hash 都是空字符串,绝不出网
|
||||
type AdminUser struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"password_hash"` // 注意:store.ListAdmins 返回时会清空
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
LastLoginIP string `json:"last_login_ip,omitempty"`
|
||||
MustChangePassword bool `json:"must_change_password"` // 强制改密标志
|
||||
}
|
||||
|
||||
// ConnectionLog 来自 OpenVPN status 的实时/历史连接记录。
|
||||
// 周期由 OpenVPN 自身写入 status.log,本服务周期性读取解析后入库。
|
||||
type ConnectionLog struct {
|
||||
|
||||
Reference in New Issue
Block a user