Files
usernode/internal/service/admin.go
T
cao.wangrenbo 630d240dc0 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 集成 + 容器内真实系统账号端到端验证
2026-08-29 23:40:20 +08:00

154 lines
4.4 KiB
Go
Raw 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.
// Package service 承载业务逻辑,隔离 handler 与数据层/系统层。
package service
import (
"context"
"errors"
"strings"
"gorm.io/gorm"
"ws_usernode/internal/auth"
"ws_usernode/internal/model"
"ws_usernode/internal/pkg"
)
// 管理员服务错误。
var (
ErrAdminExists = errors.New("service: 管理员已存在")
ErrAdminNotFound = errors.New("service: 管理员不存在")
ErrWeakPassword = errors.New("service: 密码过弱(至少 8 位,需含字母与数字)")
ErrBadCredentials = errors.New("service: 用户名或密码错误")
)
// AdminService 管理端账号服务(CLI admin create / reset-password 与登录共用)。
type AdminService struct {
db *gorm.DB
}
// NewAdminService 创建管理员服务。
func NewAdminService(db *gorm.DB) *AdminService {
return &AdminService{db: db}
}
// Create 创建初始管理员。password 为空时由调用方提示交互输入。
func (s *AdminService) Create(ctx context.Context, username, password, email string) (*model.AdminUser, error) {
if err := validatePassword(password); err != nil {
return nil, err
}
if err := pkg.ValidateEmail(email); err != nil {
return nil, err
}
username = strings.TrimSpace(username)
var count int64
if err := s.db.WithContext(ctx).Model(&model.AdminUser{}).Where("username = ?", username).Count(&count).Error; err != nil {
return nil, err
}
if count > 0 {
return nil, ErrAdminExists
}
hash, err := auth.HashPassword(password)
if err != nil {
return nil, err
}
adm := &model.AdminUser{
Username: username,
PasswordHash: hash,
Email: email,
Role: "admin",
Status: model.StatusActive,
}
if err := s.db.WithContext(ctx).Create(adm).Error; err != nil {
return nil, err
}
return adm, nil
}
// ResetPassword 重置管理员密码(CLI 或邮件重置兜底)。
func (s *AdminService) ResetPassword(ctx context.Context, username, newPassword string) error {
if err := validatePassword(newPassword); err != nil {
return err
}
var adm model.AdminUser
if err := s.db.WithContext(ctx).First(&adm, "username = ?", username).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrAdminNotFound
}
return err
}
hash, err := auth.HashPassword(newPassword)
if err != nil {
return err
}
return s.db.WithContext(ctx).Model(&adm).Update("password_hash", hash).Error
}
// GetByUsername 按用户名查询管理员。
func (s *AdminService) GetByUsername(ctx context.Context, username string) (*model.AdminUser, error) {
var adm model.AdminUser
if err := s.db.WithContext(ctx).First(&adm, "username = ?", username).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAdminNotFound
}
return nil, err
}
return &adm, nil
}
// GetByID 按 ID 查询管理员。
func (s *AdminService) GetByID(ctx context.Context, id uint) (*model.AdminUser, error) {
var adm model.AdminUser
if err := s.db.WithContext(ctx).First(&adm, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAdminNotFound
}
return nil, err
}
return &adm, nil
}
// GetByEmail 按邮箱查询管理员(用于密码重置邮件)。
func (s *AdminService) GetByEmail(ctx context.Context, email string) (*model.AdminUser, error) {
var adm model.AdminUser
if err := s.db.WithContext(ctx).First(&adm, "email = ?", email).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAdminNotFound
}
return nil, err
}
return &adm, nil
}
// Login 校验管理员用户名与口令(bcrypt)。失败统一返回 ErrBadCredentials
// 不暴露账号是否存在。登录限速由 AuthService 的 RateLimiter 处理。
func (s *AdminService) Login(ctx context.Context, username, password string) (*model.AdminUser, error) {
adm, err := s.GetByUsername(ctx, strings.TrimSpace(username))
if err != nil {
return nil, ErrBadCredentials
}
if !auth.VerifyPassword(adm.PasswordHash, password) {
return nil, ErrBadCredentials
}
return adm, nil
}
// validatePassword 校验管理员密码强度(骨架阶段基础规则,M1 可加策略)。
func validatePassword(p string) error {
if len(p) < 8 {
return ErrWeakPassword
}
hasLetter, hasDigit := false, false
for _, r := range p {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' {
hasLetter = true
}
if r >= '0' && r <= '9' {
hasDigit = true
}
}
if !hasLetter || !hasDigit {
return ErrWeakPassword
}
return nil
}