package service import ( "context" "errors" "fmt" "time" "apskel-pos-be/config" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/models" "apskel-pos-be/internal/transformer" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type AuthService interface { Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error) ValidateToken(tokenString string) (*contract.UserResponse, error) RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) Logout(ctx context.Context, tokenString string) error } type AuthServiceImpl struct { userProcessor UserProcessor jwtSecret string refreshSecret string tokenTTL time.Duration refreshTokenTTL time.Duration } type Claims struct { UserID uuid.UUID `json:"user_id"` Email string `json:"email"` Role string `json:"role"` OrganizationID uuid.UUID `json:"organization_id"` jwt.RegisteredClaims } func NewAuthService(userProcessor UserProcessor, authConfig *config.AuthConfig) AuthService { return &AuthServiceImpl{ userProcessor: userProcessor, jwtSecret: authConfig.AccessTokenSecret(), refreshSecret: authConfig.RefreshTokenSecret(), tokenTTL: authConfig.AccessTokenTTL(), refreshTokenTTL: authConfig.RefreshTokenTTL(), } } func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error) { userResponse, err := s.userProcessor.GetUserByEmail(ctx, req.Email) if err != nil { return nil, fmt.Errorf("invalid credentials") } if !userResponse.IsActive { return nil, fmt.Errorf("user account is deactivated") } userEntity, err := s.userProcessor.GetUserEntityByEmail(ctx, req.Email) if err != nil { return nil, fmt.Errorf("invalid credentials") } err = bcrypt.CompareHashAndPassword([]byte(userEntity.PasswordHash), []byte(req.Password)) if err != nil { return nil, fmt.Errorf("invalid credentials") } contractUserResponse := transformer.UserModelResponseToResponse(userResponse) token, expiresAt, err := s.generateToken(userResponse) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } refreshToken, refreshExpiresAt, err := s.generateRefreshToken(userResponse) if err != nil { return nil, fmt.Errorf("failed to generate refresh token: %w", err) } return &contract.LoginResponse{ Token: token, RefreshToken: refreshToken, ExpiresAt: expiresAt, RefreshExpiresAt: refreshExpiresAt, User: *contractUserResponse, }, nil } func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserResponse, error) { claims, err := s.parseToken(tokenString) if err != nil { return nil, fmt.Errorf("invalid token: %w", err) } userResponse, err := s.userProcessor.GetUserByID(context.Background(), claims.UserID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } if !userResponse.IsActive { return nil, fmt.Errorf("user account is deactivated") } contractUserResponse := transformer.UserModelResponseToResponse(userResponse) return contractUserResponse, nil } func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) { claims, err := s.parseRefreshToken(tokenString) if err != nil { return nil, fmt.Errorf("invalid refresh token: %w", err) } userResponse, err := s.userProcessor.GetUserByID(ctx, claims.UserID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } if !userResponse.IsActive { return nil, fmt.Errorf("user account is deactivated") } contractUserResponse := transformer.UserModelResponseToResponse(userResponse) newToken, expiresAt, err := s.generateToken(userResponse) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } refreshToken, refreshExpiresAt, err := s.generateRefreshToken(userResponse) if err != nil { return nil, fmt.Errorf("failed to generate refresh token: %w", err) } return &contract.LoginResponse{ Token: newToken, RefreshToken: refreshToken, ExpiresAt: expiresAt, RefreshExpiresAt: refreshExpiresAt, User: *contractUserResponse, }, nil } func (s *AuthServiceImpl) Logout(ctx context.Context, tokenString string) error { // In a more sophisticated implementation, you might want to blacklist the token // For now, we'll just validate that the token is valid _, err := s.parseToken(tokenString) if err != nil { return fmt.Errorf("invalid token: %w", err) } // In the future, you could store blacklisted tokens in Redis or database return nil } func (s *AuthServiceImpl) generateToken(user *models.UserResponse) (string, time.Time, error) { expiresAt := time.Now().Add(s.tokenTTL) claims := &Claims{ UserID: user.ID, Email: user.Email, Role: string(user.Role), OrganizationID: user.OrganizationID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "apskel-pos", Subject: user.ID.String(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(s.jwtSecret)) if err != nil { return "", time.Time{}, err } return tokenString, expiresAt, nil } func (s *AuthServiceImpl) generateRefreshToken(user *models.UserResponse) (string, time.Time, error) { expiresAt := time.Now().Add(s.refreshTokenTTL) claims := &Claims{ UserID: user.ID, Email: user.Email, Role: string(user.Role), OrganizationID: user.OrganizationID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "apskel-pos-refresh", Subject: user.ID.String(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(s.refreshSecret)) if err != nil { return "", time.Time{}, err } return tokenString, expiresAt, nil } func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(s.jwtSecret), nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, errors.New("invalid token") } func (s *AuthServiceImpl) parseRefreshToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(s.refreshSecret), nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*Claims); ok && token.Valid { // Verify this is a refresh token by checking the issuer if claims.Issuer != "apskel-pos-refresh" { return nil, errors.New("not a valid refresh token") } return claims, nil } return nil, errors.New("invalid refresh token") }