package auth import ( "errors" "fmt" "strings" "tercul/internal/domain" "time" "tercul/internal/platform/config" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) var ( ErrInvalidToken = errors.New("invalid token") ErrExpiredToken = errors.New("token expired") ErrInvalidSignature = errors.New("invalid token signature") ErrMissingToken = errors.New("missing token") ErrInsufficientRole = errors.New("insufficient role") ) // Claims represents the JWT claims type Claims struct { UserID uuid.UUID `json:"user_id"` Username string `json:"username"` Email string `json:"email"` Role string `json:"role"` jwt.RegisteredClaims } // JWTManager handles JWT token operations type JWTManagement interface { GenerateToken(user *domain.User) (string, error) ValidateToken(tokenString string) (*Claims, error) } type JWTManager struct { secretKey []byte issuer string duration time.Duration } // NewJWTManager creates a new JWT manager func NewJWTManager(cfg *config.Config) *JWTManager { secretKey := cfg.JWTSecret if secretKey == "" { secretKey = "default-secret-key-change-in-production" } durationInHours := cfg.JWTExpiration if durationInHours <= 0 { durationInHours = 24 // Default to 24 hours } duration := time.Duration(durationInHours) * time.Hour return &JWTManager{ secretKey: []byte(secretKey), issuer: "tercul-api", duration: duration, } } // GenerateToken generates a new JWT token for a user func (j *JWTManager) GenerateToken(user *domain.User) (string, error) { now := time.Now() claims := &Claims{ UserID: user.ID, Username: user.Username, Email: user.Email, Role: string(user.Role), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(j.duration)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Issuer: j.issuer, Subject: user.ID.String(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(j.secretKey) } // ValidateToken validates and parses a JWT token func (j *JWTManager) ValidateToken(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 j.secretKey, nil }) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, ErrExpiredToken } return nil, ErrInvalidToken } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, ErrInvalidToken } return claims, nil } // ExtractTokenFromHeader extracts token from Authorization header func (j *JWTManager) ExtractTokenFromHeader(authHeader string) (string, error) { if authHeader == "" { return "", ErrMissingToken } parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { return "", ErrInvalidToken } return parts[1], nil } // HasRole checks if the user has the required role func (j *JWTManager) HasRole(userRole, requiredRole string) bool { roleHierarchy := map[string]int{ "reader": 1, "contributor": 2, "moderator": 3, "admin": 4, } userLevel, userExists := roleHierarchy[userRole] requiredLevel, requiredExists := roleHierarchy[requiredRole] if !userExists || !requiredExists { return false } return userLevel >= requiredLevel } // RequireRole validates that the user has the required role func (j *JWTManager) RequireRole(userRole, requiredRole string) error { if !j.HasRole(userRole, requiredRole) { return ErrInsufficientRole } return nil }