first commit
This commit is contained in:
265
app/middleware/auth.go
Normal file
265
app/middleware/auth.go
Normal file
@@ -0,0 +1,265 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user