Added authorization

This commit is contained in:
KamilM1205 2025-09-25 09:01:00 +04:00
parent c3c3d65d32
commit b96dd39795
50 changed files with 685 additions and 410 deletions

View file

@ -23,6 +23,8 @@ import (
"58team_blog/internal/utils"
"log"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
@ -30,9 +32,14 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"
)
const secret = "58secret"
func main() {
router := gin.Default()
// Setup cookie container
router.Use(sessions.Sessions("session", cookie.NewStore([]byte(secret))))
// Register custom validators
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("password", utils.PasswordValidator)
@ -64,12 +71,15 @@ func main() {
postRepository := repo.CreatePostRepository(d)
userRepository := repo.CreateUserRepository(d)
imagesRepository := repo.CreateImagesRepository(d)
postService := services.CreatePostService(&postRepository)
userService := services.CreateUserService(&userRepository)
imagesService := services.CreateImagesService(&imagesRepository)
interfaces.BindPostAdmin(&postService, g)
interfaces.BindUser(&userService, g)
interfaces.BindPostAdmin(&postService, &userService, g)
interfaces.BindUser(config.AdminName, config.AdminPassword, &userService, g)
interfaces.BindImages(config.ImagesPath, &imagesService, g)
router.Run(":8080")
}

View file

@ -4,7 +4,7 @@ db-password: 1205
db-host: localhost
db-port: 5432
admin_name: muts
admin_pass: 1205
admin_pass: Abc1205
images_path: ./images/
posts_path: ./posts/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,8 @@ basePath: /api/v1
definitions:
requests.CreatePostRequest:
properties:
category:
type: string
content:
minLength: 36
type: string
@ -9,6 +11,10 @@ definitions:
maxLength: 255
minLength: 8
type: string
tags:
items:
type: string
type: array
title:
maxLength: 255
minLength: 8
@ -35,6 +41,20 @@ definitions:
- password
- username
type: object
requests.LoginUserRequest:
properties:
password:
maxLength: 32
minLength: 6
type: string
username:
maxLength: 32
minLength: 3
type: string
required:
- password
- username
type: object
requests.PutPostRequest:
properties:
content:
@ -67,15 +87,36 @@ definitions:
type: object
responses.GetListPostResponseItem:
properties:
category:
type: string
description:
type: string
id:
type: string
tags:
items:
type: string
type: array
title:
type: string
updatedAt:
type: string
userId:
type: string
username:
type: string
type: object
responses.ImageResponse:
properties:
id:
type: string
path:
type: string
type: object
responses.PostResponse:
properties:
category:
type: string
content:
type: string
createdAt:
@ -84,12 +125,18 @@ definitions:
type: string
id:
type: string
tags:
items:
type: string
type: array
title:
type: string
updatedAt:
type: string
userId:
type: string
username:
type: string
type: object
responses.UserResponse:
properties:
@ -112,13 +159,36 @@ info:
title: 58team blog backend
version: "1.0"
paths:
/images/:
post:
description: Upload new image and returns uploaded image json object
parameters:
- description: image file
in: formData
name: file
required: true
type: file
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/responses.ImageResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse'
summary: Upload new image
tags:
- images
/images/{path}:
get:
description: get image by path
delete:
description: Delete image from server by given path
parameters:
- description: Path to image
in: query
name: path
in: path
name: filename
required: true
type: string
produces:
@ -127,7 +197,103 @@ paths:
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/responses.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse'
summary: Delete image by path
tags:
- images
get:
description: get image by path
parameters:
- description: Path to image
in: path
name: path
required: true
type: string
produces:
- application/octet-stream
- application/json
responses:
"200":
description: OK
schema:
type: file
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/responses.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse'
summary: Get an image by path
tags:
- images
/login:
post:
consumes:
- application/json
description: Login user into system
parameters:
- description: User login data
in: body
name: request
required: true
schema:
$ref: '#/definitions/requests.LoginUserRequest'
produces:
- application/json
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/responses.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse'
summary: Login
tags:
- user
/logout:
get:
description: Creates new user in system
produces:
- application/json
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse'
summary: Create new user
tags:
- user
/post:
get:
description: Return first 5 posts

4
go.mod
View file

@ -14,6 +14,7 @@ require (
github.com/creasty/defaults v1.8.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sessions v1.0.4 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.10.1 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
@ -28,6 +29,9 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-migrate/migrate v3.5.4+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect

14
go.sum
View file

@ -5,6 +5,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/appleboy/gin-jwt/v2 v2.10.3 h1:KNcPC+XPRNpuoBh+j+rgs5bQxN+SwG/0tHbIqpRoBGc=
github.com/appleboy/gin-jwt/v2 v2.10.3/go.mod h1:LDUaQ8mF2W6LyXIbd5wqlV2SFebuyYs4RDwqMNgpsp8=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
@ -24,6 +26,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
@ -60,11 +64,19 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
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/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
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=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@ -143,6 +155,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -7,4 +7,6 @@ type CreatePostCommand struct {
Title string
Description string
Content string
Category string
Tags []string
}

View file

@ -1,8 +0,0 @@
package commands
import "github.com/google/uuid"
type CreatePostsCommand struct {
PostId uuid.UUID
UserId uuid.UUID
}

View file

@ -1,7 +0,0 @@
package commands
import "github.com/google/uuid"
type DeletePostsCommand struct {
Id uuid.UUID
}

View file

@ -14,6 +14,8 @@ type PostResult struct {
Content string
CreatedAt time.Time
UpdatedAt time.Time
Category string
Tags []string
}
type PostResultList struct {

View file

@ -1,13 +0,0 @@
package common
import "github.com/google/uuid"
type PostsResult struct {
Id uuid.UUID
UserId uuid.UUID
PostId uuid.UUID
}
type PostsResultList struct {
Result []*PostsResult
}

View file

@ -0,0 +1,15 @@
package errors
type ReadFileError struct {
msg string
}
func NewReadFileError(msg string) *ReadFileError {
return &ReadFileError{
msg: msg,
}
}
func (e *ReadFileError) Error() string {
return "Read file error: " + e.msg
}

View file

@ -1,16 +0,0 @@
package interfaces
import (
"58team_blog/internal/application/commands"
"58team_blog/internal/application/common"
"58team_blog/internal/application/queries"
)
type PostsService interface {
Create(commands.CreatePostsCommand) (*common.PostsResult, error)
FindByUserId(queries.PostsFindByUserIdQuery) (*queries.PostsFindByUserIdResult, error)
FindByPostId(queries.PostsFindByPostIdQuery) (*queries.PostsFindByPostIdResult, error)
FindAllByUserId(queries.PostsFindByUserIdQuery) (*queries.PostsFindAllByUserIdResult, error)
GetAll() (queries.PostsGetAllResult, error)
Delete(commands.DeletePostsCommand) error
}

View file

@ -15,6 +15,8 @@ func CreatePostResultFromEntity(entity *entities.Post) *common.PostResult {
Content: entity.Content,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
Category: entity.Category,
Tags: entity.Tags,
}
}

View file

@ -1,48 +0,0 @@
package mapper
import (
"58team_blog/internal/application/common"
"58team_blog/internal/application/queries"
"58team_blog/internal/domain/entities"
)
func CreatePostsResultFromEntity(entity *entities.Posts) *common.PostsResult {
return &common.PostsResult{
Id: entity.Id,
UserId: entity.UserId,
PostId: entity.PostId,
}
}
func CreatePostsResultListFromEntityList(entity_list []*entities.Posts) *common.PostsResultList {
var result common.PostsResultList
for _, e := range entity_list {
result.Result = append(result.Result, CreatePostsResultFromEntity(e))
}
return &result
}
func CreatePostsFindByUserIdResultFromEntity(entity *entities.Posts) *queries.PostsFindByUserIdResult {
return &queries.PostsFindByUserIdResult{
Result: CreatePostsResultFromEntity(entity),
}
}
func CreatePostsFindByPostIdResultFromEntity(entity *entities.Posts) *queries.PostsFindByPostIdResult {
return &queries.PostsFindByPostIdResult{
Result: CreatePostsResultFromEntity(entity),
}
}
func CreatePostsFindAllByUserIdResultFromEntity(entity_list []*entities.Posts) *queries.PostsFindAllByUserIdResult {
return &queries.PostsFindAllByUserIdResult{
Result: CreatePostsResultListFromEntityList(entity_list),
}
}
func CreatePostsGetAllResultFromEntity(entity_list []*entities.Posts) *queries.PostsGetAllResult {
return &queries.PostsGetAllResult{
Result: CreatePostsResultListFromEntityList(entity_list),
}
}

View file

@ -1,15 +0,0 @@
package queries
import (
"58team_blog/internal/application/common"
"github.com/google/uuid"
)
type PostsFindAllByUserIdQuery struct {
UserId uuid.UUID
}
type PostsFindAllByUserIdResult struct {
Result *common.PostsResultList
}

View file

@ -1,15 +0,0 @@
package queries
import (
"58team_blog/internal/application/common"
"github.com/google/uuid"
)
type PostsFindByPostIdQuery struct {
PostId uuid.UUID
}
type PostsFindByPostIdResult struct {
Result *common.PostsResult
}

View file

@ -1,15 +0,0 @@
package queries
import (
"58team_blog/internal/application/common"
"github.com/google/uuid"
)
type PostsFindByUserIdQuery struct {
UserId uuid.UUID
}
type PostsFindByUserIdResult struct {
Result *common.PostsResult
}

View file

@ -1,7 +0,0 @@
package queries
import "58team_blog/internal/application/common"
type PostsGetAllResult struct {
Result *common.PostsResultList
}

View file

@ -14,7 +14,7 @@ type ImagesService struct {
repo repository.ImagesRepository
}
func NewImagesService(repo repository.ImagesRepository) ImagesService {
func CreateImagesService(repo repository.ImagesRepository) ImagesService {
return ImagesService{
repo: repo,
}

View file

@ -22,7 +22,7 @@ func CreatePostService(repo repository.PostRepository) PostService {
}
func (s *PostService) Create(cmd commands.CreatePostCommand) (*common.PostResult, error) {
entity, err := entities.CreatePost(cmd.UserId, cmd.Title, cmd.Description, cmd.Content)
entity, err := entities.CreatePost(cmd.UserId, cmd.Title, cmd.Description, cmd.Content, cmd.Category, cmd.Tags)
if err != nil {
return nil, errors.NewValidationError("Invalid input data " + err.Error())
}

View file

@ -1,141 +0,0 @@
package services
import (
"58team_blog/internal/application/commands"
"58team_blog/internal/application/common"
"58team_blog/internal/application/mapper"
"58team_blog/internal/application/queries"
"58team_blog/internal/domain/entities"
"58team_blog/internal/domain/repository"
"errors"
"fmt"
)
type PostsService struct {
repo repository.PostsRepository
}
func CreatePostsService(repo repository.PostsRepository) PostsService {
return PostsService{
repo: repo,
}
}
func (s *PostsService) Create(cmd commands.CreatePostsCommand) (*common.PostsResult, error) {
if user, err := s.repo.FindByPostId(cmd.PostId); user != nil {
if err != nil {
return nil, err
}
return nil, errors.New("Posts already exists")
}
entity, err := entities.CreatePosts(cmd.UserId, cmd.PostId)
if err != nil {
return nil, err
}
if err := entity.Validate(); err != nil {
return nil, err
}
result := mapper.CreatePostsResultFromEntity(&entity)
return result, nil
}
func (s *PostsService) FindByUserId(query queries.PostsFindByUserIdQuery) (*queries.PostsFindByUserIdResult, error) {
entity, err := s.repo.FindByUserId(query.UserId)
if err != nil {
return nil, err
}
if entity == nil {
return nil, errors.New("Posts not found")
}
if err := entity.Validate(); err != nil {
return nil, err
}
result := mapper.CreatePostsFindByUserIdResultFromEntity(entity)
return result, nil
}
func (s *PostsService) FindByPostId(query queries.PostsFindByPostIdQuery) (*queries.PostsFindByPostIdResult, error) {
entity, err := s.repo.FindByPostId(query.PostId)
if err != nil {
return nil, err
}
if entity == nil {
return nil, errors.New("Posts not found")
}
if err := entity.Validate(); err != nil {
return nil, err
}
result := mapper.CreatePostsFindByPostIdResultFromEntity(entity)
return result, nil
}
func (s *PostsService) FindAllByUserId(query queries.PostsFindByUserIdQuery) (*queries.PostsFindAllByUserIdResult, error) {
entities, err := s.repo.FindAllByUserId(query.UserId)
if err != nil {
return nil, err
}
if entities == nil {
return nil, fmt.Errorf("No posts owned by user: %s", query.UserId.String())
}
for _, e := range entities {
if err := e.Validate(); err != nil {
return nil, err
}
}
result := mapper.CreatePostsFindAllByUserIdResultFromEntity(entities)
return result, nil
}
func (s *PostsService) GetAll() (*queries.PostsGetAllResult, error) {
entities, err := s.repo.GetAll()
if err != nil {
return nil, err
}
for _, e := range entities {
if err := e.Validate(); err != nil {
return nil, err
}
}
result := mapper.CreatePostsGetAllResultFromEntity(entities)
return result, nil
}
func (s *PostsService) Delete(cmd commands.DeletePostsCommand) error {
entity, err := s.repo.FindById(cmd.Id)
if err != nil {
return err
}
if entity == nil {
return fmt.Errorf("Posts row not found: %s", cmd.Id)
}
if err := entity.Validate(); err != nil {
return err
}
if err := s.repo.Delete(cmd.Id); err != nil {
return err
}
return nil
}

View file

@ -5,6 +5,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/lib/pq"
)
const PostTable = "post"
@ -17,9 +18,11 @@ type Post struct {
Content string `db:"content"`
CreatedAt time.Time `db:"createdat"`
UpdatedAt time.Time `db:"updatedat"`
Category string `db:"category"`
Tags pq.StringArray `db:"tags"` // TODO: rewrite it to many2many
}
func CreatePost(userId uuid.UUID, title string, description string, content string) (post Post, err error) {
func CreatePost(userId uuid.UUID, title string, description string, content string, category string, tags []string) (post Post, err error) {
post = Post{
Id: uuid.New(),
UserId: userId,
@ -28,6 +31,8 @@ func CreatePost(userId uuid.UUID, title string, description string, content stri
Content: content,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Category: category,
Tags: tags,
}
err = post.Validate()

View file

@ -1,43 +0,0 @@
package entities
import (
"errors"
"github.com/google/uuid"
)
const PostsTable = "posts"
type Posts struct {
Id uuid.UUID `db:"id"`
UserId uuid.UUID `db:"user_id"`
PostId uuid.UUID `db:"post_id"`
}
func CreatePosts(userId uuid.UUID, postId uuid.UUID) (posts Posts, err error) {
posts = Posts{
Id: uuid.New(),
UserId: userId,
PostId: postId,
}
err = posts.Validate()
return
}
func (p *Posts) Validate() error {
if err := uuid.Validate(p.Id.String()); err != nil {
return errors.New("Invalid posts.id")
}
if err := uuid.Validate(p.UserId.String()); err != nil {
return errors.New("Invalid posts.userId")
}
if err := uuid.Validate(p.PostId.String()); err != nil {
return errors.New("Invalid posts.postId")
}
return nil
}

View file

@ -1,17 +0,0 @@
package repository
import (
"58team_blog/internal/domain/entities"
"github.com/google/uuid"
)
type PostsRepository interface {
Create(*entities.Posts) (*entities.Posts, error)
FindById(uuid.UUID) (*entities.Posts, error)
FindByPostId(uuid.UUID) (*entities.Posts, error)
FindByUserId(uuid.UUID) (*entities.Posts, error)
FindAllByUserId(uuid.UUID) ([]*entities.Posts, error)
GetAll() ([]*entities.Posts, error)
Delete(uuid.UUID) error
}

View file

@ -0,0 +1,19 @@
package infrastructure
import (
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func AuthRequired(c *gin.Context) {
session := sessions.Default(c)
if user := session.Get("user"); user == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Next()
}

View file

@ -21,8 +21,8 @@ func CreatePostRepository(conn *db.Database) PostRepository {
}
func (r *PostRepository) Create(entity *entities.Post) (*entities.Post, error) {
query := "INSERT INTO " + entities.PostTable + " (id, userid, title, description, content, createdat, updatedat)" +
"VALUES (:id, :userid, :title, :description, :content, :createdat, :updatedat)"
query := "INSERT INTO " + entities.PostTable + " (id, userid, title, description, content, createdat, updatedat, category, tags)" +
"VALUES (:id, :userid, :title, :description, :content, :createdat, :updatedat, :category, :tags)"
_, err := r.conn.Conn.NamedExec(query, entity)
return entity, err
@ -49,9 +49,23 @@ func (r *PostRepository) FindById(id uuid.UUID) (*entities.Post, error) {
func (r *PostRepository) FindAllByUserName(userName string) ([]*entities.Post, error) {
var entity_list []*entities.Post
var id string
user_query := "SELECT id FROM " + entities.UserTable + " WHERE username=?"
user_query, args, err := sqlx.In(user_query, userName)
if err != nil {
return nil, err
}
user_query = r.conn.Conn.Rebind(user_query)
err = r.conn.Conn.Select(&id, user_query, args...)
if err != nil {
return nil, err
}
query := "SELECT * FROM " + entities.PostTable + " WHERE userid=?"
query, args, err := sqlx.In(query, userName)
query, args, err = sqlx.In(query, id)
if err != nil {
return nil, err
}

View file

@ -31,7 +31,7 @@ func (r *UserRepository) Create(entity *entities.User) (*entities.User, error) {
}
func (r *UserRepository) FindById(id uuid.UUID) (*entities.User, error) {
var entity *entities.User
var entity entities.User
query := "SELECT * FROM " + entities.UserTable + " WHERE id=?"
query, arg, err := sqlx.In(query, id)
@ -40,9 +40,9 @@ func (r *UserRepository) FindById(id uuid.UUID) (*entities.User, error) {
}
query = r.conn.Conn.Rebind(query)
err = r.conn.Conn.Select(entity, query, arg...)
err = r.conn.Conn.Get(&entity, query, arg...)
return entity, err
return &entity, err
}
func (r *UserRepository) FindByName(username string) (*entities.User, error) {

View file

@ -1,43 +1,155 @@
package controllers
import (
"58team_blog/internal/application/commands"
"58team_blog/internal/application/errors"
"58team_blog/internal/application/services"
"58team_blog/internal/interfaces/api/mapper"
"58team_blog/internal/interfaces/api/responses"
"58team_blog/internal/utils"
"log"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type ImagesController struct {
images_path string
service *services.ImagesService
}
func CreateImagesController(service *services.ImagesService) ImagesController {
func CreateImagesController(images_path string, service *services.ImagesService) ImagesController {
return ImagesController{
images_path: images_path,
service: service,
}
}
// get /images/{path}
// post /images
// delete /images/{id}
// @Summary Upload new image
// @Description Upload new image and returns uploaded image json object
// @Tags images
// @Produce json
// @Param file formData file true "image file"
// @Success 200 {object} responses.ImageResponse
// @Failure 500 {object} responses.ErrorResponse
// @Router /images/ [post]
func (r *ImagesController) PostImage(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
resp := utils.HandleError(errors.NewNotFoundError("File not found"))
c.JSON(resp.ErrorCode, resp)
return
}
uploadedFile, err := file.Open()
if err != nil {
resp := utils.HandleError(errors.NewReadFileError(err.Error()))
c.JSON(resp.ErrorCode, resp)
return
}
// Read first 512 bytes for detect MIME-type
buffer := make([]byte, 512)
_, err = uploadedFile.Read(buffer)
if err != nil {
resp := utils.HandleError(errors.NewReadFileError(err.Error()))
c.JSON(resp.ErrorCode, resp)
return
}
uploadedFile.Close()
mimeType := http.DetectContentType(buffer)
if !utils.IsImageMime(mimeType) {
resp := utils.HandleError(errors.NewValidationError("Unexpected file type. Expected: jpeg, png, gif, webp, bmp."))
c.JSON(resp.ErrorCode, resp)
return
}
cmd := commands.CreateImageCommand{
Path: uuid.NewString(),
}
image, err := r.service.Create(cmd)
if err != nil {
resp := utils.HandleError(errors.NewValidationError(err.Error()))
c.JSON(resp.ErrorCode, resp)
return
}
c.SaveUploadedFile(file, r.images_path+"/"+image.Path)
resp := mapper.ResponseFromImageResult(image)
c.JSON(http.StatusOK, resp)
}
// @Summary Get an image by path
// @Description get image by path
// @Param path query string true "Path to image"
// @Tags images
// @Param path path string true "Path to image"
// @Produce octet-stream
// @Produce json
// @Success 200 {file} blob
// @Failure 400 {object} responses.ErrorResponse
// @Failure 404 {object} responses.ErrorResponse
// @Failure 500 {object} responses.ErrorResponse
// @Router /images/{path} [get]
func (r *ImagesController) GetImage(c *gin.Context) {
filename := c.Param("path")
filePath := filepath.Join(r.images_path, filename)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
log.Println(err)
resp := responses.CreateErrorResponse(http.StatusNotFound, "Image not found")
c.JSON(resp.ErrorCode, resp)
return
}
file, err := os.Open(filePath)
if err != nil {
log.Println(err)
resp := responses.CreateErrorResponse(http.StatusInternalServerError, "Cannot load image file from server")
c.JSON(resp.ErrorCode, resp)
return
}
mimeData := make([]byte, 512)
if _, err := file.Read(mimeData); err != nil {
log.Println(err)
resp := responses.CreateErrorResponse(http.StatusInternalServerError, "Cannot load image from server")
c.JSON(resp.ErrorCode, resp)
return
}
mimeType, err := utils.GetImageMimeType(mimeData)
if err != nil {
log.Println(err)
resp := responses.CreateErrorResponse(http.StatusInternalServerError, err.Error())
c.JSON(resp.ErrorCode, resp)
return
}
c.Header("Content-Type", mimeType)
c.File(filePath)
}
// @Summary Delete image by path
// @Description Delete image from server by given path
// @Tags images
// @Param filename path string true "Path to image"
// @Produce image/png
// @Produce image/jpeg
// @Success 200
// @Router /images/{path} [get]
func (r *ImagesController) GetImage(c *gin.Context) {
// TODO: return image
panic("Not implemented")
}
func (r *ImagesController) PostImage(c *gin.Context) {
// TODO: return image
panic("Not implemented")
}
// @Failure 400 {object} responses.ErrorResponse
// @Failure 404 {object} responses.ErrorResponse
// @Failure 500 {object} responses.ErrorResponse
// @Router /images/{path} [delete]
func (r *ImagesController) DeleteImage(c *gin.Context) {
// TODO: return image
panic("Not implemented")
}

View file

@ -18,11 +18,13 @@ import (
type PostController struct {
service *services.PostService
userService *services.UserService
}
func CreatePostController(service *services.PostService) PostController {
func CreatePostController(service *services.PostService, userService *services.UserService) PostController {
return PostController{
service: service,
userService: userService,
}
}
@ -61,6 +63,8 @@ func (r *PostController) Post(c *gin.Context) {
Title: request.Title,
Description: request.Description,
Content: request.Content,
Category: request.Category,
Tags: request.Tags,
}
res, err := r.service.Create(cmd)
@ -70,7 +74,16 @@ func (r *PostController) Post(c *gin.Context) {
return
}
// Get username by userid
user, err := r.userService.FindById(queries.UserFindByIdQuery{Id: userId})
if err != nil {
resp := utils.HandleError(err)
c.JSON(resp.ErrorCode, resp)
return
}
response := mapper.ResponseFromPostResult(res)
response.Username = user.Result.UserName
c.JSON(http.StatusCreated, response)
}
@ -94,6 +107,17 @@ func (r *PostController) GetAll(c *gin.Context) {
res := mapper.ResponseFromPostGetAllResult(result)
for e, i := range res {
// Get username by userid
user, err := r.userService.FindById(queries.UserFindByIdQuery{Id: uuid.MustParse(i.UserId)})
if err != nil {
resp := utils.HandleError(err)
c.JSON(resp.ErrorCode, resp)
return
}
res[e].Username = user.Result.UserName
}
c.JSON(http.StatusOK, res)
}
@ -127,6 +151,17 @@ func (r *PostController) GetAllWithOffset(c *gin.Context) {
res := mapper.ResponseFromPostGetAllResult(result)
for e, i := range res {
// Get username by userid
user, err := r.userService.FindById(queries.UserFindByIdQuery{Id: uuid.MustParse(i.UserId)})
if err != nil {
resp := utils.HandleError(err)
c.JSON(resp.ErrorCode, resp)
return
}
res[e].Username = user.Result.UserName
}
c.JSON(http.StatusOK, res)
}
@ -166,6 +201,14 @@ func (r *PostController) GetById(c *gin.Context) {
result := mapper.ResponseFormPostFindByIdResult(posts)
user, err := r.userService.FindById(queries.UserFindByIdQuery{Id: uuid.MustParse(result.UserId)})
if err != nil {
resp := utils.HandleError(err)
c.JSON(resp.ErrorCode, resp)
return
}
result.Username = user.Result.UserName
c.JSON(http.StatusOK, result)
}

View file

@ -11,20 +11,117 @@ import (
"log"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type UserController struct {
adminName string
adminPass string
service *services.UserService
}
func CreateUserController(service *services.UserService) UserController {
func CreateUserController(service *services.UserService, adminName string, adminPass string) UserController {
return UserController{
service: service,
adminName: adminName,
adminPass: adminPass,
}
}
// @Summary Login
// @Description Login user into system
// @Tags user
// @Accept json
// @Produce json
// @Param request body requests.LoginUserRequest true "User login data"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse
// @Failure 401 {object} responses.ErrorResponse
// @Failure 500 {object} responses.ErrorResponse
// @Router /login [post]
func (r *UserController) Login(c *gin.Context) {
session := sessions.Default(c)
var request requests.LoginUserRequest
if err := c.BindJSON(&request); err != nil {
log.Println("User invalid request: ", err)
resp := responses.CreateErrorResponse(http.StatusBadRequest, err.Error())
c.JSON(resp.ErrorCode, resp)
return
}
// Check admin login
if request.Username == r.adminName && request.Password == r.adminPass {
session.Set("user", uuid.NewString())
if err := session.Save(); err != nil {
log.Println("User save session error: ", err)
resp := responses.CreateErrorResponse(http.StatusInternalServerError, "Internal server error")
c.JSON(resp.ErrorCode, resp)
return
}
c.Status(http.StatusOK)
return
}
user, err := r.service.FindByName(queries.UserFindByNameQuery{Name: request.Username})
if err != nil {
resp := utils.HandleError(err)
c.JSON(resp.ErrorCode, resp)
return
}
pass, err := utils.EncryptPassword(request.Password)
if err != nil {
log.Println("User encrypt password error: ", err)
resp := responses.CreateErrorResponse(http.StatusInternalServerError, "Internal server error")
c.JSON(resp.ErrorCode, resp)
return
}
if utils.CheckPassword(user.Result.Password, pass) {
log.Println("Pass ", user.Result.Password, " != ", pass)
resp := responses.CreateErrorResponse(http.StatusUnauthorized, "Authentication error")
c.JSON(resp.ErrorCode, resp)
return
}
session.Set("user", user.Result.Id.String())
if err := session.Save(); err != nil {
log.Println("User save session error: ", err)
resp := responses.CreateErrorResponse(http.StatusInternalServerError, "Internal server error")
c.JSON(resp.ErrorCode, resp)
return
}
c.Status(http.StatusOK)
}
// @Summary Create new user
// @Description Creates new user in system
// @Tags user
// @Produce json
// @Success 200
// @Failure 400 {object} responses.ErrorResponse
// @Failure 500 {object} responses.ErrorResponse
// @Router /logout [get]
func (r *UserController) Logout(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")
if user == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid session token"})
return
}
session.Delete("user")
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
return
}
c.Status(http.StatusOK)
}
// @Summary Create new user
// @Description Creates new user in system
// @Tags user

View file

@ -0,0 +1,13 @@
package mapper
import (
"58team_blog/internal/application/common"
"58team_blog/internal/interfaces/api/responses"
)
func ResponseFromImageResult(result *common.ImageResult) responses.ImageResponse {
return responses.ImageResponse{
Id: result.Id.String(),
Path: result.Path,
}
}

View file

@ -15,5 +15,7 @@ func ResponseFormPostFindByIdResult(result *queries.PostFindByIdResult) response
Content: res.Content,
CreatedAt: res.CreatedAt,
UpdatedAt: res.UpdatedAt,
Tags: res.Tags,
Category: res.Category,
}
}

View file

@ -9,8 +9,12 @@ import (
func itemFromResult(item *common.PostResult) responses.GetListPostResponseItem {
return responses.GetListPostResponseItem{
Id: item.Id.String(),
UserId: item.UserId.String(),
Title: item.Title,
Description: item.Description,
UpdatedAt: item.UpdatedAt.String(),
Tags: item.Tags,
Category: item.Category,
}
}

View file

@ -14,5 +14,7 @@ func ResponseFromPostResult(result *common.PostResult) responses.PostResponse {
Content: result.Content,
CreatedAt: result.CreatedAt,
UpdatedAt: result.UpdatedAt,
Category: result.Category,
Tags: result.Tags,
}
}

View file

@ -5,4 +5,6 @@ type CreatePostRequest struct {
Description string `json:"description" validate:"required,min=8,max=255"`
Content string `json:"content" validate:"required,min=36"`
UserId string `json:"userId" validate:"required,uuid5"`
Category string `json:"category"`
Tags []string `json:"tags"`
}

View file

@ -0,0 +1,6 @@
package requests
type LoginUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=32"`
Password string `json:"password" validate:"required,min=6,max=32,password"`
}

View file

@ -2,8 +2,13 @@ package responses
type GetListPostResponseItem struct {
Id string `json:"id"`
UserId string `json:"userId"`
Title string `json:"title"`
Description string `json:"description"`
UpdatedAt string `json:"updatedAt"`
Tags []string `json:"tags"`
Category string `json:"category"`
Username string `json:"username"`
}
type GetListPostResponse []GetListPostResponseItem

View file

@ -0,0 +1,6 @@
package responses
type ImageResponse struct {
Id string `json:"id"`
Path string `json:"path"`
}

View file

@ -10,6 +10,9 @@ type PostResponse struct {
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Tags []string `json:"tags"`
Category string `json:"category"`
Username string `json:"username"`
}
type PostResponseList []*PostResponse

View file

@ -2,27 +2,41 @@ package interfaces
import (
"58team_blog/internal/application/services"
"58team_blog/internal/infrastructure"
"58team_blog/internal/interfaces/api/controllers"
"github.com/gin-gonic/gin"
)
func BindPostAdmin(service *services.PostService, group *gin.RouterGroup) {
post := controllers.CreatePostController(service)
func BindPostAdmin(service *services.PostService, userService *services.UserService, group *gin.RouterGroup) {
post := controllers.CreatePostController(service, userService)
g := group.Group("/post")
g.GET("/", post.GetAll)
g.GET("/offset/:offset", post.GetAllWithOffset)
g.GET("/:id", post.GetById)
g.Use(infrastructure.AuthRequired)
g.POST("/", post.Post)
g.PUT("/:id", post.Put)
g.DELETE("/:id", post.Delete)
}
func BindUser(service *services.UserService, group *gin.RouterGroup) {
user := controllers.CreateUserController(service)
func BindPost(service *services.PostService, userService *services.UserService, group *gin.RouterGroup) {
post := controllers.CreatePostController(service, userService)
g := group.Group("/post")
g.GET("/", post.GetAll)
g.GET("/offset/:offset", post.GetAllWithOffset)
g.GET("/:id", post.GetById)
}
func BindUser(adminName string, adminPass string, service *services.UserService, group *gin.RouterGroup) {
user := controllers.CreateUserController(service, adminName, adminPass)
group.POST("/login", user.Login)
group.GET("/logout", user.Logout)
g := group.Group("/user/")
g.Use(infrastructure.AuthRequired)
g.POST("/", user.Post)
g.GET("/", user.GetAll)
g.GET("/:id", user.FindById)
@ -30,3 +44,12 @@ func BindUser(service *services.UserService, group *gin.RouterGroup) {
g.PUT("/:id", user.Put)
g.DELETE("/:id", user.Delete)
}
func BindImages(images_path string, service *services.ImagesService, group *gin.RouterGroup) {
images := controllers.CreateImagesController(images_path, service)
g := group.Group("/images/")
g.POST("/", images.PostImage)
g.GET("/:path", images.GetImage)
g.DELETE("/:path", images.DeleteImage)
}

View file

@ -16,6 +16,8 @@ func HandleError(err error) responses.ErrorResponse {
if errors.Is(&ie.ValidationError{}, err) {
errorCode = http.StatusBadRequest
} else if errors.Is(&ie.ReadFileError{}, err) {
errorCode = http.StatusInternalServerError
} else if errors.Is(&ie.NotFoundError{}, err) {
errorCode = http.StatusNotFound
} else if errors.Is(&ie.AlreadyExistsError{}, err) {

View file

@ -0,0 +1,29 @@
package utils
import (
"errors"
"net/http"
)
var allowedTypes = map[string]bool{
"image/jpeg": true,
"image/jpg": true,
"image/png": true,
"image/gif": true,
"image/webp": true,
"image/bmp": true,
}
func IsImageMime(data string) bool {
return allowedTypes[data]
}
func GetImageMimeType(data []byte) (string, error) {
content_type := http.DetectContentType(data)
if !IsImageMime(content_type) {
return "", errors.New("Unexpected image format.")
}
return content_type, nil
}

View file

@ -6,9 +6,10 @@ import (
"golang.org/x/crypto/bcrypt"
)
const salt = "58_team:%s:1205secret"
func EncryptPassword(pass string) (string, error) {
var salted string
salt := "58_team:%s:1205secret"
salted = fmt.Sprintf(salt, pass)
@ -19,3 +20,14 @@ func EncryptPassword(pass string) (string, error) {
return string(hashed), nil
}
func CheckPassword(pass_hashed string, pass string) bool {
salted := fmt.Sprintf(salt, pass)
err := bcrypt.CompareHashAndPassword([]byte(pass_hashed), []byte(salted))
if err != nil {
return false
}
return true
}

View file

@ -0,0 +1,2 @@
ALTER TABLE post DROP COLUMN category;
ALTER TABLE post DROP COLUMN tags;

View file

@ -0,0 +1,2 @@
ALTER TABLE post ADD COLUMN category TEXT;
ALTER TABLE post ADD COLUMN tags TEXT[];

View file

@ -0,0 +1,2 @@
UPDATE post SET category = '' WHERE category = NULL;
UPDATE post SET tags = '{}' WHERE tags = NULL;