package auth import ( "crypto/rand" "encoding/hex" "errors" "time" "github.com/golang-jwt/jwt/v5" ) type Claims struct { UserID uint `json:"user_id"` Username string `json:"username"` Timezone string `json:"timezone"` Type string `json:"type"` jwt.RegisteredClaims } type TokenManager struct { secret []byte accessTTLMinutes int refreshTTLHours int } func (tm *TokenManager) RefreshMaxAgeSeconds() int { return tm.refreshTTLHours * 3600 } func NewTokenManager(secret string, accessTTLMinutes, refreshTTLHours int) *TokenManager { return &TokenManager{ secret: []byte(secret), accessTTLMinutes: accessTTLMinutes, refreshTTLHours: refreshTTLHours, } } func (tm *TokenManager) GenerateAccessToken(userID uint, username, timezone string) (string, error) { now := time.Now().UTC() claims := Claims{ UserID: userID, Username: username, Timezone: timezone, Type: "access", RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(tm.accessTTLMinutes) * time.Minute)), Subject: username, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(tm.secret) } func (tm *TokenManager) GenerateRefreshToken(userID uint, username, timezone string) (string, string, time.Time, error) { now := time.Now().UTC() expiresAt := now.Add(time.Duration(tm.refreshTTLHours) * time.Hour) jti, err := randomJTI() if err != nil { return "", "", time.Time{}, err } claims := Claims{ UserID: userID, Username: username, Timezone: timezone, Type: "refresh", RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(expiresAt), Subject: username, ID: jti, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenStr, err := token.SignedString(tm.secret) if err != nil { return "", "", time.Time{}, err } return tokenStr, jti, expiresAt, nil } func (tm *TokenManager) ParseAndValidate(tokenStr string, expectedType string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("unexpected signing method") } return tm.secret, nil }) if err != nil { return nil, err } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, errors.New("invalid token") } if claims.Type != expectedType { return nil, errors.New("invalid token type") } return claims, nil } func randomJTI() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }