From a58e0f44515365513288787b015c997c63da470b Mon Sep 17 00:00:00 2001 From: d3m0k1d Date: Tue, 10 Feb 2026 21:26:48 +0300 Subject: [PATCH] feat: add simple jwt middleware and update oauth handlers --- backend/.gitignore | 1 + backend/go.mod | 1 + backend/go.sum | 2 + backend/internal/auth/jwt.go | 60 ++++++++++++++ backend/internal/handlers/auth_handlers.go | 79 ++++++++++++++----- .../internal/handlers/registry_handlers.go | 3 +- backend/internal/storage/migrations.go | 10 ++- backend/internal/storage/models.go | 8 ++ 8 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 backend/internal/auth/jwt.go diff --git a/backend/.gitignore b/backend/.gitignore index 634b0a2..adef40a 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ /bin /data +.env diff --git a/backend/go.mod b/backend/go.mod index 362c206..8341c4a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -31,6 +31,7 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/backend/go.sum b/backend/go.sum index 660e839..3a6e0ba 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -61,6 +61,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/backend/internal/auth/jwt.go b/backend/internal/auth/jwt.go new file mode 100644 index 0000000..1da5967 --- /dev/null +++ b/backend/internal/auth/jwt.go @@ -0,0 +1,60 @@ +package auth + +import ( + "fmt" + "os" + "strings" + + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +var jwtSecret = []byte(os.Getenv("JWT_SECRET")) + +func GenerateJWT(user storage.User) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{ + "id": user.ID, + "email": user.Email, + "login": user.GithubLogin, + }) + tokenString, err := token.SignedString(jwtSecret) + if err != nil { + return "", err + } + return tokenString, nil +} + +func JWTMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + auth := c.GetHeader("Authorization") + if !strings.HasPrefix(auth, "Bearer ") { + c.AbortWithStatusJSON(401, gin.H{"error": "Bearer required"}) + return + } + + tokenString := strings.TrimPrefix(auth, "Bearer ") + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return jwtSecret, nil + }) + + if err != nil || !token.Valid { + c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"}) + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + c.AbortWithStatusJSON(401, gin.H{"error": "invalid claims"}) + return + } + + c.Set("user_id", int(claims["id"].(float64))) + c.Set("login", claims["login"].(string)) + c.Next() + } +} diff --git a/backend/internal/handlers/auth_handlers.go b/backend/internal/handlers/auth_handlers.go index c7c94ff..b3ef451 100644 --- a/backend/internal/handlers/auth_handlers.go +++ b/backend/internal/handlers/auth_handlers.go @@ -13,36 +13,75 @@ import ( type AuthHandlers struct { repo repositories.AuthRepository logger *logger.Logger -} - -var configGithub = &oauth2.Config{ - ClientID: os.Getenv("GITHUB_CLIENT_ID"), - ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), - RedirectURL: "https://d3m0k1d.ru/", - Scopes: []string{"user"}, - Endpoint: endpoints.GitHub, + config *oauth2.Config } func NewAuthHandlers(repo repositories.AuthRepository) *AuthHandlers { - return &AuthHandlers{repo: repo, logger: logger.New(false)} + clientID := os.Getenv("GITHUB_CLIENT_ID") + clientSecret := os.Getenv("GITHUB_CLIENT_SECRET") + redirectURL := os.Getenv("REDIRECT_URL") + + if clientID == "" || clientSecret == "" { + panic("GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET must be set") + } + if redirectURL == "" { + redirectURL = "http://localhost:8080/api/v1/callback/github" + } + + return &AuthHandlers{ + repo: repo, + logger: logger.New(false), + config: &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURL, + Scopes: []string{"user:email"}, + Endpoint: endpoints.GitHub, + }, + } } -// Callback godoc -// @Summary Callback for oauth2 providers -// @Description Callback for oauth2 providers +// LoginGithub godoc +// @Summary Start GitHub OAuth login +// @Description Redirects to GitHub authorization // @Tags auth -// @Accept json -// @Produce json -// @Success 200 {object} map[string]string +// @Success 302 +// @Router /api/v1/auth/github [get] +func (h *AuthHandlers) LoginGithub(c *gin.Context) { + url := h.config.AuthCodeURL("state", oauth2.AccessTypeOnline) + h.logger.Info("Redirect to GitHub: " + url) + c.Redirect(302, url) +} + +// CallbackGithub godoc +// @Summary GitHub OAuth callback +// @Description Exchanges authorization code for access token +// @Tags auth +// @Param code query string true "Authorization code" +// @Produce json +// @Success 200 {object} map[string]interface{} "Access token" +// @Failure 400 {object} map[string]string "Missing code" +// @Failure 500 {object} map[string]string "Exchange failed" // @Router /callback/github [get] func (h *AuthHandlers) CallbackGithub(c *gin.Context) { h.logger.Info("CallbackGithub called") - token, err := configGithub.Exchange(c.Request.Context(), c.Query("code")) - if err != nil { - h.logger.Error("error request: " + err.Error()) - c.Status(500) + code := c.Query("code") + if code == "" { + h.logger.Error("missing code") + c.JSON(400, gin.H{"error": "missing code"}) + return } - h.logger.Info("200 OK GET /callback/github") + + h.logger.Info("Processing code: " + code[:10] + "...") + + token, err := h.config.Exchange(c.Request.Context(), code) + if err != nil { + h.logger.Error("Exchange failed: " + err.Error()) + c.JSON(500, gin.H{"error": "exchange failed", "details": err.Error()}) + return + } + + h.logger.Info("200 OK - token received") c.JSON(200, gin.H{"token": token}) } diff --git a/backend/internal/handlers/registry_handlers.go b/backend/internal/handlers/registry_handlers.go index f121cfe..d8e9a16 100644 --- a/backend/internal/handlers/registry_handlers.go +++ b/backend/internal/handlers/registry_handlers.go @@ -11,7 +11,8 @@ func Register(router *gin.Engine, db *sql.DB) { handler_posts := NewPostHandlers(repositories.NewPostRepository(db)) handler_auth := NewAuthHandlers(repositories.NewAuthRepository(db)) v1 := router.Group("api/v1") - v1.GET("/callback", handler_auth.CallbackGithub) + v1.GET("/callback/github", handler_auth.CallbackGithub) + v1.GET("/auth/github", handler_auth.LoginGithub) posts := v1.Group("posts") { posts.GET("/", handler_posts.GetPosts) diff --git a/backend/internal/storage/migrations.go b/backend/internal/storage/migrations.go index 5cdf8da..58f1a9d 100644 --- a/backend/internal/storage/migrations.go +++ b/backend/internal/storage/migrations.go @@ -6,5 +6,13 @@ CREATE TABLE IF NOT EXISTS posts( title TEXT NOT NULL, content TEXT NOT NULL, CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP - ); +); + +CREATE TABLE IF NOT EXISTS users( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT, + github_id TEXT, + github_login TEXT, + avatar_url TEXT +); ` diff --git a/backend/internal/storage/models.go b/backend/internal/storage/models.go index 69c8637..99c8de7 100644 --- a/backend/internal/storage/models.go +++ b/backend/internal/storage/models.go @@ -17,3 +17,11 @@ type PostCreate struct { Title string `json:"title"` Content string `json:"content"` } + +type User struct { + ID int `db:"id"` + Email string `db:"email"` + GithubID string `db:"github_id"` + GithubLogin string `db:"github_login"` + AvatarURL string `db:"avatar_url"` +}