diff --git a/go.mod b/go.mod index c75bc0a..02afe22 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.24.5 require ( github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + golang.org/x/crypto v0.40.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 @@ -37,7 +39,6 @@ require ( github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.40.0 // indirect golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect diff --git a/go.sum b/go.sum index 42484de..4e49751 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go new file mode 100644 index 0000000..e58cc31 --- /dev/null +++ b/internal/auth/jwt.go @@ -0,0 +1,54 @@ +// internal/auth/jwt.go +package auth + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var secret = []byte("change-me-in-env") + +type Claims struct { + UserID int64 `json:"user_id"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +func GenerateToken(userID int64, username string) (string, error) { + now := time.Now() + + claims := Claims{ + UserID: userID, + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString(secret) +} + +func ParseToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims( + tokenString, + &Claims{}, + func(token *jwt.Token) (interface{}, error) { + return secret, nil + }, + ) + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, jwt.ErrTokenInvalidClaims + } + + return claims, nil +} diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go new file mode 100644 index 0000000..8c5f81d --- /dev/null +++ b/internal/middleware/jwt.go @@ -0,0 +1,41 @@ +// internal/middleware/jwt.go +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "feedsystem_video_go/internal/auth" +) + +// JWTAuth check jwt token +func JWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"}) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header"}) + return + } + + tokenString := parts[1] + + claims, err := auth.ParseToken(tokenString) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"}) + return + } + + c.Set("userID", claims.UserID) + c.Set("username", claims.Username) + + c.Next() + } +}