package application import ( "context" "time" "mengstack/internal/kernel/errors" "mengstack/internal/kernel/tenant" "mengstack/internal/modules/auth/domain" "golang.org/x/crypto/bcrypt" ) 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 } 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{}, err } 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 }