Compare commits

...

7 Commits

Author SHA1 Message Date
33a41ad066 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
2026-02-15 10:59:33 +00:00
8d6225f136 Merge branch 'master' into develop
Some checks failed
Backend ci / build (pull_request) Failing after 3m31s
2026-02-15 10:33:35 +00:00
d3m0k1d
18b3e318ab feat: update secure on get handler for files
Some checks failed
Backend ci / build (pull_request) Failing after 3m21s
2026-02-15 13:31:28 +03:00
d3m0k1d
482e8571af feat: add static files simple server 2026-02-15 02:15:05 +03:00
d3m0k1d
51f8a125e9 feat: update logic for update query to db for posts 2026-02-15 01:11:59 +03:00
d3m0k1d
a96ef069cc feat: add published at db models and fix repo for this update 2026-02-15 00:51:29 +03:00
d3m0k1d
ea8fa90a31 fix: redirect 2026-02-15 00:22:48 +03: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:
@@ -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

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

View File

@@ -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
@@ -292,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.

View File

@@ -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)

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 (
"context"
"database/sql"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger"
"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) {
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 +46,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)
@@ -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 {

View File

@@ -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
);

View File

@@ -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 {

View File

@@ -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");