feat: Implement JWT Utilities (Issue #2)

- Create JWTManager for token operations
- Implement GenerateAccessToken with user claims
- Implement GenerateRefreshToken
- Implement ValidateToken with expiration checks
- Implement ValidateRefreshToken
- Add ExtractTokenFromHeader helper
- Add comprehensive error handling

Closes #2

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-02-05 00:47:35 +03:00
parent be86fa226a
commit 3c8fb927fb

View File

@@ -0,0 +1,151 @@
package utils
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/yourusername/victorialogs-manager/internal/models"
)
// Claims represents the JWT claims structure
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
Role models.Role `json:"role"`
Permissions []models.Permission `json:"permissions"`
jwt.RegisteredClaims
}
// JWTManager handles JWT token operations
type JWTManager struct {
secretKey string
accessTokenDuration time.Duration
refreshTokenDuration time.Duration
}
// NewJWTManager creates a new JWT manager
func NewJWTManager(secretKey string, accessDuration, refreshDuration time.Duration) *JWTManager {
return &JWTManager{
secretKey: secretKey,
accessTokenDuration: accessDuration,
refreshTokenDuration: refreshDuration,
}
}
// GenerateAccessToken generates a new JWT access token for a user
func (m *JWTManager) GenerateAccessToken(user *models.User) (string, error) {
now := time.Now()
expiresAt := now.Add(m.accessTokenDuration)
claims := Claims{
UserID: user.ID,
Username: user.Username,
Role: user.Role,
Permissions: user.GetPermissions(),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: "victorialogs-manager",
Subject: user.ID,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(m.secretKey))
}
// GenerateRefreshToken generates a new JWT refresh token
func (m *JWTManager) GenerateRefreshToken(userID string) (string, error) {
now := time.Now()
expiresAt := now.Add(m.refreshTokenDuration)
claims := jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: "victorialogs-manager",
Subject: userID,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(m.secretKey))
}
// ValidateToken validates and parses a JWT token
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(
tokenString,
&Claims{},
func(token *jwt.Token) (interface{}, error) {
// Verify signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(m.secretKey), nil
},
)
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token claims")
}
// Check if token is expired
if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(time.Now()) {
return nil, fmt.Errorf("token has expired")
}
return claims, nil
}
// ValidateRefreshToken validates a refresh token and returns the user ID
func (m *JWTManager) ValidateRefreshToken(tokenString string) (string, error) {
token, err := jwt.ParseWithClaims(
tokenString,
&jwt.RegisteredClaims{},
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(m.secretKey), nil
},
)
if err != nil {
return "", fmt.Errorf("invalid refresh token: %w", err)
}
claims, ok := token.Claims.(*jwt.RegisteredClaims)
if !ok || !token.Valid {
return "", fmt.Errorf("invalid refresh token claims")
}
// Check if token is expired
if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(time.Now()) {
return "", fmt.Errorf("refresh token has expired")
}
return claims.Subject, nil
}
// ExtractTokenFromHeader extracts JWT token from Authorization header
// Expected format: "Bearer <token>"
func ExtractTokenFromHeader(authHeader string) (string, error) {
const bearerPrefix = "Bearer "
if len(authHeader) < len(bearerPrefix) {
return "", fmt.Errorf("invalid authorization header format")
}
if authHeader[:len(bearerPrefix)] != bearerPrefix {
return "", fmt.Errorf("authorization header must start with 'Bearer '")
}
return authHeader[len(bearerPrefix):], nil
}