package middleware import ( "crypto/rand" "encoding/hex" "errors" "fmt" "net/http" "os" "strconv" "strings" "time" "ginimageApi/app/accounts/models" "ginimageApi/configs" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) type accessTokenPayload struct { UserID uint `json:"user_id"` Email string `json:"email"` Username string `json:"username"` Exp int64 `json:"exp"` } type accessTokenClaims struct { TokenType string `json:"token_type"` UserID string `json:"user_id"` Email string `json:"email"` Username string `json:"username"` jwt.RegisteredClaims } type refreshTokenClaims struct { TokenType string `json:"token_type"` UserID string `json:"user_id"` jwt.RegisteredClaims } func jwtIssuer() string { issuer := os.Getenv("JWT_ISSUER") if issuer == "" { issuer = "ginimageApi" } return issuer } func jwtAudience() string { audience := os.Getenv("JWT_AUDIENCE") if audience == "" { audience = "ginimageApi-client" } return audience } func jwtSecret() string { secret := os.Getenv("JWT_SECRET") if secret == "" { secret = "dev-secret-change-me" } return secret } func randomTokenID() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil } func GenerateAccessToken(userID uint, email, username string, ttl time.Duration) (string, error) { now := time.Now() tokenID, err := randomTokenID() if err != nil { return "", err } claims := accessTokenClaims{ TokenType: "access", UserID: strconv.FormatUint(uint64(userID), 10), Email: email, Username: username, RegisteredClaims: jwt.RegisteredClaims{ ID: tokenID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(jwtSecret())) } func GenerateRefreshToken(userID uint, ttl time.Duration) (string, string, error) { now := time.Now() tokenID, err := randomTokenID() if err != nil { return "", "", err } claims := refreshTokenClaims{ TokenType: "refresh", UserID: strconv.FormatUint(uint64(userID), 10), RegisteredClaims: jwt.RegisteredClaims{ ID: tokenID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(jwtSecret())) if err != nil { return "", "", err } return signed, tokenID, nil } func parseAccessToken(token string) (accessTokenPayload, error) { parsed, err := jwt.ParseWithClaims( token, &accessTokenClaims{}, func(t *jwt.Token) (any, error) { return []byte(jwtSecret()), nil }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithExpirationRequired(), ) if err != nil { return accessTokenPayload{}, errors.New("token gecersiz") } claims, ok := parsed.Claims.(*accessTokenClaims) if !ok || !parsed.Valid { return accessTokenPayload{}, errors.New("token gecersiz") } if claims.TokenType != "access" { return accessTokenPayload{}, errors.New("token type gecersiz") } uid64, err := strconv.ParseUint(claims.UserID, 10, 64) if err != nil { return accessTokenPayload{}, errors.New("user_id claim gecersiz") } exp := int64(0) if claims.ExpiresAt != nil { exp = claims.ExpiresAt.Time.Unix() } return accessTokenPayload{ UserID: uint(uid64), Email: claims.Email, Username: claims.Username, Exp: exp, }, nil } func bearerToken(c *gin.Context) (string, error) { header := strings.TrimSpace(c.GetHeader("Authorization")) if header == "" { return "", errors.New("authorization basligi yok") } parts := strings.SplitN(header, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { return "", errors.New("authorization formati gecersiz") } token := strings.TrimSpace(parts[1]) if token == "" { return "", errors.New("authorization formati gecersiz") } return token, nil } // AuthRequired access token dogrular ve kullanici bilgisini context'e yazar. func AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { token, err := bearerToken(c) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } payload, err := parseAccessToken(token) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } c.Set("user_id", payload.UserID) c.Set("email", payload.Email) c.Set("username", payload.Username) c.Next() } } // AdminRequired mutating endpointlerde kullanicinin admin oldugunu dogrular. func AdminRequired() gin.HandlerFunc { return func(c *gin.Context) { userIDAny, ok := c.Get("user_id") if !ok { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "kullanici bulunamadi"}) return } var userID uint switch v := userIDAny.(type) { case uint: userID = v case int: if v < 0 { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "gecersiz kullanici"}) return } userID = uint(v) case string: parsed, err := strconv.ParseUint(v, 10, 64) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "gecersiz kullanici"}) return } userID = uint(parsed) default: c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "gecersiz kullanici"}) return } var user models.User if err := configs.DB.First(&user, userID).Error; err != nil { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin yetkisi gerekli"}) return } if user.IsAdmin == nil || !*user.IsAdmin { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin yetkisi gerekli"}) return } c.Next() } } func BuildAccessTokenForUser(user models.User) (string, error) { return GenerateAccessToken(user.ID, user.Email, user.UserName, 15*time.Minute) } func RefreshTokenExpiry() time.Duration { return 7 * 24 * time.Hour } func AccessTokenTTL() time.Duration { return 15 * time.Minute } func TokenPayloadDebug(token string) string { payload, err := parseAccessToken(token) if err != nil { return err.Error() } return fmt.Sprintf("uid=%d email=%s username=%s exp=%d", payload.UserID, payload.Email, payload.Username, payload.Exp) }