- repo.Create 错误包装为 CREATE_USER_FAILED - FindByEmail 非 RecordNotFound 错误包装为 QUERY_USER_FAILED - 修复无效 UUID tenant 导致 gorm 原生错误泄露为 500 的问题
181 lines
4.7 KiB
Go
181 lines
4.7 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"mengstack/internal/kernel/errors"
|
|
"mengstack/internal/kernel/tenant"
|
|
"mengstack/internal/modules/auth/domain"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Service struct {
|
|
repo domain.UserRepository
|
|
jwtCfg JWTConfig
|
|
}
|
|
|
|
type JWTConfig struct {
|
|
Secret string
|
|
AccessExpiryMinutes int
|
|
RefreshExpiryDays int
|
|
Issuer string
|
|
}
|
|
|
|
func NewService(repo domain.UserRepository, jwtCfg JWTConfig) *Service {
|
|
return &Service{repo: repo, jwtCfg: jwtCfg}
|
|
}
|
|
|
|
func (s *Service) Register(ctx context.Context, req domain.RegisterRequest) (domain.TokenPair, error) {
|
|
tenantID, ok := tenant.FromContext(ctx)
|
|
if !ok {
|
|
return domain.TokenPair{}, errors.ErrTenantRequired
|
|
}
|
|
|
|
if _, err := s.repo.FindByEmail(ctx, tenantID, req.Email); err == nil {
|
|
return domain.TokenPair{}, errors.ErrUserExists
|
|
} else if err != gorm.ErrRecordNotFound {
|
|
return domain.TokenPair{}, errors.Wrap(err, "QUERY_USER_FAILED", "failed to check existing user", 500)
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
|
|
if err != nil {
|
|
return domain.TokenPair{}, errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500)
|
|
}
|
|
|
|
user := &domain.User{
|
|
TenantID: tenantID,
|
|
Username: req.Username,
|
|
Email: req.Email,
|
|
Password: string(hash),
|
|
Nickname: req.Nickname,
|
|
Status: 1,
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, user); err != nil {
|
|
return domain.TokenPair{}, errors.Wrap(err, "CREATE_USER_FAILED", "failed to create user", 500)
|
|
}
|
|
|
|
return s.generateTokens(user)
|
|
}
|
|
|
|
func (s *Service) Login(ctx context.Context, req domain.LoginRequest) (domain.TokenPair, error) {
|
|
tenantID, ok := tenant.FromContext(ctx)
|
|
if !ok {
|
|
return domain.TokenPair{}, errors.ErrTenantRequired
|
|
}
|
|
|
|
user, err := s.repo.FindByEmail(ctx, tenantID, req.Email)
|
|
if err != nil {
|
|
return domain.TokenPair{}, errors.ErrUnauthorized
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
|
return domain.TokenPair{}, errors.ErrPasswordWrong
|
|
}
|
|
|
|
if user.Status != 1 {
|
|
return domain.TokenPair{}, errors.ErrForbidden
|
|
}
|
|
|
|
tokens, err := s.generateTokens(user)
|
|
if err != nil {
|
|
return domain.TokenPair{}, err
|
|
}
|
|
|
|
now := time.Now()
|
|
user.LastLogin = &now
|
|
_ = s.repo.Update(ctx, user)
|
|
|
|
return tokens, nil
|
|
}
|
|
|
|
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (domain.TokenPair, error) {
|
|
claims, err := ParseToken(refreshToken, s.jwtCfg.Secret)
|
|
if err != nil {
|
|
return domain.TokenPair{}, errors.ErrRefreshToken
|
|
}
|
|
if claims.Type != "refresh" {
|
|
return domain.TokenPair{}, errors.ErrRefreshToken
|
|
}
|
|
|
|
tenantID, ok := tenant.FromContext(ctx)
|
|
if !ok {
|
|
return domain.TokenPair{}, errors.ErrTenantRequired
|
|
}
|
|
user, err := s.repo.FindByID(ctx, tenantID, claims.UserID)
|
|
if err != nil {
|
|
return domain.TokenPair{}, errors.ErrUnauthorized
|
|
}
|
|
|
|
return s.generateTokens(user)
|
|
}
|
|
|
|
func (s *Service) ChangePassword(ctx context.Context, userID uint, req domain.ChangePasswordRequest) error {
|
|
tenantID, ok := tenant.FromContext(ctx)
|
|
if !ok {
|
|
return errors.ErrTenantRequired
|
|
}
|
|
|
|
user, err := s.repo.FindByID(ctx, tenantID, userID)
|
|
if err != nil {
|
|
return errors.ErrUserNotFound
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
|
|
return errors.ErrPasswordWrong
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12)
|
|
if err != nil {
|
|
return errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500)
|
|
}
|
|
|
|
user.Password = string(hash)
|
|
return s.repo.Update(ctx, user)
|
|
}
|
|
|
|
func (s *Service) GetProfile(ctx context.Context, userID uint) (domain.UserDTO, error) {
|
|
tenantID, ok := tenant.FromContext(ctx)
|
|
if !ok {
|
|
return domain.UserDTO{}, errors.ErrTenantRequired
|
|
}
|
|
|
|
user, err := s.repo.FindByID(ctx, tenantID, userID)
|
|
if err != nil {
|
|
return domain.UserDTO{}, errors.ErrUserNotFound
|
|
}
|
|
|
|
return domain.ToUserDTO(user), nil
|
|
}
|
|
|
|
func (s *Service) JWTSecret() string {
|
|
return s.jwtCfg.Secret
|
|
}
|
|
|
|
func (s *Service) generateTokens(user *domain.User) (domain.TokenPair, error) {
|
|
access, err := GenerateToken(user.ID, user.TenantID, "access",
|
|
time.Duration(s.jwtCfg.AccessExpiryMinutes)*time.Minute,
|
|
s.jwtCfg.Secret, s.jwtCfg.Issuer,
|
|
)
|
|
if err != nil {
|
|
return domain.TokenPair{}, errors.Wrap(err, "TOKEN_GEN_FAILED", "failed to generate token", 500)
|
|
}
|
|
|
|
refresh, err := GenerateToken(user.ID, user.TenantID, "refresh",
|
|
time.Duration(s.jwtCfg.RefreshExpiryDays)*24*time.Hour,
|
|
s.jwtCfg.Secret, s.jwtCfg.Issuer,
|
|
)
|
|
if err != nil {
|
|
return domain.TokenPair{}, errors.Wrap(err, "TOKEN_GEN_FAILED", "failed to generate refresh token", 500)
|
|
}
|
|
|
|
return domain.TokenPair{
|
|
AccessToken: access,
|
|
RefreshToken: refresh,
|
|
ExpiresIn: s.jwtCfg.AccessExpiryMinutes * 60,
|
|
}, nil
|
|
}
|