130 lines
3.4 KiB
Go
130 lines
3.4 KiB
Go
package service
|
||
|
||
import (
|
||
"regexp"
|
||
"time"
|
||
|
||
"metazone.cc/mce/internal/common"
|
||
"metazone.cc/mce/internal/model"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
var pwLower = regexp.MustCompile(`[a-z]`)
|
||
var pwUpper = regexp.MustCompile(`[A-Z]`)
|
||
var pwDigit = regexp.MustCompile(`\d`)
|
||
|
||
// dummyHash 防时序攻击:当用户不存在时,仍对虚拟哈希执行完整的 bcrypt 比对
|
||
var dummyHash []byte
|
||
|
||
func init() {
|
||
hash, err := bcrypt.GenerateFromPassword([]byte("metazone-dummy-hash-2026"), 12)
|
||
if err != nil {
|
||
panic("failed to generate bcrypt dummy hash: " + err.Error())
|
||
}
|
||
dummyHash = hash
|
||
}
|
||
|
||
// validatePassword 密码强度:至少 8 位 + 包含大写字母 + 包含小写字母 + 包含数字
|
||
func validatePassword(pw string) error {
|
||
if len(pw) < 8 || !pwLower.MatchString(pw) || !pwUpper.MatchString(pw) || !pwDigit.MatchString(pw) {
|
||
return common.ErrWeakPassword
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ChangePassword 修改密码
|
||
// 流程:验证当前密码 → 校验新密码强度 → 哈希 → 更新 → 删除所有 session(强制重新登录)
|
||
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {
|
||
user, err := s.userRepo.FindByID(userID)
|
||
if err != nil {
|
||
return common.ErrUserNotFound
|
||
}
|
||
|
||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)) != nil {
|
||
return common.ErrIncorrectPassword
|
||
}
|
||
|
||
if err := validatePassword(newPassword); err != nil {
|
||
return err
|
||
}
|
||
|
||
hash, err := common.HashPassword(newPassword, s.cfg.Bcrypt.Cost)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
user.PasswordHash = hash
|
||
if err := s.userRepo.Update(user); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 删除所有 session,强制所有设备重新登录
|
||
return s.invalidateSessions(userID)
|
||
}
|
||
|
||
// DeleteAccount 用户自主注销
|
||
func (s *AuthService) DeleteAccount(userID uint, password, reason string) error {
|
||
user, err := s.userRepo.FindByID(userID)
|
||
if err != nil {
|
||
return common.ErrUserNotFound
|
||
}
|
||
|
||
if user.Role == model.RoleOwner {
|
||
return common.ErrOwnerCannotDelete
|
||
}
|
||
|
||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) != nil {
|
||
return common.ErrIncorrectPassword
|
||
}
|
||
|
||
user.Status = model.StatusDeleted
|
||
user.DeleteReason = reason
|
||
|
||
if err := s.userRepo.Update(user); err != nil {
|
||
return err
|
||
}
|
||
|
||
return s.invalidateSessions(userID)
|
||
}
|
||
|
||
// invalidateSessions 销毁某用户的所有 session(内部方法)
|
||
func (s *AuthService) invalidateSessions(userID uint) error {
|
||
return s.sessionManager.DestroyByUID(userID)
|
||
}
|
||
|
||
// InvalidateSessions 公开方法:销毁用户所有 session(供 admin 等外部调用)
|
||
func (s *AuthService) InvalidateSessions(userID uint) error {
|
||
return s.sessionManager.DestroyByUID(userID)
|
||
}
|
||
|
||
// ConfirmRestore 二次确认恢复已注销账号
|
||
func (s *AuthService) ConfirmRestore(req model.LoginRequest, loginIP string) (*model.User, error) {
|
||
user, err := s.userRepo.FindByEmail(req.Email)
|
||
if err != nil {
|
||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||
return nil, common.ErrInvalidCred
|
||
}
|
||
|
||
if user.Status != model.StatusDeleted {
|
||
return nil, common.ErrUserNotFound
|
||
}
|
||
|
||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) != nil {
|
||
return nil, common.ErrInvalidCred
|
||
}
|
||
|
||
// 恢复账号
|
||
user.Status = model.StatusActive
|
||
user.DeletedAt = gorm.DeletedAt{}
|
||
now := time.Now()
|
||
user.LastLoginIP = loginIP
|
||
user.LastLoginAt = &now
|
||
if err := s.userRepo.Update(user); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return user, nil
|
||
}
|