package service import ( "context" "errors" "strings" "time" "gorm.io/gorm" "ws_usernode/internal/config" "ws_usernode/internal/model" "ws_usernode/internal/pkg" "ws_usernode/internal/system" ) // 用户服务错误。 var ( ErrUserNotFound = errors.New("service: 用户不存在") ErrUserExists = errors.New("service: 用户名已存在") ErrUserExpired = errors.New("service: 用户已过期,请先延期") ErrUserDisabled = errors.New("service: 用户已禁用,无法操作") ErrSystemAccountMissing = errors.New("service: 系统账号不存在,无法操作") ) // UserService 外部用户生命周期服务:DB 记录 + system.Manager 系统账号操作。 type UserService struct { db *gorm.DB sys system.Manager cfg *config.Config } // NewUserService 创建用户服务。 func NewUserService(db *gorm.DB, sys system.Manager, cfg *config.Config) *UserService { return &UserService{db: db, sys: sys, cfg: cfg} } // GetByUsername 按用户名查询外部用户(含或不含 ext_ 前缀均可)。 func (s *UserService) GetByUsername(ctx context.Context, username string) (*model.User, error) { full := normalizeName(username, s.cfg.System.UserPrefix) var u model.User if err := s.db.WithContext(ctx).First(&u, "username = ?", full).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, err } return &u, nil } // GetByID 按 ID 查询外部用户。 func (s *UserService) GetByID(ctx context.Context, id uint) (*model.User, error) { var u model.User if err := s.db.WithContext(ctx).First(&u, "id = ?", id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, err } return &u, nil } // UserFilter 用户列表筛选条件。 type UserFilter struct { Status string // active / disabled / expired,空为全部 Supervisor string // 挂靠老师模糊匹配 Page int PageSize int } // List 分页查询用户(admin)。 func (s *UserService) List(ctx context.Context, f UserFilter) ([]model.User, int64, error) { q := s.db.WithContext(ctx).Model(&model.User{}) if f.Status != "" { q = q.Where("status = ?", f.Status) } if f.Supervisor != "" { q = q.Where("supervisor LIKE ?", "%"+f.Supervisor+"%") } var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } page, size := f.Page, f.PageSize if page < 1 { page = 1 } if size < 1 { size = 20 } if size > 100 { size = 100 } var users []model.User if err := q.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&users).Error; err != nil { return nil, 0, err } return users, total, nil } // Create 创建外部用户:DB 记录 + 系统账号(useradd + passwd -l)。 // username 不含前缀;ttl 为有效期时长,0 表示用配置默认(90 天)。 // 系统建号失败时回滚 DB 记录,保证两侧一致。 func (s *UserService) Create(ctx context.Context, username, email, supervisor, purpose string, ttl time.Duration, createdBy uint) (*model.User, error) { if err := pkg.ValidateUserName(username); err != nil { return nil, err } if err := pkg.ValidateEmail(email); err != nil { return nil, err } username = strings.TrimSpace(username) full := s.cfg.System.UserPrefix + username var count int64 if err := s.db.WithContext(ctx).Model(&model.User{}).Where("username = ?", full).Count(&count).Error; err != nil { return nil, err } if count > 0 { return nil, ErrUserExists } if ttl <= 0 { ttl = s.cfg.Policy.DefaultTTL } expireAt := time.Now().Add(ttl) u := &model.User{ Username: full, Email: email, Supervisor: supervisor, Purpose: purpose, Status: model.UserStatusActive, ExpireAt: &expireAt, Shell: s.cfg.System.Shell, CreatedBy: createdBy, } if err := s.db.WithContext(ctx).Create(u).Error; err != nil { return nil, err } // 系统账号创建(dry-run / 直接 / sudo),失败时回滚 DB 记录 if err := s.sys.CreateUser(ctx, system.Account{Username: full, Shell: s.cfg.System.Shell}); err != nil { _ = s.db.WithContext(ctx).Delete(u).Error return nil, err } return u, nil } // Update 更新外部用户信息(仅更新非 nil 字段;邮箱由管理员修改,用户不可自助改)。 func (s *UserService) Update(ctx context.Context, id uint, email, supervisor, purpose *string) (*model.User, error) { if _, err := s.GetByID(ctx, id); err != nil { return nil, err } updates := make(map[string]any) if email != nil { if err := pkg.ValidateEmail(*email); err != nil { return nil, err } updates["email"] = *email } if supervisor != nil { updates["supervisor"] = *supervisor } if purpose != nil { updates["purpose"] = *purpose } if len(updates) > 0 { if err := s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Updates(updates).Error; err != nil { return nil, err } } return s.GetByID(ctx, id) } // systemAccountOK 检查系统账号存在;dry-run 模式跳过检查(演练流程)。 func (s *UserService) systemAccountOK(ctx context.Context, username string) bool { if s.cfg.System.DryRun { return true } ok, err := s.sys.Exists(ctx, username) return err == nil && ok } // Disable 禁用用户:DB 置 disabled + 清空 authorized_keys(SSH 立即失效)。 func (s *UserService) Disable(ctx context.Context, id uint) error { u, err := s.GetByID(ctx, id) if err != nil { return err } if u.Status == model.UserStatusDisabled { return nil // 幂等 } if !s.systemAccountOK(ctx, u.Username) { return ErrSystemAccountMissing } if err := s.sys.SyncAuthorizedKeys(ctx, u.Username, nil); err != nil { return err } return s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Update("status", model.UserStatusDisabled).Error } // Enable 启用用户:DB 置 active + 按 DB 状态重写 authorized_keys。 // 已过期的用户需先延期(Extend)。 func (s *UserService) Enable(ctx context.Context, id uint) error { u, err := s.GetByID(ctx, id) if err != nil { return err } if u.Status == model.UserStatusActive { return nil // 幂等 } if u.ExpireAt != nil && time.Now().After(*u.ExpireAt) { return ErrUserExpired } if !s.systemAccountOK(ctx, u.Username) { return ErrSystemAccountMissing } // 恢复有效密钥(M1 阶段用户尚无密钥,M2 接入后按 DB 同步) keys, err := s.activeKeys(ctx, u.ID) if err != nil { return err } if err := s.sys.SyncAuthorizedKeys(ctx, u.Username, keys); err != nil { return err } return s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Update("status", model.UserStatusActive).Error } // Extend 延长有效期:重设 expire_at(days<=0 用配置默认 TTL)。 // 已过期用户在回收期内可经此恢复(PLAN §2.2),恢复后同步密钥。 func (s *UserService) Extend(ctx context.Context, id uint, days int) error { u, err := s.GetByID(ctx, id) if err != nil { return err } ttl := time.Duration(days) * 24 * time.Hour if days <= 0 { ttl = s.cfg.Policy.DefaultTTL } newExpire := time.Now().Add(ttl) updates := map[string]any{"expire_at": newExpire} if u.Status == model.UserStatusExpired { if !s.systemAccountOK(ctx, u.Username) { return ErrSystemAccountMissing } keys, err := s.activeKeys(ctx, u.ID) if err != nil { return err } if err := s.sys.SyncAuthorizedKeys(ctx, u.Username, keys); err != nil { return err } updates["status"] = model.UserStatusActive } return s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Updates(updates).Error } // Delete 删除并回收用户:删除系统账号(userdel -r)+ 家目录 + 密钥记录, // 保留审计。系统账号已不存在时仍完成 DB 清理。 func (s *UserService) Delete(ctx context.Context, id uint) error { u, err := s.GetByID(ctx, id) if err != nil { return err } if s.systemAccountOK(ctx, u.Username) { if err := s.sys.RemoveUser(ctx, u.Username); err != nil { return err } } return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("user_id = ?", u.ID).Delete(&model.SSHKey{}).Error; err != nil { return err } return tx.Delete(&model.User{}, "id = ?", u.ID).Error }) } // activeKeys 返回用户当前有效(active)密钥,供授权同步(M2 完善密钥管理)。 func (s *UserService) activeKeys(ctx context.Context, userID uint) ([]system.Key, error) { var rows []model.SSHKey if err := s.db.WithContext(ctx).Where("user_id = ? AND status = ?", userID, model.StatusActive).Find(&rows).Error; err != nil { return nil, err } keys := make([]system.Key, 0, len(rows)) for _, k := range rows { keys = append(keys, system.Key{Type: k.KeyType, PublicKey: k.PublicKey}) } return keys, nil } // normalizeName 补全系统账号前缀(如 ext_)。 func normalizeName(username, prefix string) string { name := strings.TrimSpace(username) if !strings.HasPrefix(name, prefix) { return prefix + name } return name }