Merge pull request 'fix redirect to homepage after auth and add static server for files' (#14) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 5m26s
Frontend deploy / deploy-frontend (push) Successful in 1m37s

Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-02-15 10:59:33 +00:00
10 changed files with 360 additions and 16 deletions

View File

@@ -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: test:
@@ -29,6 +29,9 @@ docker:
swag init -g ./cmd/main.go --parseDependency --parseInternal swag init -g ./cmd/main.go --parseDependency --parseInternal
docker build -t backend . 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: run-docker:
docker build -t backend . docker build -t backend .
docker run --rm -p 8080:8080 --env-file .env backend:latest docker run --rm -p 8080:8080 --env-file .env backend:latest

View File

@@ -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": { "definitions": {
@@ -455,6 +537,9 @@ const docTemplate = `{
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -466,6 +551,9 @@ const docTemplate = `{
"content": { "content": {
"type": "string" "type": "string"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }

View File

@@ -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": { "definitions": {
@@ -444,6 +526,9 @@
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -455,6 +540,9 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }

View File

@@ -25,6 +25,8 @@ definitions:
type: string type: string
id: id:
type: integer type: integer
published:
type: boolean
title: title:
type: string type: string
type: object type: object
@@ -32,6 +34,8 @@ definitions:
properties: properties:
content: content:
type: string type: string
published:
type: boolean
title: title:
type: string type: string
type: object type: object
@@ -292,6 +296,57 @@ paths:
summary: Get user session summary: Get user session
tags: tags:
- auth - 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: securityDefinitions:
Bearer: Bearer:
description: Type "Bearer" followed by a space and the JWT token. description: Type "Bearer" followed by a space and the JWT token.

View File

@@ -11,8 +11,12 @@ import (
func Register(router *gin.Engine, db *sql.DB) { func Register(router *gin.Engine, db *sql.DB) {
handler_posts := NewPostHandlers(repositories.NewPostRepository(db)) handler_posts := NewPostHandlers(repositories.NewPostRepository(db))
handler_auth := NewAuthHandlers(repositories.NewAuthRepository(db)) handler_auth := NewAuthHandlers(repositories.NewAuthRepository(db))
handler_static := NewStaticHandlers()
router.GET("/health", func(c *gin.Context) { c.Status(200) }) router.GET("/health", func(c *gin.Context) { c.Status(200) })
v1 := router.Group("api/v1") 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("/callback/github", handler_auth.CallbackGithub)
v1.GET("/auth/github", handler_auth.LoginGithub) v1.GET("/auth/github", handler_auth.LoginGithub)
v1.GET("/session", auth.JWTMiddleware(), handler_auth.GetSession) v1.GET("/session", auth.JWTMiddleware(), handler_auth.GetSession)

View File

@@ -0,0 +1,90 @@
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 {
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) {
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)
}

View File

@@ -3,6 +3,7 @@ package repositories
import ( import (
"context" "context"
"database/sql" "database/sql"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage"
@@ -21,7 +22,7 @@ func NewPostRepository(db *sql.DB) PostRepository {
} }
func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error) { func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error) {
var result []storage.PostReq 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 { if err != nil {
p.logger.Error(err.Error()) p.logger.Error(err.Error())
return nil, err return nil, err
@@ -45,7 +46,7 @@ func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error)
func (p *postRepository) GetByID(ctx context.Context, id int) (storage.PostReq, error) { func (p *postRepository) GetByID(ctx context.Context, id int) (storage.PostReq, error) {
var result storage.PostReq 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 title string
var content string var content string
err := row.Scan(&title, &content) err := row.Scan(&title, &content)
@@ -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 { func (p *postRepository) Update(ctx context.Context, id int, post storage.Post) error {
_, err := p.db.Exec( query := "UPDATE posts SET "
"UPDATE posts SET title = ?, content = ? WHERE id = ?", args := []interface{}{}
post.Title, updates := []string{}
post.Content,
id, if post.Title != "" {
) updates = append(updates, "title = ?")
if err != nil { args = append(args, post.Title)
return err
} }
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 { func (p *postRepository) Delete(ctx context.Context, id int) error {

View File

@@ -4,6 +4,7 @@ const Migrations = `
CREATE TABLE IF NOT EXISTS posts( CREATE TABLE IF NOT EXISTS posts(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL, title TEXT NOT NULL,
published BOOLEAN DEFAULT 0,
content TEXT NOT NULL, content TEXT NOT NULL,
CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP
); );

View File

@@ -3,6 +3,7 @@ package storage
type Post struct { type Post struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Title string `db:"title" json:"title"` Title string `db:"title" json:"title"`
Published bool `db:"published" json:"published"`
Content string `db:"content" json:"content"` Content string `db:"content" json:"content"`
CreatedAt string `db:"created_at" json:"created_at"` CreatedAt string `db:"created_at" json:"created_at"`
} }
@@ -15,6 +16,7 @@ type PostReq struct {
type PostCreate struct { type PostCreate struct {
Title string `db:"title" json:"title"` Title string `db:"title" json:"title"`
Published bool `db:"published" json:"published"`
Content string `db:"content" json:"content"` Content string `db:"content" json:"content"`
} }

View File

@@ -18,7 +18,7 @@ export default function AuthCallback() {
await checkAuth(); await checkAuth();
navigate("/", { replace: true }); window.location.href = "/";
} else { } else {
console.error("No token in URL"); console.error("No token in URL");
navigate("/login?error=no_token"); navigate("/login?error=no_token");