Files
Beyhan Oğur e04ba85564 first commit
2026-04-26 21:40:14 +03:00

266 lines
6.2 KiB
Go

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)
}