package service import ( "context" "errors" "fmt" "time" "eslogad-be/internal/contract" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type AuthServiceImpl struct { userProcessor UserProcessor jwtSecret string tokenTTL time.Duration } type Claims struct { UserID uuid.UUID `json:"user_id"` Email string `json:"email"` Role string `json:"role"` Roles []string `json:"roles"` Permissions []string `json:"permissions"` jwt.RegisteredClaims } func NewAuthService(userProcessor UserProcessor, jwtSecret string) *AuthServiceImpl { return &AuthServiceImpl{ userProcessor: userProcessor, jwtSecret: jwtSecret, tokenTTL: 24 * time.Hour, } } 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") } // fetch roles, permissions, positions for response and token roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID) permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID) positions, _ := s.userProcessor.GetUserPositions(ctx, userResponse.ID) token, expiresAt, err := s.generateToken(userResponse, roles, permCodes) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } return &contract.LoginResponse{ Token: token, ExpiresAt: expiresAt, User: *userResponse, Roles: roles, Permissions: permCodes, Positions: positions, }, 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") } return userResponse, nil } func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) { claims, err := s.parseToken(tokenString) if err != nil { return nil, fmt.Errorf("invalid 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") } roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID) permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID) newToken, expiresAt, err := s.generateToken(userResponse, roles, permCodes) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } positions, _ := s.userProcessor.GetUserPositions(ctx, userResponse.ID) return &contract.LoginResponse{ Token: newToken, ExpiresAt: expiresAt, User: *userResponse, Roles: roles, Permissions: permCodes, Positions: positions, }, nil } func (s *AuthServiceImpl) Logout(ctx context.Context, tokenString string) error { _, err := s.parseToken(tokenString) if err != nil { return fmt.Errorf("invalid token: %w", err) } return nil } func (s *AuthServiceImpl) generateToken(user *contract.UserResponse, roles []contract.RoleResponse, permissionCodes []string) (string, time.Time, error) { expiresAt := time.Now().Add(s.tokenTTL) roleCodes := make([]string, 0, len(roles)) for _, r := range roles { roleCodes = append(roleCodes, r.Code) } claims := &Claims{ UserID: user.ID, Email: user.Email, Roles: roleCodes, Permissions: permissionCodes, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "eslogad-be", 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) 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) ExtractAccess(tokenString string) (roles []string, permissions []string, err error) { claims, err := s.parseToken(tokenString) if err != nil { return nil, nil, err } return claims.Roles, claims.Permissions, nil }