package services import ( "errors" "fmt" "time" "gobeyhan/config" "gobeyhan/database/models" "github.com/golang-jwt/jwt/v5" ) type JWTClaim struct { UserID string `json:"user_id"` Email string `json:"email"` Permissions []string `json:"permissions,omitempty"` jwt.RegisteredClaims } type JWTService struct{} func NewJWTService() *JWTService { return &JWTService{} } func (s *JWTService) GenerateToken(user models.User) (string, error) { if config.AppConfig == nil || config.AppConfig.JWTSecret == "" { return "", errors.New("jwt secret not configured") } expirationTime := time.Now().Add(24 * time.Hour) claims := &JWTClaim{ UserID: fmt.Sprintf("%d", user.ID), Email: user.Email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.AppConfig.JWTSecret)) } func (s *JWTService) GenerateTokenPair(user models.User) (string, string, error) { if config.AppConfig == nil || config.AppConfig.JWTSecret == "" { return "", "", errors.New("jwt secret not configured") } // Extract permissions permissionMap := make(map[string]bool) for _, role := range user.Roles { for _, perm := range role.Permissions { permissionMap[perm.Name] = true } } var permissions []string for p := range permissionMap { permissions = append(permissions, p) } // Access Token expirationMinutes := 120 // Default fallback if config.AppConfig.AccessTokenExpireMinutes > 0 { expirationMinutes = config.AppConfig.AccessTokenExpireMinutes } accessTokenExp := time.Now().Add(time.Duration(expirationMinutes) * time.Minute) accessClaims := &JWTClaim{ UserID: fmt.Sprintf("%d", user.ID), Email: user.Email, Permissions: permissions, RegisteredClaims: jwt.RegisteredClaims{ Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim ExpiresAt: jwt.NewNumericDate(accessTokenExp), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) signedAccessToken, err := accessToken.SignedString([]byte(config.AppConfig.JWTSecret)) if err != nil { return "", "", err } // Refresh Token expirationDays := 30 // Default fallback if config.AppConfig.RefreshTokenExpireDays > 0 { expirationDays = config.AppConfig.RefreshTokenExpireDays } refreshTokenExp := time.Now().Add(time.Duration(expirationDays) * 24 * time.Hour) refreshClaims := &JWTClaim{ UserID: fmt.Sprintf("%d", user.ID), Email: user.Email, Permissions: nil, // Refresh token doesn't need permissions usually, or keep them if needed RegisteredClaims: jwt.RegisteredClaims{ Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim ExpiresAt: jwt.NewNumericDate(refreshTokenExp), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) signedRefreshToken, err := refreshToken.SignedString([]byte(config.AppConfig.JWTSecret)) if err != nil { return "", "", err } return signedAccessToken, signedRefreshToken, nil } func (s *JWTService) ValidateToken(signedToken string) (*JWTClaim, error) { if config.AppConfig == nil || config.AppConfig.JWTSecret == "" { return nil, errors.New("jwt secret not configured") } token, err := jwt.ParseWithClaims( signedToken, &JWTClaim{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("unexpected signing method") } return []byte(config.AppConfig.JWTSecret), nil }, ) if err != nil { return nil, err } claims, ok := token.Claims.(*JWTClaim) if !ok { return nil, errors.New("could not parse claims") } if claims.ExpiresAt.Time.Before(time.Now()) { return nil, errors.New("token expired") } return claims, nil } // GenerateVerificationToken generates a JWT token for email verification (24 hours expiry) func (s *JWTService) GenerateVerificationToken(userID, email string) (string, error) { if config.AppConfig == nil || config.AppConfig.JWTSecret == "" { return "", errors.New("jwt secret not configured") } expirationTime := time.Now().Add(24 * time.Hour) claims := &JWTClaim{ UserID: userID, Email: email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.AppConfig.JWTSecret)) } // ValidateVerificationToken validates a verification token and returns user ID and email func (s *JWTService) ValidateVerificationToken(tokenString string) (string, string, error) { claims, err := s.ValidateToken(tokenString) if err != nil { return "", "", err } return claims.UserID, claims.Email, nil }