package service import ( "encoding/json" "errors" "strings" "time" "juhe-factory/api/internal/model" "juhe-factory/api/internal/security" "github.com/google/uuid" "gorm.io/gorm" ) var ErrUnauthorized = errors.New("账号或密码错误") var ErrReauthFailed = errors.New("当前管理员密码错误") var ErrAdminPasswordConfirmation = errors.New("两次输入的新密码不一致") var ErrAdminPasswordLength = errors.New("新密码长度必须为 8~20 位") type Auth struct { db *gorm.DB passwords security.PasswordHasher tokens security.TokenService } type TokenPair struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresAt time.Time `json:"expires_at"` Admin map[string]any `json:"admin"` } func NewAuth(db *gorm.DB, passwords security.PasswordHasher, tokens security.TokenService) *Auth { return &Auth{db: db, passwords: passwords, tokens: tokens} } func (s *Auth) Bootstrap(username, password string) error { if strings.TrimSpace(password) == "" { return nil } username = strings.TrimSpace(username) var admin model.AdminUser err := s.db.Where("username = ? AND deleted_at IS NULL", username).First(&admin).Error if err == nil { if s.passwords.Verify(admin.PasswordHash, password) { return nil } hash, hashErr := s.passwords.Hash(password) if hashErr != nil { return hashErr } return s.db.Model(&admin).Update("password_hash", hash).Error } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } var count int64 if err := s.db.Model(&model.AdminUser{}).Count(&count).Error; err != nil || count > 0 { return err } hash, err := s.passwords.Hash(password) if err != nil { return err } return s.db.Create(&model.AdminUser{Username: username, PasswordHash: hash, Enabled: true}).Error } func (s *Auth) Login(username, password string) (*TokenPair, error) { var admin model.AdminUser if err := s.db.Where("username = ? AND deleted_at IS NULL", strings.TrimSpace(username)).First(&admin).Error; err != nil || !admin.Enabled || !s.passwords.Verify(admin.PasswordHash, password) { return nil, ErrUnauthorized } return s.issue(&admin, true) } func (s *Auth) Refresh(refreshToken string) (*TokenPair, error) { hash := security.HashToken(refreshToken) var record model.AdminRefreshToken if err := s.db.Where("token_hash = ? AND revoked_at IS NULL AND expires_at > ?", hash, time.Now()).First(&record).Error; err != nil { return nil, ErrUnauthorized } var admin model.AdminUser if err := s.db.First(&admin, "id = ? AND enabled = true", record.AdminID).Error; err != nil { return nil, ErrUnauthorized } now := time.Now() if err := s.db.Model(&record).Update("revoked_at", now).Error; err != nil { return nil, err } return s.issue(&admin, false) } func (s *Auth) Logout(refreshToken string) error { if refreshToken == "" { return nil } now := time.Now() return s.db.Model(&model.AdminRefreshToken{}).Where("token_hash = ? AND revoked_at IS NULL", security.HashToken(refreshToken)).Update("revoked_at", now).Error } func (s *Auth) Authenticate(raw string) (*model.AdminUser, error) { claims, err := s.tokens.ParseAccessToken(raw) if err != nil { return nil, ErrUnauthorized } id, err := uuid.Parse(claims.AdminID) if err != nil { return nil, ErrUnauthorized } var admin model.AdminUser if err := s.db.First(&admin, "id = ? AND enabled = true", id).Error; err != nil { return nil, ErrUnauthorized } return &admin, nil } func (s *Auth) ChangePassword(adminID uuid.UUID, currentPassword, newPassword, confirmPassword string) error { if newPassword != confirmPassword { return ErrAdminPasswordConfirmation } if len(newPassword) < 8 || len(newPassword) > 20 { return ErrAdminPasswordLength } var admin model.AdminUser if err := s.db.Where("id = ? AND enabled = true AND deleted_at IS NULL", adminID).First(&admin).Error; err != nil { return ErrUnauthorized } if !s.passwords.Verify(admin.PasswordHash, currentPassword) { return ErrReauthFailed } hash, err := s.passwords.Hash(newPassword) if err != nil { return err } now := time.Now() return s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&admin).Update("password_hash", hash).Error; err != nil { return err } return tx.Model(&model.AdminRefreshToken{}). Where("admin_id = ? AND revoked_at IS NULL", adminID). Update("revoked_at", now).Error }) } func (s *Auth) Audit(admin *model.AdminUser, action, resource, resourceID, reason, ip, traceID string, detail any) error { return WriteAudit(s.db, admin, action, resource, resourceID, reason, ip, traceID, detail) } func (s *Auth) issue(admin *model.AdminUser, updateLogin bool) (*TokenPair, error) { access, expiresAt, err := s.tokens.NewAccessToken(admin.ID.String(), admin.Username) if err != nil { return nil, err } refresh, hash, refreshExpires, err := s.tokens.NewRefreshToken() if err != nil { return nil, err } record := model.AdminRefreshToken{AdminID: admin.ID, TokenHash: hash, ExpiresAt: refreshExpires} if err := s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&record).Error; err != nil { return err } if updateLogin { return tx.Model(admin).Update("last_login_at", time.Now()).Error } return nil }); err != nil { return nil, err } return &TokenPair{AccessToken: access, RefreshToken: refresh, ExpiresAt: expiresAt, Admin: map[string]any{"id": admin.ID, "username": admin.Username}}, nil } func WriteAudit(db *gorm.DB, admin *model.AdminUser, action, resource, resourceID, reason, ip, traceID string, detail any) error { data, _ := json.Marshal(detail) log := model.AdminAuditLog{AdminID: &admin.ID, AdminUsername: admin.Username, Action: action, ResourceType: resource, ResourceID: resourceID, Reason: reason, Detail: string(data), IPAddress: ip, TraceID: traceID} return db.Create(&log).Error }