266 lines
6.2 KiB
Go
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)
|
|
}
|