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) ListUsers(ctx context.Context, page, pageSize int, search string, status *int) ([]domain.UserDTO, int64, error) { tenantID, ok := tenant.FromContext(ctx) if !ok { return nil, 0, errors.ErrTenantRequired } users, total, err := s.repo.List(ctx, tenantID, page, pageSize, search, status) if err != nil { return nil, 0, errors.Wrap(err, "LIST_USERS_FAILED", "failed to list users", 500) } dtos := make([]domain.UserDTO, len(users)) for i, u := range users { dtos[i] = domain.ToUserDTO(&u) } return dtos, total, nil } func (s *Service) GetUser(ctx context.Context, id uint) (domain.UserDTO, error) { tenantID, ok := tenant.FromContext(ctx) if !ok { return domain.UserDTO{}, errors.ErrTenantRequired } user, err := s.repo.FindByID(ctx, tenantID, id) if err != nil { return domain.UserDTO{}, errors.ErrUserNotFound } return domain.ToUserDTO(user), nil } func (s *Service) CreateUser(ctx context.Context, req domain.CreateUserRequest) (domain.UserDTO, error) { tenantID, ok := tenant.FromContext(ctx) if !ok { return domain.UserDTO{}, errors.ErrTenantRequired } if _, err := s.repo.FindByEmail(ctx, tenantID, req.Email); err == nil { return domain.UserDTO{}, errors.ErrUserExists } else if err != gorm.ErrRecordNotFound { return domain.UserDTO{}, 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.UserDTO{}, errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500) } status := req.Status if status == 0 { status = 1 } user := &domain.User{ TenantID: tenantID, Username: req.Username, Email: req.Email, Password: string(hash), Nickname: req.Nickname, Status: status, } if err := s.repo.Create(ctx, user); err != nil { return domain.UserDTO{}, errors.Wrap(err, "CREATE_USER_FAILED", "failed to create user", 500) } return domain.ToUserDTO(user), nil } func (s *Service) UpdateUser(ctx context.Context, id uint, req domain.UpdateUserRequest) (domain.UserDTO, error) { tenantID, ok := tenant.FromContext(ctx) if !ok { return domain.UserDTO{}, errors.ErrTenantRequired } user, err := s.repo.FindByID(ctx, tenantID, id) if err != nil { return domain.UserDTO{}, errors.ErrUserNotFound } if req.Nickname != "" { user.Nickname = req.Nickname } if req.Email != "" { user.Email = req.Email } if req.Status != nil { user.Status = *req.Status } if err := s.repo.Update(ctx, user); err != nil { return domain.UserDTO{}, errors.Wrap(err, "UPDATE_USER_FAILED", "failed to update user", 500) } return domain.ToUserDTO(user), nil } func (s *Service) DeleteUser(ctx context.Context, id uint) error { tenantID, ok := tenant.FromContext(ctx) if !ok { return errors.ErrTenantRequired } if _, err := s.repo.FindByID(ctx, tenantID, id); err != nil { return errors.ErrUserNotFound } if err := s.repo.Delete(ctx, tenantID, id); err != nil { return errors.Wrap(err, "DELETE_USER_FAILED", "failed to delete user", 500) } return nil } func (s *Service) UserCount(ctx context.Context, tenantID string) (int64, error) { return s.repo.Count(ctx, tenantID) } func (s *Service) UserCountAll(ctx context.Context) (int64, error) { return s.repo.CountAll(ctx) } func (s *Service) ListAllUsers(ctx context.Context, page, pageSize int) ([]domain.UserDTO, int64, error) { users, total, err := s.repo.ListAll(ctx, page, pageSize) if err != nil { return nil, 0, errors.Wrap(err, "LIST_USERS_FAILED", "failed to list users", 500) } dtos := make([]domain.UserDTO, len(users)) for i, u := range users { dtos[i] = domain.ToUserDTO(&u) } return dtos, total, nil } 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 }