diff --git a/.gitea/workflows/cd-back.yml b/.gitea/workflows/cd-back.yml new file mode 100644 index 0000000..6cb35d7 --- /dev/null +++ b/.gitea/workflows/cd-back.yml @@ -0,0 +1,47 @@ +name: Backend deploy +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + deploy-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Import Secrets + id: import-secrets + uses: hashicorp/vault-action@v3.4.0 + with: + url: https://vault.d3m0k1d.ru + token: ${{ secrets.VAULT }} + secrets: | + secrets/site/prod/data/gitea TOKEN | GITEA_TOKEN ; + secrets/site/prod/data/server SSH_KEY | SSH_KEY ; + secrets/site/prod/data/server USER | SERVER_USER ; + secrets/site/prod/data/server HOST | SERVER_HOST ; + secrets/site/prod/data/server PORT | SERVER_PORT + + - name: Login to registry + run: echo "${{ steps.import-secrets.outputs.GITEA_TOKEN }}" | docker login gitea.d3m0k1d.ru -u d3m0k1d --password-stdin + + - name: Build and push + run: | + docker build -t gitea.d3m0k1d.ru/d3m0k1d/backend:latest ./backend + docker push gitea.d3m0k1d.ru/d3m0k1d/backend:latest + + - name: Deploy at server + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ steps.import-secrets.outputs.SERVER_HOST }} + port: ${{ steps.import-secrets.outputs.SERVER_PORT }} + username: ${{ steps.import-secrets.outputs.SERVER_USER }} + key: ${{ steps.import-secrets.outputs.SSH_KEY }} + script: | + docker login -u d3m0k1d -p ${{ steps.import-secrets.outputs.GITEA_TOKEN }} gitea.d3m0k1d.ru + docker pull gitea.d3m0k1d.ru/d3m0k1d/backend:latest + docker rm -f d3m0k1d-backend || true + docker run --name d3m0k1d-backend -d -p 80:80 --restart unless-stopped gitea.d3m0k1d.ru/d3m0k1d/backend:latest diff --git a/backend/.gitignore b/backend/.gitignore index 5e56e04..634b0a2 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1 +1,2 @@ /bin +/data diff --git a/backend/Dockerfile b/backend/Dockerfile index 5b83b4d..f9c0a02 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app COPY . . ENV CGO_ENABLED=0 +ENV GIN_MODE=release RUN go mod tidy RUN go build -ldflags "-s -w" -o backend ./cmd/main.go diff --git a/backend/Makefile b/backend/Makefile index 1dca6ef..bd98eea 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,4 +1,4 @@ -.PHONY: test build clean lint +.PHONY: test build clean lint dev test: @@ -15,3 +15,7 @@ clean: lint: golangci-lint run --fix + +dev: + swag init -g ./cmd/main.go --parseDependency --parseInternal + go run ./cmd/main.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 2b18496..bcde071 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,18 +1,49 @@ package main import ( - docs "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/docs" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/docs" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/handlers" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" ) func main() { - router := gin.Default() - docs.SwaggerInfo.BasePath = "/api/v1" - docs.SwaggerInfo.Title = "d3m0k1d.ru" - docs.SwaggerInfo.Version = "1.0" - err := router.Run() + log := logger.New(false) + + db, err := storage.OpenSession() if err != nil { + log.Error("Failed to open database", "error", err) + panic(err) + } + defer func() { + err := db.Close() + if err != nil { + log.Error("Failed to close database", "error", err) + } + }() + if err := storage.CreateTables(db); err != nil { + log.Error("Failed to create tables", "error", err) panic(err) } + router := gin.Default() + + // Swagger config + docs.SwaggerInfo.BasePath = "/api/v1" + docs.SwaggerInfo.Title = "d3m0k1d.ru API" + docs.SwaggerInfo.Version = "1.0" + docs.SwaggerInfo.Description = "API for d3m0k1d.ru" + docs.SwaggerInfo.Schemes = []string{"http"} + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + handlers.Register(router, db) + + log.Info("Starting server on :8080...") + if err := router.Run(":8080"); err != nil { + log.Error("Error starting server", "error", err) + panic(err) + } } diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 6676994..25c54ed 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -14,7 +14,182 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "paths": {} + "paths": { + "/posts": { + "get": { + "description": "Get all posts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Get all posts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + } + }, + "post": { + "description": "Create new post", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Create post", + "parameters": [ + { + "description": "Post data", + "name": "post", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/posts/{id}": { + "get": { + "description": "Get post by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Get post by id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + }, + "put": { + "description": "Update post", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Update post", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + }, + "delete": { + "description": "Delete post", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Delete post", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + } + } + }, + "definitions": { + "gin.H": { + "type": "object", + "additionalProperties": {} + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } }` // SwaggerInfo holds exported Swagger Info so clients can modify it diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index ec416cd..ca1073b 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -3,5 +3,180 @@ "info": { "contact": {} }, - "paths": {} + "paths": { + "/posts": { + "get": { + "description": "Get all posts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Get all posts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + } + }, + "post": { + "description": "Create new post", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Create post", + "parameters": [ + { + "description": "Post data", + "name": "post", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/posts/{id}": { + "get": { + "description": "Get post by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Get post by id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + }, + "put": { + "description": "Update post", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Update post", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + }, + "delete": { + "description": "Delete post", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "posts" + ], + "summary": "Delete post", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + } + } + }, + "definitions": { + "gin.H": { + "type": "object", + "additionalProperties": {} + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index b64379c..ff0700f 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,4 +1,118 @@ +definitions: + gin.H: + additionalProperties: {} + type: object + gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post: + properties: + content: + type: string + createdAt: + type: string + id: + type: integer + title: + type: string + type: object + gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate: + properties: + content: + type: string + title: + type: string + type: object + gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq: + properties: + content: + type: string + title: + type: string + type: object info: contact: {} -paths: {} +paths: + /posts: + get: + consumes: + - application/json + description: Get all posts + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' + type: array + summary: Get all posts + tags: + - posts + post: + consumes: + - application/json + description: Create new post + parameters: + - description: Post data + in: body + name: post + required: true + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + summary: Create post + tags: + - posts + /posts/{id}: + delete: + consumes: + - application/json + description: Delete post + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post' + summary: Delete post + tags: + - posts + get: + consumes: + - application/json + description: Get post by id + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' + summary: Get post by id + tags: + - posts + put: + consumes: + - application/json + description: Update post + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post' + summary: Update post + tags: + - posts swagger: "2.0" diff --git a/backend/go.mod b/backend/go.mod index 170543c..4e7eb31 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -62,6 +62,7 @@ require ( golang.org/x/text v0.33.0 // indirect golang.org/x/tools v0.41.0 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.67.6 // indirect diff --git a/backend/go.sum b/backend/go.sum index 440ec09..da97b44 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -169,6 +169,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/handlers/post_handlers.go b/backend/internal/handlers/post_handlers.go new file mode 100644 index 0000000..d5bf7a7 --- /dev/null +++ b/backend/internal/handlers/post_handlers.go @@ -0,0 +1,104 @@ +package handlers + +import ( + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" + "github.com/gin-gonic/gin" +) + +type PostHandlers struct { + repo repositories.PostRepository +} + +func NewPostHandler(repo repositories.PostRepository) *PostHandlers { + return &PostHandlers{repo: repo} +} + +var log = logger.New(false) + +// GetPosts godoc +// @Summary Get all posts +// @Description Get all posts +// @Tags posts +// @Accept json +// @Produce json +// @Success 200 {object} []storage.PostReq +// @Router /posts [get] +func (h *PostHandlers) GetPosts(c *gin.Context) { + var result []storage.PostReq + result, err := h.repo.GetAll(c.Request.Context()) + if err != nil { + log.Error("error request: " + err.Error()) + c.Status(500) + } + log.Info("200 OK GET /posts") + c.JSON(200, result) +} + +// GetPost godoc +// @Summary Get post by id +// @Description Get post by id +// @Tags posts +// @Accept json +// @Produce json +// @Success 200 {object} storage.PostReq +// @Router /posts/{id} [get] +func GetPost(c *gin.Context) { + log.Info("GetPost") +} + +// CreatePost godoc +// @Summary Create post +// @Description Create new post +// @Tags posts +// @Accept json +// @Produce json +// @Param post body storage.PostCreate true "Post data" +// @Success 200 {object} storage.PostReq +// @Failure 400 {object} gin.H +// @Router /posts [post] +func (h *PostHandlers) CreatePost(c *gin.Context) { + var req storage.PostCreate + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + post := storage.Post{ + Title: req.Title, + Content: req.Content, + } + + if err := h.repo.Create(c.Request.Context(), post); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, post) +} + +// UpdatePost godoc +// @Summary Update post +// @Description Update post +// @Tags posts +// @Accept json +// @Produce json +// @Success 200 {object} storage.Post +// @Router /posts/{id} [put] +func UpdatePost(c *gin.Context) { + log.Info("UpdatePost") +} + +// DeletePost godoc +// @Summary Delete post +// @Description Delete post +// @Tags posts +// @Accept json +// @Produce json +// @Success 200 {object} storage.Post +// @Router /posts/{id} [delete] +func DeletePost(c *gin.Context) { + log.Info("DeletePost") +} diff --git a/backend/internal/handlers/registry_handlers.go b/backend/internal/handlers/registry_handlers.go new file mode 100644 index 0000000..4a8ff7b --- /dev/null +++ b/backend/internal/handlers/registry_handlers.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "database/sql" + + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories" + "github.com/gin-gonic/gin" +) + +func Register(router *gin.Engine, db *sql.DB) { + handler_posts := NewPostHandler(repositories.NewPostRepository(db)) + v1 := router.Group("api/v1") + posts := v1.Group("posts") + { + posts.GET("/", handler_posts.GetPosts) + posts.GET("/:id", GetPost) + posts.POST("/", handler_posts.CreatePost) + posts.PUT("/:id", UpdatePost) + posts.DELETE("/:id", DeletePost) + } +} diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go new file mode 100644 index 0000000..640336d --- /dev/null +++ b/backend/internal/logger/logger.go @@ -0,0 +1,45 @@ +package logger + +import ( + "io" + "log/slog" + "os" + "path/filepath" + + "gopkg.in/natefinch/lumberjack.v2" +) + +type Logger struct { + *slog.Logger +} + +func New(debug bool) *Logger { + logDir := "/var/log/backend" + if err := os.MkdirAll(logDir, 0750); err != nil { + return nil + } + + fileWriter := &lumberjack.Logger{ + Filename: filepath.Join(logDir, "backend.log"), + MaxSize: 500, + MaxBackups: 3, + MaxAge: 28, + Compress: true, + } + + var level slog.Level + if debug { + level = slog.LevelDebug + } else { + level = slog.LevelInfo + } + multiWriter := io.MultiWriter(fileWriter, os.Stdout) + + handler := slog.NewTextHandler(multiWriter, &slog.HandlerOptions{ + Level: level, + }) + + return &Logger{ + Logger: slog.New(handler), + } +} diff --git a/backend/internal/repositories/interface.go b/backend/internal/repositories/interface.go new file mode 100644 index 0000000..c22dc8a --- /dev/null +++ b/backend/internal/repositories/interface.go @@ -0,0 +1,15 @@ +package repositories + +import ( + "context" + + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" +) + +type PostRepository interface { + GetAll(ctx context.Context) ([]storage.PostReq, error) + GetByID(ctx context.Context, id int) (storage.PostReq, error) + Create(ctx context.Context, post storage.Post) error + Update(ctx context.Context, id int, post storage.Post) error + Delete(ctx context.Context, id int) error +} diff --git a/backend/internal/repositories/post_repository.go b/backend/internal/repositories/post_repository.go new file mode 100644 index 0000000..0ee0db3 --- /dev/null +++ b/backend/internal/repositories/post_repository.go @@ -0,0 +1,85 @@ +package repositories + +import ( + "context" + "database/sql" + + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" +) + +type postRepository struct { + db *sql.DB + logger *logger.Logger +} + +func NewPostRepository(db *sql.DB) PostRepository { + return &postRepository{ + db: db, + logger: logger.New(false), + } +} +func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error) { + var result []storage.PostReq + rows, err := p.db.Query("SELECT title content FROM posts") + if err != nil { + p.logger.Error(err.Error()) + return nil, err + } + for rows.Next() { + var title string + var content string + err := rows.Scan(&title, &content) + if err != nil { + p.logger.Error("error scan: " + err.Error()) + } + result = append(result, storage.PostReq{ + Title: title, + Content: content, + }) + } + return result, nil +} + +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) + var title string + var content string + err := row.Scan(&title, &content) + if err != nil { + p.logger.Error("error scan: " + err.Error()) + } + result = storage.PostReq{ + Title: title, + Content: content, + } + return result, nil +} + +func (p *postRepository) Create(ctx context.Context, post storage.Post) error { + query, err := p.db.Exec( + "INSERT INTO posts(title, content) VALUES(?, ?)", + post.Title, + post.Content, + ) + if err != nil { + return err + } + id, err := query.LastInsertId() + if err != nil { + return err + } + p.logger.Info("Post created:", "id", id) + return nil +} + +func (p *postRepository) Update(ctx context.Context, id int, post storage.Post) error { + + return nil +} + +func (p *postRepository) Delete(ctx context.Context, id int) error { + + return nil +} diff --git a/backend/internal/storage/db.go b/backend/internal/storage/db.go new file mode 100644 index 0000000..94c53af --- /dev/null +++ b/backend/internal/storage/db.go @@ -0,0 +1,32 @@ +package storage + +import ( + "database/sql" + "os" + + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" + _ "modernc.org/sqlite" +) + +var db_path = os.Getenv( + "DB_PATH", +) + "?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON" + +func CreateTables(db *sql.DB) error { + logger := logger.New(false) + _, err := db.Exec(Migrations) + if err != nil { + logger.Error(err.Error()) + return err + } + + return nil +} + +func OpenSession() (*sql.DB, error) { + db, err := sql.Open("sqlite", db_path) + if err != nil { + return nil, err + } + return db, nil +} diff --git a/backend/internal/storage/migrations.go b/backend/internal/storage/migrations.go new file mode 100644 index 0000000..5cdf8da --- /dev/null +++ b/backend/internal/storage/migrations.go @@ -0,0 +1,10 @@ +package storage + +const Migrations = ` +CREATE TABLE IF NOT EXISTS posts( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP + ); +` diff --git a/backend/internal/storage/models.go b/backend/internal/storage/models.go new file mode 100644 index 0000000..4023e90 --- /dev/null +++ b/backend/internal/storage/models.go @@ -0,0 +1,18 @@ +package storage + +type Post struct { + ID int `db:"id"` + Title string `db:"title"` + Content string `db:"content"` + CreatedAt string `db:"created_at"` +} + +type PostReq struct { + Title string `json:"title"` + Content string `json:"content"` +} + +type PostCreate struct { + Title string `json:"title"` + Content string `json:"content"` +} diff --git a/frontend/index.html b/frontend/index.html index 9edb463..b8aa7e8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,42 @@ -
- - - -