From ea8fa90a31363eb0f36ab4deab23e28e626e35c9 Mon Sep 17 00:00:00 2001 From: d3m0k1d Date: Sun, 15 Feb 2026 00:22:43 +0300 Subject: [PATCH 1/5] fix: redirect --- frontend/src/components/AuthCallback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/AuthCallback.tsx b/frontend/src/components/AuthCallback.tsx index 114936c..2404551 100644 --- a/frontend/src/components/AuthCallback.tsx +++ b/frontend/src/components/AuthCallback.tsx @@ -18,7 +18,7 @@ export default function AuthCallback() { await checkAuth(); - navigate("/", { replace: true }); + window.location.href = "/"; } else { console.error("No token in URL"); navigate("/login?error=no_token"); From a96ef069cc0d2d8c221a4ddcad46680b7bb72705 Mon Sep 17 00:00:00 2001 From: d3m0k1d Date: Sun, 15 Feb 2026 00:51:29 +0300 Subject: [PATCH 2/5] feat: add published at db models and fix repo for this update --- backend/internal/repositories/post_repository.go | 6 ++++-- backend/internal/storage/migrations.go | 1 + backend/internal/storage/models.go | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/internal/repositories/post_repository.go b/backend/internal/repositories/post_repository.go index d248b14..5216441 100644 --- a/backend/internal/repositories/post_repository.go +++ b/backend/internal/repositories/post_repository.go @@ -21,7 +21,7 @@ func NewPostRepository(db *sql.DB) PostRepository { } func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error) { var result []storage.PostReq - rows, err := p.db.Query("SELECT id, title, content FROM posts") + rows, err := p.db.Query("SELECT id, title, content FROM posts WHERE published = 1") if err != nil { p.logger.Error(err.Error()) return nil, err @@ -45,7 +45,7 @@ func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error) func (p *postRepository) GetByID(ctx context.Context, id int) (storage.PostReq, error) { var result storage.PostReq - row := p.db.QueryRow("SELECT title, content FROM posts WHERE id = ?", id) + row := p.db.QueryRow("SELECT title, content FROM posts WHERE id = ? AND published = 1", id) var title string var content string err := row.Scan(&title, &content) @@ -122,3 +122,5 @@ func (p *postRepository) IsExist(ctx context.Context, id int) bool { } return true } + +// TODO: Add query for change published status diff --git a/backend/internal/storage/migrations.go b/backend/internal/storage/migrations.go index 50cd1c3..19015d6 100644 --- a/backend/internal/storage/migrations.go +++ b/backend/internal/storage/migrations.go @@ -4,6 +4,7 @@ const Migrations = ` CREATE TABLE IF NOT EXISTS posts( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, + published BOOLEAN DEFAULT 0, content TEXT NOT NULL, CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP ); diff --git a/backend/internal/storage/models.go b/backend/internal/storage/models.go index ea70986..c974685 100644 --- a/backend/internal/storage/models.go +++ b/backend/internal/storage/models.go @@ -3,6 +3,7 @@ package storage type Post struct { ID int `db:"id" json:"id"` Title string `db:"title" json:"title"` + Published bool `db:"published" json:"published"` Content string `db:"content" json:"content"` CreatedAt string `db:"created_at" json:"created_at"` } @@ -14,8 +15,9 @@ type PostReq struct { } type PostCreate struct { - Title string `db:"title" json:"title"` - Content string `db:"content" json:"content"` + Title string `db:"title" json:"title"` + Published bool `db:"published" json:"published"` + Content string `db:"content" json:"content"` } type User struct { From 51f8a125e9204998e0b5cde5c4423fff95f70c40 Mon Sep 17 00:00:00 2001 From: d3m0k1d Date: Sun, 15 Feb 2026 01:11:59 +0300 Subject: [PATCH 3/5] feat: update logic for update query to db for posts --- backend/docs/docs.go | 6 ++++ backend/docs/swagger.json | 6 ++++ backend/docs/swagger.yaml | 4 +++ .../internal/repositories/post_repository.go | 35 ++++++++++++------- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 70aaa57..b179ebf 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -455,6 +455,9 @@ const docTemplate = `{ "id": { "type": "integer" }, + "published": { + "type": "boolean" + }, "title": { "type": "string" } @@ -466,6 +469,9 @@ const docTemplate = `{ "content": { "type": "string" }, + "published": { + "type": "boolean" + }, "title": { "type": "string" } diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 86967b0..6f365dc 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -444,6 +444,9 @@ "id": { "type": "integer" }, + "published": { + "type": "boolean" + }, "title": { "type": "string" } @@ -455,6 +458,9 @@ "content": { "type": "string" }, + "published": { + "type": "boolean" + }, "title": { "type": "string" } diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 6bbafa1..98d6a4c 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -25,6 +25,8 @@ definitions: type: string id: type: integer + published: + type: boolean title: type: string type: object @@ -32,6 +34,8 @@ definitions: properties: content: type: string + published: + type: boolean title: type: string type: object diff --git a/backend/internal/repositories/post_repository.go b/backend/internal/repositories/post_repository.go index 5216441..e981a4b 100644 --- a/backend/internal/repositories/post_repository.go +++ b/backend/internal/repositories/post_repository.go @@ -3,6 +3,7 @@ package repositories import ( "context" "database/sql" + "strings" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" @@ -78,17 +79,29 @@ func (p *postRepository) Create(ctx context.Context, post storage.Post) error { } func (p *postRepository) Update(ctx context.Context, id int, post storage.Post) error { - _, err := p.db.Exec( - "UPDATE posts SET title = ?, content = ? WHERE id = ?", - post.Title, - post.Content, - id, - ) - if err != nil { - return err + query := "UPDATE posts SET " + args := []interface{}{} + updates := []string{} + + if post.Title != "" { + updates = append(updates, "title = ?") + args = append(args, post.Title) } - p.logger.Info("Post updated:", "id", id) - return nil + + if post.Content != "" { + updates = append(updates, "content = ?") + args = append(args, post.Content) + } + + updates = append(updates, "published = ?") + args = append(args, post.Published) + updates = append(updates, "updated_at = CURRENT_TIMESTAMP") + query += strings.Join(updates, ", ") + query += " WHERE id = ?" + args = append(args, id) + + _, err := p.db.ExecContext(ctx, query, args...) + return err } func (p *postRepository) Delete(ctx context.Context, id int) error { @@ -122,5 +135,3 @@ func (p *postRepository) IsExist(ctx context.Context, id int) bool { } return true } - -// TODO: Add query for change published status From 482e8571af76e57c9b2296e3ae4504f6622fd99f Mon Sep 17 00:00:00 2001 From: d3m0k1d Date: Sun, 15 Feb 2026 02:15:05 +0300 Subject: [PATCH 4/5] feat: add static files simple server --- backend/Makefile | 5 +- backend/docs/docs.go | 82 +++++++++++++++++++ backend/docs/swagger.json | 82 +++++++++++++++++++ backend/docs/swagger.yaml | 51 ++++++++++++ .../internal/handlers/registry_handlers.go | 4 + backend/internal/handlers/static_handlers.go | 59 +++++++++++++ 6 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 backend/internal/handlers/static_handlers.go diff --git a/backend/Makefile b/backend/Makefile index 10bad81..57a5e48 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,4 +1,4 @@ -.PHONY: test build clean lint dev run-docker docs-upd docker +.PHONY: test build clean lint dev run-docker docs-upd docker docker-run test: @@ -29,6 +29,9 @@ docker: swag init -g ./cmd/main.go --parseDependency --parseInternal docker build -t backend . +docker-run: + docker run --rm -p 8080:8080 --env-file .env -v /opt/d3m0k1d.ru/data:/data backend:latest + run-docker: docker build -t backend . docker run --rm -p 8080:8080 --env-file .env backend:latest diff --git a/backend/docs/docs.go b/backend/docs/docs.go index b179ebf..03bf45c 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -412,6 +412,88 @@ const docTemplate = `{ } } } + }, + "/upload": { + "post": { + "description": "Upload static content to the server", + "produces": [ + "application/json" + ], + "tags": [ + "static" + ], + "summary": "Upload static content", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + } + } + } + }, + "/upload/{file}": { + "get": { + "description": "Get static content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "static" + ], + "summary": "Get static content", + "parameters": [ + { + "type": "string", + "description": "File name", + "name": "file", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Static content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "404": { + "description": "File not found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + } + } + } } }, "definitions": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 6f365dc..274c5f4 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -401,6 +401,88 @@ } } } + }, + "/upload": { + "post": { + "description": "Upload static content to the server", + "produces": [ + "application/json" + ], + "tags": [ + "static" + ], + "summary": "Upload static content", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + } + } + } + }, + "/upload/{file}": { + "get": { + "description": "Get static content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "static" + ], + "summary": "Get static content", + "parameters": [ + { + "type": "string", + "description": "File name", + "name": "file", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Static content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "404": { + "description": "File not found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + } + } + } } }, "definitions": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 98d6a4c..21f3420 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -296,6 +296,57 @@ paths: summary: Get user session tags: - auth + /upload: + post: + description: Upload static content to the server + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + summary: Upload static content + tags: + - static + /upload/{file}: + get: + consumes: + - application/json + description: Get static content + parameters: + - description: File name + in: path + name: file + required: true + type: string + produces: + - application/json + responses: + "200": + description: Static content + schema: + allOf: + - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse' + - properties: + data: + type: string + type: object + "404": + description: File not found + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + summary: Get static content + tags: + - static securityDefinitions: Bearer: description: Type "Bearer" followed by a space and the JWT token. diff --git a/backend/internal/handlers/registry_handlers.go b/backend/internal/handlers/registry_handlers.go index c4a916a..cb977a5 100644 --- a/backend/internal/handlers/registry_handlers.go +++ b/backend/internal/handlers/registry_handlers.go @@ -11,8 +11,12 @@ import ( func Register(router *gin.Engine, db *sql.DB) { handler_posts := NewPostHandlers(repositories.NewPostRepository(db)) handler_auth := NewAuthHandlers(repositories.NewAuthRepository(db)) + handler_static := NewStaticHandlers() router.GET("/health", func(c *gin.Context) { c.Status(200) }) v1 := router.Group("api/v1") + v1.Static("/uploads", "/data/uploads") + v1.POST("/upload", auth.JWTMiddleware(), auth.RequireAdmin(), handler_static.PostStatic) + v1.GET("/upload/:file", handler_static.GetStatic) v1.GET("/callback/github", handler_auth.CallbackGithub) v1.GET("/auth/github", handler_auth.LoginGithub) v1.GET("/session", auth.JWTMiddleware(), handler_auth.GetSession) diff --git a/backend/internal/handlers/static_handlers.go b/backend/internal/handlers/static_handlers.go new file mode 100644 index 0000000..e2da9bc --- /dev/null +++ b/backend/internal/handlers/static_handlers.go @@ -0,0 +1,59 @@ +package handlers + +import ( + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/models" + + "github.com/gin-gonic/gin" +) + +type StaticHandlers struct { + logger *logger.Logger +} + +func NewStaticHandlers() *StaticHandlers { + return &StaticHandlers{ + logger: logger.New(false), + } +} + +// PostStatic godoc +// @Summary Upload static content +// @Description Upload static content to the server +// @Tags static +// @Produce json +// @Success 200 {object} models.SuccessResponse(data=string) "Static content" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /upload [post] +func (h *StaticHandlers) PostStatic(c *gin.Context) { + content, err := c.FormFile("file") + if err != nil { + h.logger.Error("error request: " + err.Error()) + models.Error(c, 500, "Internal server error", err.Error()) + return + } + dst := "/data/upload/" + content.Filename + if err = c.SaveUploadedFile(content, dst); err != nil { + h.logger.Error("error request: " + err.Error()) + models.Error(c, 500, "Internal server error", err.Error()) + return + } + models.Success(c, "Static content saved") +} + +// GetStatic godoc +// @Summary Get static content +// @Description Get static content +// @Tags static +// @Accept json +// @Produce json +// @Param file path string true "File name" +// @Success 200 {object} models.SuccessResponse{data=string} "Static content" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Failure 404 {object} models.ErrorResponse "File not found" +// @Router /upload/{file} [get] +func (h *StaticHandlers) GetStatic(c *gin.Context) { + // TODO: Unsecure handler need to be fixed + c.File("/data/upload/" + c.Param("file")) + +} From 18b3e318abe683cc16d5faacdd229fd019b7f1e1 Mon Sep 17 00:00:00 2001 From: d3m0k1d Date: Sun, 15 Feb 2026 13:31:28 +0300 Subject: [PATCH 5/5] feat: update secure on get handler for files --- backend/internal/handlers/static_handlers.go | 37 ++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/backend/internal/handlers/static_handlers.go b/backend/internal/handlers/static_handlers.go index e2da9bc..2272214 100644 --- a/backend/internal/handlers/static_handlers.go +++ b/backend/internal/handlers/static_handlers.go @@ -3,8 +3,10 @@ package handlers import ( "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/models" - "github.com/gin-gonic/gin" + "os" + "path/filepath" + "strings" ) type StaticHandlers struct { @@ -53,7 +55,36 @@ func (h *StaticHandlers) PostStatic(c *gin.Context) { // @Failure 404 {object} models.ErrorResponse "File not found" // @Router /upload/{file} [get] func (h *StaticHandlers) GetStatic(c *gin.Context) { - // TODO: Unsecure handler need to be fixed - c.File("/data/upload/" + c.Param("file")) + filename := c.Param("file") + if filename == "" { + models.Error(c, 404, "File not found", "") + return + } + + filename = filepath.Clean(filename) + + if strings.Contains(filename, "..") { + models.Error(c, 400, "Invalid file path", "") + return + } + + if filepath.IsAbs(filename) { + models.Error(c, 400, "Invalid file path", "") + return + } + + baseDir := "/data/upload/" + fullPath := filepath.Join(baseDir, filename) + if !strings.HasPrefix(fullPath, baseDir) { + models.Error(c, 400, "Invalid file path", "") + return + } + + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + models.Error(c, 404, "File not found", "") + return + } + + c.File(fullPath) }