feat(M1): 认证与用户管理 — 双通道登录、cookie 会话、用户 CRUD 与真实系统账号对接

认证:
- 图形验证码 GET /auth/captcha(内置 PNG 渲染,零第三方依赖)
- 外部用户 OTP 双通道:DB 存储(otp_codes)使邮件与 CLI 共用同一验证码/冷却/失败限速
- 管理员 bcrypt 登录 + 连续失败限速锁定;admin/forgot + admin/reset 邮件重置(SMTP 或日志)
- cookie 会话(HttpOnly/SameSite)、me/logout、admin/user 鉴权中间件

用户管理(admin):
- CRUD + disable/enable/extend/delete,对接 system 层真实 useradd/usermod/userdel/passwd
- system 层三执行模式:dry-run(默认,安全)/ direct(容器/测试用户)/ sudo(生产 sudoers 白名单)
- Exists 系统账号一致性检查;deploy/sudoers.example 白名单模板
- 关键操作接入 append-only 审计

其他:
- CLI user otp 改 DB store,与邮件通道真正对齐
- 容器镜像补 shadow(alpine 无 useradd);Makefile VERSION 0.2.0-m1
- 测试:auth/service 单测 + api httptest 集成 + 容器内真实系统账号端到端验证
This commit is contained in:
2026-08-29 23:40:20 +08:00
parent ae45aba607
commit 630d240dc0
32 changed files with 2923 additions and 188 deletions
+32
View File
@@ -141,11 +141,43 @@ type MailLog struct {
UpdatedAt time.Time `json:"updated_at"`
}
// OTPCode 外部用户 OTP 验证码(DB 存储,邮件与 CLI 双通道共享)。
//
// 每用户一行(username 唯一):邮件通道经 Send 生成,CLI 通道经 Current
// 读取同一验证码,保证"同一验证码、同一有效期、同一冷却与失败限速"(PLAN §2.2)。
// 验证码为 6 位数字,生命周期短(10 分钟)且一次性消费,存储明文以便 CLI
// 复用返回;并发写由单实例部署的串行事务保证(多实例需改 DB 行锁/Redis,PLAN §6)。
type OTPCode struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
Code string `gorm:"size:16;not null" json:"-"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
CooldownUntil time.Time `json:"cooldown_until"` // Send 冷却截止
Failures int `json:"failures"`
FailedAt *time.Time `json:"failed_at"` // 失败计数窗口起点(窗口内达阈值限速)
ConsumedAt *time.Time `json:"consumed_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PasswordResetToken 管理员密码重置令牌(邮件重置)。DB 只存哈希,
// 明文令牌仅经邮件/日志发送给管理员,一次有效。
type PasswordResetToken struct {
ID uint `gorm:"primaryKey" json:"id"`
AdminID uint `gorm:"index;not null" json:"admin_id"`
TokenHash string `gorm:"size:64;not null" json:"-"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
UsedAt *time.Time `json:"used_at"`
IP string `gorm:"size:64" json:"ip"`
CreatedAt time.Time `json:"created_at"`
}
// AllModels 供 AutoMigrate 使用的全部模型。
func AllModels() []any {
return []any{
&AdminUser{}, &User{}, &SSHKey{}, &Approval{},
&AuditLog{}, &Session{}, &Setting{}, &MailLog{},
&OTPCode{}, &PasswordResetToken{},
}
}