mengstack-api/internal/modules/auth/application/service.go
MengStack Dev 22dba67920 fix(auth): 包装数据库错误为 AppError 避免裸 500
- repo.Create 错误包装为 CREATE_USER_FAILED
- FindByEmail 非 RecordNotFound 错误包装为 QUERY_USER_FAILED
- 修复无效 UUID tenant 导致 gorm 原生错误泄露为 500 的问题
2026-10-03 00:35:15 +08:00

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
}