├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── api ├── docs.go ├── swagger.json └── swagger.yaml ├── cmd ├── cli │ └── main.go └── server │ └── main.go ├── db └── migrations │ ├── 000001_create_posts_table.down.sql │ ├── 000001_create_posts_table.up.sql │ ├── 000002_add_unique_to_posts.down.sql │ ├── 000002_add_unique_to_posts.up.sql │ ├── 000003_create_users_table.down.sql │ ├── 000003_create_users_table.up.sql │ ├── 000004_add_author_id_to_posts.down.sql │ ├── 000004_add_author_id_to_posts.up.sql │ ├── 000005_add_fk_author_id_to_users.down.sql │ ├── 000005_add_fk_author_id_to_users.up.sql │ ├── 000006_create_role_table.down.sql │ ├── 000006_create_role_table.up.sql │ ├── 000007_add_dk_role_id_to_users.down.sql │ └── 000007_add_dk_role_id_to_users.up.sql ├── go.mod ├── mux └── pkg ├── adding ├── post.go └── service.go ├── deleting └── service.go ├── http └── rest │ ├── auth │ └── auth.go │ ├── middleware │ └── middleware.go │ ├── post_handler.go │ ├── router.go │ └── user_handler.go ├── listing ├── post.go └── service.go ├── login ├── service.go └── user.go ├── register ├── service.go └── user.go ├── storage ├── memory │ ├── post.go │ └── repository.go └── postgres │ ├── main.go │ ├── post.go │ ├── user.go │ └── userpolicy.go └── userpolicy └── service.go /.gitignore: -------------------------------------------------------------------------------- 1 | go.sum 2 | build/ 3 | main 4 | .idea/ 5 | bin/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Abylaikhan Zulbukharov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SERVER_FILE=cmd/server/main.go 2 | SERVER_DEST=bin/blog-web 3 | 4 | all: build 5 | 6 | build: 7 | go build -o ${SERVER_DEST} ${SERVER_FILE} 8 | 9 | run: 10 | go build -o ${SERVER_DEST} ${SERVER_FILE} 11 | ${SERVER_DEST} 12 | 13 | # test: 14 | # go test -v ./... 15 | 16 | clean: 17 | rm -f ${SERVER_DEST} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang DDD+HEX demo blog api 2 | 3 | - tactical design 4 | - entities 5 | - an object defined primarily by its identity 6 | - value objects 7 | - immutable 8 | - repositories 9 | - how will communicate with storage 10 | - services 11 | - corresponds application use cases 12 | - example: 13 | ` oauth -> user policy -> adding post` 14 | 15 | context: blogpost
16 | ubiquitous language: author, post, vote, comments, storage
17 | entities: author, post, votes, comments
18 | value objects: it can be part of entity object (author, voter, commenter)
19 | aggregates: PostAuthor, PostVotes, PostComments
20 | - service: 21 | - stateless operations 22 | - post adder / listing, 23 | - vote increment / decrement / count 24 | - comment adder / listing 25 | - author register / login 26 | - events: can affect to the system (errors, logs) 27 | - repository: facade over backend 28 | 29 | ## Goals: good structure goal 30 | 31 | - consistent. 32 | - easy to test. 33 | - easy to understand. 34 | - easy to change. 35 | 36 | ## Project Specs 37 | 38 | - author can add a post. 39 | - author can add a comment for the post. 40 | - author can vote for the post. 41 | - author can list all posts with votes. 42 | - author can list all reviews for the post. 43 | 44 | 45 | ## Dependencies: 46 | - [pgx](https://github.com/jackc/pgx) 47 | 48 | ## Status: 49 | in progress 50 | 51 | ## Run: 52 | ```sh 53 | go build cmd/boilerplate/main.go 54 | ``` 55 | 56 | ## Example 57 | 58 | http://localhost:8000/swagger/index.html 59 | 60 | ```sh 61 | # add new post 62 | curl -X POST "http://localhost:8000/api/post" -H "accept: application/json" -H "Content-Type: application/json" -d '{"content": "hello cruel world"}' 63 | 64 | # get all posts 65 | curl -X GET "http://localhost:8000/api/posts" -H "accept: application/json" 66 | 67 | # migrate 68 | migrate -source file://$PWD/db/migrations -database "postgres://adm:1234@localhost:5432/alem?sslmode=disable" up 69 | 70 | swag init -g cmd/server/main.go --parseDependency --parseInternal -o ./api 71 | ``` 72 | 73 | ## Links 74 | https://khalilstemmler.com/wiki/conways-law/
75 | https://medium.com/@hatajoe/clean-architecture-in-go-4030f11ec1b1
76 | https://khalilstemmler.com/articles/software-design-architecture/organizing-app-logic/
77 | http://olivergierke.de/2020/03/Implementing-DDD-Building-Blocks-in-Java/
78 | https://archfirst.org/domain-driven-design-8-conclusion/
79 | https://herbertograca.com/2017/09/14/ports-adapters-architecture/
80 | https://blog.fedecarg.com/2009/03/11/domain-driven-design-and-mvc-architectures/#:~:text=According%20to%20Eric%20Evans%2C%20Domain,a%20technology%20or%20a%20methodology.&text=Domain%2Ddriven%20design%20separates%20the,to%20retrieve%20and%20store%20data.
81 | http://www.newtonmeters.com/blog/domain-driven-design-in-go/
82 | https://vaadin.com/learn/tutorials/ddd/ddd_and_hexagonal
83 | https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
84 | 85 | ![](https://visitor-badge.laobi.icu/badge?page_id=Zulbukharov.golang-ddd-hex) 86 | -------------------------------------------------------------------------------- /api/docs.go: -------------------------------------------------------------------------------- 1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | 4 | package api 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "strings" 10 | 11 | "github.com/alecthomas/template" 12 | "github.com/swaggo/swag" 13 | ) 14 | 15 | var doc = `{ 16 | "schemes": {{ marshal .Schemes }}, 17 | "swagger": "2.0", 18 | "info": { 19 | "description": "{{.Description}}", 20 | "title": "{{.Title}}", 21 | "contact": { 22 | "name": "Zulbukharov Abylaikhan", 23 | "email": "l1pt0n1905@gmail.com" 24 | }, 25 | "license": { 26 | "name": "MIT License", 27 | "url": "https://github.com/Zulbukharov/golang-ddd-hex/blob/master/LICENSE" 28 | }, 29 | "version": "{{.Version}}" 30 | }, 31 | "host": "{{.Host}}", 32 | "basePath": "{{.BasePath}}", 33 | "paths": { 34 | "/api/posts": { 35 | "get": { 36 | "description": "get posts", 37 | "consumes": [ 38 | "application/json" 39 | ], 40 | "produces": [ 41 | "application/json" 42 | ], 43 | "summary": "List posts", 44 | "parameters": [ 45 | { 46 | "type": "integer", 47 | "description": "Account ID", 48 | "name": "id", 49 | "in": "path", 50 | "required": true 51 | } 52 | ], 53 | "responses": { 54 | "200": { 55 | "description": "OK", 56 | "schema": { 57 | "type": "array", 58 | "items": { 59 | "$ref": "#/definitions/listing.Post" 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "definitions": { 68 | "listing.Post": { 69 | "type": "object", 70 | "properties": { 71 | "author_id": { 72 | "type": "integer" 73 | }, 74 | "content": { 75 | "type": "string" 76 | }, 77 | "id": { 78 | "type": "integer" 79 | } 80 | } 81 | } 82 | } 83 | }` 84 | 85 | type swaggerInfo struct { 86 | Version string 87 | Host string 88 | BasePath string 89 | Schemes []string 90 | Title string 91 | Description string 92 | } 93 | 94 | // SwaggerInfo holds exported Swagger Info so clients can modify it 95 | var SwaggerInfo = swaggerInfo{ 96 | Version: "1.0", 97 | Host: "localhost:8080", 98 | BasePath: "/api", 99 | Schemes: []string{}, 100 | Title: "Blogspot API", 101 | Description: "This is a sample blogspot server.", 102 | } 103 | 104 | type s struct{} 105 | 106 | func (s *s) ReadDoc() string { 107 | sInfo := SwaggerInfo 108 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 109 | 110 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 111 | "marshal": func(v interface{}) string { 112 | a, _ := json.Marshal(v) 113 | return string(a) 114 | }, 115 | }).Parse(doc) 116 | if err != nil { 117 | return doc 118 | } 119 | 120 | var tpl bytes.Buffer 121 | if err := t.Execute(&tpl, sInfo); err != nil { 122 | return doc 123 | } 124 | 125 | return tpl.String() 126 | } 127 | 128 | func init() { 129 | swag.Register(swag.Name, &s{}) 130 | } 131 | -------------------------------------------------------------------------------- /api/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample blogspot server.", 5 | "title": "Blogspot API", 6 | "contact": { 7 | "name": "Zulbukharov Abylaikhan", 8 | "email": "l1pt0n1905@gmail.com" 9 | }, 10 | "license": { 11 | "name": "MIT License", 12 | "url": "https://github.com/Zulbukharov/golang-ddd-hex/blob/master/LICENSE" 13 | }, 14 | "version": "1.0" 15 | }, 16 | "host": "localhost:8080", 17 | "basePath": "/api", 18 | "paths": { 19 | "/api/posts": { 20 | "get": { 21 | "description": "get posts", 22 | "consumes": [ 23 | "application/json" 24 | ], 25 | "produces": [ 26 | "application/json" 27 | ], 28 | "summary": "List posts", 29 | "parameters": [ 30 | { 31 | "type": "integer", 32 | "description": "Account ID", 33 | "name": "id", 34 | "in": "path", 35 | "required": true 36 | } 37 | ], 38 | "responses": { 39 | "200": { 40 | "description": "OK", 41 | "schema": { 42 | "type": "array", 43 | "items": { 44 | "$ref": "#/definitions/listing.Post" 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "definitions": { 53 | "listing.Post": { 54 | "type": "object", 55 | "properties": { 56 | "author_id": { 57 | "type": "integer" 58 | }, 59 | "content": { 60 | "type": "string" 61 | }, 62 | "id": { 63 | "type": "integer" 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /api/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /api 2 | definitions: 3 | listing.Post: 4 | properties: 5 | author_id: 6 | type: integer 7 | content: 8 | type: string 9 | id: 10 | type: integer 11 | type: object 12 | host: localhost:8080 13 | info: 14 | contact: 15 | email: l1pt0n1905@gmail.com 16 | name: Zulbukharov Abylaikhan 17 | description: This is a sample blogspot server. 18 | license: 19 | name: MIT License 20 | url: https://github.com/Zulbukharov/golang-ddd-hex/blob/master/LICENSE 21 | title: Blogspot API 22 | version: "1.0" 23 | paths: 24 | /api/posts: 25 | get: 26 | consumes: 27 | - application/json 28 | description: get posts 29 | parameters: 30 | - description: Account ID 31 | in: path 32 | name: id 33 | required: true 34 | type: integer 35 | produces: 36 | - application/json 37 | responses: 38 | "200": 39 | description: OK 40 | schema: 41 | items: 42 | $ref: '#/definitions/listing.Post' 43 | type: array 44 | summary: List posts 45 | swagger: "2.0" 46 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Zulbukharov/golang-ddd-hex/pkg/storage/memory" 5 | 6 | "github.com/Zulbukharov/golang-ddd-hex/pkg/adding" 7 | ) 8 | 9 | func main() { 10 | s := new(memory.Storage) 11 | adder := adding.NewService(s) 12 | // listing := listing.NewService(s) 13 | 14 | adder.AddPost(adding.Post{Content: "sample1"}) 15 | adder.AddPost(adding.Post{Content: "sample2"}) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Zulbukharov/golang-ddd-hex/pkg/deleting" 6 | auth2 "github.com/Zulbukharov/golang-ddd-hex/pkg/http/rest/auth" 7 | "github.com/Zulbukharov/golang-ddd-hex/pkg/http/rest/middleware" 8 | "github.com/Zulbukharov/golang-ddd-hex/pkg/userpolicy" 9 | "log" 10 | "net/http" 11 | 12 | "github.com/Zulbukharov/golang-ddd-hex/pkg/adding" 13 | "github.com/Zulbukharov/golang-ddd-hex/pkg/http/rest" 14 | "github.com/Zulbukharov/golang-ddd-hex/pkg/listing" 15 | "github.com/Zulbukharov/golang-ddd-hex/pkg/login" 16 | "github.com/Zulbukharov/golang-ddd-hex/pkg/register" 17 | "github.com/Zulbukharov/golang-ddd-hex/pkg/storage/postgres" 18 | ) 19 | 20 | // @title Blogspot API 21 | // @version 1.0 22 | // @description This is a sample blogspot server. 23 | 24 | // @contact.name Zulbukharov Abylaikhan 25 | // @contact.email l1pt0n1905@gmail.com 26 | 27 | // @license.name MIT License 28 | // @license.url https://github.com/Zulbukharov/golang-ddd-hex/blob/master/LICENSE 29 | 30 | // @host localhost:8080 31 | // @BasePath /api 32 | 33 | func main() { 34 | c, err := postgres.NewStorage("localhost", "5432", "adm", "1234", "blog") 35 | if err != nil { 36 | log.Printf("%v", err) 37 | return 38 | } 39 | postRepo := postgres.NewPostRepository(c) 40 | userRepo := postgres.NewUserRepository(c) 41 | userPolicyRepo := postgres.NewUserPolicyRepository(c) 42 | 43 | adder := adding.NewService(postRepo) 44 | listing := listing.NewService(postRepo) 45 | deleting := deleting.NewService(postRepo) 46 | login := login.NewService(userRepo) 47 | register := register.NewService(userRepo) 48 | userPolicy := userpolicy.NewService(userPolicyRepo) 49 | 50 | auth := auth2.NewAuthenticator("ok") 51 | middleware := middleware.NewRules(auth) 52 | 53 | postHandler := rest.NewPostHandler(listing, adder, deleting, userPolicy) 54 | userHandler := rest.NewUserHandler(login, register, auth) 55 | 56 | server := &http.Server{ 57 | Addr: fmt.Sprint(":8000"), 58 | Handler: rest.Route(postHandler, userHandler, middleware), 59 | } 60 | log.Printf("Starting HTTP Server. Listening at %q", server.Addr) 61 | if err := server.ListenAndServe(); err != nil { 62 | log.Printf("%v", err) 63 | } else { 64 | log.Println("Server closed ! ") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /db/migrations/000001_create_posts_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS posts; 2 | 3 | -------------------------------------------------------------------------------- /db/migrations/000001_create_posts_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE posts ( 2 | id SERIAL NOT NULL UNIQUE PRIMARY KEY, 3 | content VARCHAR not NULL 4 | ); 5 | -------------------------------------------------------------------------------- /db/migrations/000002_add_unique_to_posts.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE posts DROP CONSTRAINT posts_unique_content_key; 2 | -------------------------------------------------------------------------------- /db/migrations/000002_add_unique_to_posts.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE posts ADD CONSTRAINT posts_unique_content_key UNIQUE (content); 2 | -------------------------------------------------------------------------------- /db/migrations/000003_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | 3 | -------------------------------------------------------------------------------- /db/migrations/000003_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id SERIAL NOT NULL UNIQUE PRIMARY KEY, 3 | username VARCHAR not NULL UNIQUE, 4 | password VARCHAR not null 5 | ); 6 | -------------------------------------------------------------------------------- /db/migrations/000004_add_author_id_to_posts.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE posts DROP COLUMN IF EXISTS author_id; 2 | -------------------------------------------------------------------------------- /db/migrations/000004_add_author_id_to_posts.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE posts ADD COLUMN author_id INTEGER; 2 | -------------------------------------------------------------------------------- /db/migrations/000005_add_fk_author_id_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE posts DROP CONSTRAINT fk_author_id_to_users; 2 | -------------------------------------------------------------------------------- /db/migrations/000005_add_fk_author_id_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE posts ADD CONSTRAINT fk_author_id_to_users FOREIGN KEY (author_id) REFERENCES users(id); 2 | -------------------------------------------------------------------------------- /db/migrations/000006_create_role_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS roles; 2 | 3 | -------------------------------------------------------------------------------- /db/migrations/000006_create_role_table.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TABLE roles ( 4 | id SERIAL NOT NULL UNIQUE PRIMARY KEY, 5 | role_name VARCHAR not NULL 6 | ); 7 | 8 | INSERT INTO roles(id, role_name) VALUES 9 | (1, 'USER'), 10 | (2, 'AUTHOR'), 11 | (3, 'ADMIN'); 12 | 13 | COMMIT; -------------------------------------------------------------------------------- /db/migrations/000007_add_dk_role_id_to_users.down.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | ALTER TABLE users DROP CONSTRAINT fk_role_id_to_users; 4 | ALTER TABLE users DROP COLUMN IF EXISTS author_id; 5 | 6 | COMMIT; -------------------------------------------------------------------------------- /db/migrations/000007_add_dk_role_id_to_users.up.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | ALTER TABLE users ADD COLUMN role_id INTEGER; 4 | ALTER TABLE users ADD CONSTRAINT fk_role_id_to_users FOREIGN KEY (role_id) REFERENCES roles(id); 5 | 6 | COMMIT; -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Zulbukharov/golang-ddd-hex 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 7 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 9 | github.com/go-openapi/spec v0.19.9 // indirect 10 | github.com/go-openapi/swag v0.19.9 // indirect 11 | github.com/gorilla/mux v1.7.4 12 | github.com/jackc/pgx/v4 v4.8.1 13 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 14 | github.com/kr/pty v1.1.8 // indirect 15 | github.com/lib/pq v1.3.0 16 | github.com/mailru/easyjson v0.7.2 // indirect 17 | github.com/stretchr/objx v0.2.0 // indirect 18 | github.com/swaggo/http-swagger v0.0.0-20200308142732-58ac5e232fba 19 | github.com/swaggo/swag v1.6.7 20 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect 21 | golang.org/x/tools v0.0.0-20200806234136-990129eca547 // indirect 22 | gopkg.in/yaml.v2 v2.3.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /mux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zulbukharov/golang-ddd-hex/88b2b22881cb9ee6773500bf1ee1fd0c4f727419/mux -------------------------------------------------------------------------------- /pkg/adding/post.go: -------------------------------------------------------------------------------- 1 | package adding 2 | 3 | // Post basic post struct 4 | type Post struct { 5 | AuthorID uint `json:"author_id"` 6 | Content string `json:"content"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/adding/service.go: -------------------------------------------------------------------------------- 1 | package adding 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ErrDuplicate defines the error message 8 | var ErrDuplicate = fmt.Errorf("Post already exist") 9 | 10 | // Service provides Post adding operations. 11 | type Service interface { 12 | AddPost(Post) error 13 | } 14 | 15 | // Repository provides access to Post repository. 16 | type Repository interface { 17 | AddPost(Post) error 18 | } 19 | 20 | type service struct { 21 | tR Repository 22 | } 23 | 24 | // NewService creates an adding service with the necessary dependencies 25 | func NewService(r Repository) Service { 26 | return &service{r} 27 | } 28 | 29 | // AddPost adds the given Post to the database 30 | func (s *service) AddPost(u Post) error { 31 | if u.AuthorID == 0 || u.Content == "" { 32 | return fmt.Errorf("invalid input") 33 | } 34 | return s.tR.AddPost(u) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/deleting/service.go: -------------------------------------------------------------------------------- 1 | package deleting 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Service provides Post adding operations. 8 | type Service interface { 9 | DeletePost(uint) error 10 | } 11 | 12 | // Repository provides access to Post repository. 13 | type Repository interface { 14 | DeletePost(uint) error 15 | } 16 | 17 | type service struct { 18 | dR Repository 19 | } 20 | 21 | // NewService creates an adding service with the necessary dependencies 22 | func NewService(r Repository) Service { 23 | return &service{r} 24 | } 25 | 26 | // AddPost adds the given Post to the database 27 | func (s *service) DeletePost(u uint) error { 28 | if u == 0 { 29 | return fmt.Errorf("invalid post id") 30 | } 31 | return s.dR.DeletePost(u) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/http/rest/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dgrijalva/jwt-go" 6 | "time" 7 | ) 8 | 9 | type AppClaims struct { 10 | ID uint `json:"id,omitempty"` 11 | //Roles []string `json:"roles,omitempty"` 12 | jwt.StandardClaims 13 | } 14 | 15 | type Authenticator interface { 16 | ParseToken(token string) (*AppClaims, error) 17 | GenerateToken(id uint) (string, error) 18 | } 19 | 20 | type authenticator struct { 21 | secretKey []byte 22 | } 23 | 24 | func NewAuthenticator(secretKey string) Authenticator { 25 | return &authenticator{[]byte(secretKey)} 26 | } 27 | 28 | func (a authenticator) ParseToken(tokenString string) (*AppClaims, error) { 29 | var claims AppClaims 30 | token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) { 31 | return a.secretKey, nil 32 | }) 33 | if err != nil { 34 | return nil, fmt.Errorf("failed to parse token with claims") 35 | } 36 | 37 | if claim, ok := token.Claims.(*AppClaims); ok && token.Valid { 38 | return claim, nil 39 | } 40 | return nil, fmt.Errorf("invalid token") 41 | } 42 | 43 | func (a authenticator) GenerateToken(id uint) (string, error) { 44 | var claims AppClaims 45 | claims.ID = id 46 | claims.ExpiresAt = time.Now().Add(time.Hour * 72).Unix() 47 | 48 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 49 | return token.SignedString(a.secretKey) 50 | } 51 | 52 | func (a authenticator) Validate(claims AppClaims) error { 53 | if claims.ID == 0 { 54 | return fmt.Errorf("user ID must be set") 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/http/rest/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/Zulbukharov/golang-ddd-hex/pkg/http/rest/auth" 6 | "net/http" 7 | ) 8 | 9 | type Rules interface { 10 | LoggedIn(next http.Handler) http.Handler 11 | } 12 | 13 | type rules struct { 14 | auth auth.Authenticator 15 | } 16 | 17 | func NewRules(auth auth.Authenticator) Rules { 18 | return &rules{auth} 19 | } 20 | 21 | // LoggedIn simple middleware to push value to the context 22 | func (m rules) LoggedIn(next http.Handler) http.Handler { 23 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | credentials, err := r.Cookie("credentials") 25 | if err != nil { 26 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 27 | return 28 | } 29 | 30 | t, err := m.auth.ParseToken(credentials.Value) 31 | if err != nil { 32 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 33 | return 34 | } 35 | ctx := context.WithValue(r.Context(), "credentials", t) 36 | next.ServeHTTP(w, r.WithContext(ctx)) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/http/rest/post_handler.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/Zulbukharov/golang-ddd-hex/pkg/deleting" 6 | "github.com/Zulbukharov/golang-ddd-hex/pkg/http/rest/auth" 7 | "github.com/Zulbukharov/golang-ddd-hex/pkg/userpolicy" 8 | "github.com/gorilla/mux" 9 | "net/http" 10 | "strconv" 11 | 12 | "github.com/Zulbukharov/golang-ddd-hex/pkg/adding" 13 | "github.com/Zulbukharov/golang-ddd-hex/pkg/listing" 14 | ) 15 | 16 | // PostHandler provides access to Post api methods. 17 | type PostHandler interface { 18 | GetPosts(w http.ResponseWriter, r *http.Request) 19 | AddPost(w http.ResponseWriter, r *http.Request) 20 | DeletePost(w http.ResponseWriter, r *http.Request) 21 | EditPost(w http.ResponseWriter, r *http.Request) 22 | // FindPostByID() 23 | // UpdatePost() 24 | // DeletePost() 25 | } 26 | 27 | type postHandler struct { 28 | l listing.Service 29 | a adding.Service 30 | d deleting.Service 31 | u userpolicy.Service 32 | // logger 33 | } 34 | 35 | // NewPostHandler post handler 36 | func NewPostHandler(l listing.Service, a adding.Service, d deleting.Service, u userpolicy.Service) PostHandler { 37 | return &postHandler{ 38 | l: l, 39 | a: a, 40 | d: d, 41 | u: u, 42 | } 43 | } 44 | 45 | // GetPosts godoc 46 | // @Summary List posts 47 | // @Description get posts 48 | // @Accept json 49 | // @Produce json 50 | // @Success 200 {array} listing.Post 51 | // @Router /posts [get] 52 | func (h postHandler) GetPosts(w http.ResponseWriter, r *http.Request) { 53 | w.Header().Set("Content-Type", "application/json") 54 | list, err := h.l.GetAllPosts() 55 | 56 | if err != nil { 57 | http.Error(w, "Failed to get posts", http.StatusBadRequest) 58 | return 59 | } 60 | json.NewEncoder(w).Encode(list) 61 | } 62 | 63 | // AddPost handler for POST /api/post requests 64 | func (h postHandler) AddPost(w http.ResponseWriter, r *http.Request) { 65 | var post adding.Post 66 | 67 | decoder := json.NewDecoder(r.Body) 68 | if err := decoder.Decode(&post); err != nil { 69 | http.Error(w, "Failed to parse post", http.StatusBadRequest) 70 | return 71 | } 72 | 73 | credentials := r.Context().Value("credentials").(*auth.AppClaims) 74 | post.AuthorID = credentials.ID 75 | 76 | if err := h.a.AddPost(post); err != nil { 77 | http.Error(w, "Failed to add post", http.StatusBadRequest) 78 | return 79 | } 80 | 81 | w.Header().Set("Content-Type", "application/json") 82 | json.NewEncoder(w).Encode("New post added.") 83 | } 84 | 85 | func (h postHandler) DeletePost(w http.ResponseWriter, r *http.Request) { 86 | params := mux.Vars(r) 87 | postID, err := strconv.ParseUint(params["id"], 10, 64) 88 | if err != nil || postID == 0 { 89 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 90 | return 91 | } 92 | 93 | credentials := r.Context().Value("credentials").(*auth.AppClaims) 94 | userID := credentials.ID 95 | 96 | if allowed := h.u.IsOwnerOfPost(userID, uint(postID)); allowed == false { 97 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 98 | return 99 | } 100 | if err := h.d.DeletePost(uint(postID)); err != nil { 101 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 102 | return 103 | } 104 | 105 | w.Header().Set("Content-Type", "application/json") 106 | json.NewEncoder(w).Encode("post deleted.") 107 | } 108 | 109 | func (h postHandler) EditPost(w http.ResponseWriter, r *http.Request) { 110 | params := mux.Vars(r) 111 | _, ok := params["id"] 112 | if !ok { 113 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 114 | return 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pkg/http/rest/router.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "encoding/json" 5 | _ "github.com/Zulbukharov/golang-ddd-hex/api" 6 | "github.com/Zulbukharov/golang-ddd-hex/pkg/http/rest/middleware" 7 | "github.com/gorilla/mux" 8 | "github.com/swaggo/http-swagger" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | // Route returns an http handler for the api. 14 | func Route(h PostHandler, u UserHandler, m middleware.Rules) http.Handler { 15 | router := mux.NewRouter() 16 | router.Use(accessControl) 17 | 18 | router.PathPrefix("/swagger/").Handler(httpSwagger.Handler( 19 | httpSwagger.URL("http://localhost:8000/swagger/doc.json"), //The url pointing to API definition 20 | httpSwagger.DeepLinking(true), 21 | httpSwagger.DocExpansion("none"), 22 | httpSwagger.DomID("#swagger-ui"), 23 | )) 24 | 25 | api := router.PathPrefix("/api").Subrouter() 26 | api.HandleFunc("/posts", h.GetPosts).Methods("GET") 27 | //api.Handle("/post/{id}", m.LoggedIn(http.HandlerFunc(h.AddPost))).Methods("GET") 28 | //api.Handle("/post", m.LoggedIn(http.HandlerFunc(h.AddPost))).Methods("POST") 29 | //api.Handle("/post", m.LoggedIn(http.HandlerFunc(h.DeletePost))).Methods("PUT") 30 | //api.Handle("/post/{id}", m.LoggedIn(http.HandlerFunc(h.DeletePost))).Methods("DELETE") 31 | //api.HandleFunc("/login", u.Login).Methods("POST") 32 | //api.HandleFunc("/register", u.Register).Methods("POST") 33 | 34 | http.Handle("/", router) 35 | return router 36 | } 37 | 38 | func accessControl(h http.Handler) http.Handler { 39 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | w.Header().Set("Access-Control-Allow-Origin", "*") 41 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 42 | w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type") 43 | 44 | if r.Method == "OPTIONS" { 45 | return 46 | } 47 | 48 | h.ServeHTTP(w, r) 49 | }) 50 | } 51 | 52 | func respHandler(h func(http.ResponseWriter, *http.Request) (interface{}, int, error)) http.HandlerFunc { 53 | return func(w http.ResponseWriter, r *http.Request) { 54 | data, status, err := h(w, r) 55 | if err != nil { 56 | data = struct { 57 | Error string `json: "error"` 58 | }{ 59 | Error: err.Error(), 60 | } 61 | } 62 | w.Header().Set("Content-Type", "application/json") 63 | w.WriteHeader(status) 64 | err = json.NewEncoder(w).Encode(data) 65 | if err != nil { 66 | log.Printf("could not encode response to output: %v", err) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/http/rest/user_handler.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/Zulbukharov/golang-ddd-hex/pkg/http/rest/auth" 7 | "github.com/Zulbukharov/golang-ddd-hex/pkg/login" 8 | "github.com/Zulbukharov/golang-ddd-hex/pkg/register" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // UserHandler provides access to User api methods. 14 | type UserHandler interface { 15 | Login(w http.ResponseWriter, r *http.Request) 16 | Register(w http.ResponseWriter, r *http.Request) 17 | } 18 | 19 | type userHandler struct { 20 | l login.Service 21 | r register.Service 22 | auth auth.Authenticator 23 | // logger 24 | } 25 | 26 | // NewUserHandler login handler 27 | func NewUserHandler(l login.Service, r register.Service, auth auth.Authenticator) UserHandler { 28 | return &userHandler{l, r, auth} 29 | } 30 | 31 | // Login handler for POST /api/login requests 32 | func (h userHandler) Login(w http.ResponseWriter, r *http.Request) { 33 | var user login.User 34 | 35 | decoder := json.NewDecoder(r.Body) 36 | 37 | err := decoder.Decode(&user) 38 | if err != nil { 39 | http.Error(w, err.Error(), http.StatusBadRequest) 40 | return 41 | } 42 | 43 | var id uint 44 | id, err = h.l.Login(user) 45 | if err != nil { 46 | http.Error(w, err.Error(), http.StatusBadRequest) 47 | return 48 | } 49 | 50 | w.Header().Set("Content-Type", "application/json") 51 | claims, err := h.auth.GenerateToken(id) 52 | http.SetCookie(w, &http.Cookie{ 53 | Name: "credentials", 54 | Value: claims, 55 | Expires: time.Now().Add(72 * time.Hour), 56 | }) 57 | json.NewEncoder(w).Encode(fmt.Sprintf("user logged in %v", claims)) 58 | } 59 | 60 | func (h userHandler) Register(w http.ResponseWriter, r *http.Request) { 61 | var user register.User 62 | 63 | decoder := json.NewDecoder(r.Body) 64 | 65 | err := decoder.Decode(&user) 66 | if err != nil { 67 | http.Error(w, err.Error(), http.StatusBadRequest) 68 | return 69 | } 70 | 71 | id, err := h.r.Register(user) 72 | if err != nil { 73 | http.Error(w, err.Error(), http.StatusBadRequest) 74 | return 75 | } 76 | 77 | w.Header().Set("Content-Type", "application/json") 78 | claims, err := h.auth.GenerateToken(id) 79 | http.SetCookie(w, &http.Cookie{ 80 | Name: "credentials", 81 | Value: claims, 82 | Expires: time.Now().Add(72 * time.Hour), 83 | }) 84 | json.NewEncoder(w).Encode(fmt.Sprintf("user registered %v", claims)) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/listing/post.go: -------------------------------------------------------------------------------- 1 | package listing 2 | 3 | // Post defines the properties of a Post to be listed 4 | type Post struct { 5 | ID uint `json:"id"` 6 | AuthorID uint `json:"author_id"` 7 | Content string `json:"content"` 8 | } 9 | -------------------------------------------------------------------------------- /pkg/listing/service.go: -------------------------------------------------------------------------------- 1 | package listing 2 | 3 | // Service provides Post listing operations. 4 | type Service interface { 5 | GetAllPosts() ([]Post, error) 6 | // FindPostByID(id) 7 | // FindUserPosts(id) 8 | } 9 | 10 | // Repository provides access to Post repository. 11 | type Repository interface { 12 | // GetAllPosts returns all Posts saved in storage. 13 | GetAllPosts() ([]Post, error) 14 | //GetUserPosts(id uint) ([]Post, error) 15 | } 16 | 17 | type service struct { 18 | tR Repository 19 | } 20 | 21 | // NewService creates an list service with the necessary dependencies 22 | func NewService(r Repository) Service { 23 | return &service{r} 24 | } 25 | 26 | // GetAllPosts returns all Posts from the storage 27 | func (s *service) GetAllPosts() ([]Post, error) { 28 | return s.tR.GetAllPosts() 29 | } 30 | 31 | //func (s *service) GetUserPosts(id uint) ([]Post, error) { 32 | // return s.tR.GetUserPosts(id) 33 | //} 34 | -------------------------------------------------------------------------------- /pkg/login/service.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | // Service ... 4 | type Service interface { 5 | Login(User) (uint, error) 6 | } 7 | 8 | // Repository ... 9 | type Repository interface { 10 | Login(User) (uint, error) 11 | } 12 | 13 | type service struct { 14 | lR Repository 15 | } 16 | 17 | // NewService ... 18 | func NewService(r Repository) Service { 19 | return &service{r} 20 | } 21 | 22 | // Login ... 23 | func (s *service) Login(u User) (uint, error) { 24 | // input validation 25 | return s.lR.Login(u) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/login/user.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | type User struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/register/service.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import "fmt" 4 | 5 | // Service ... 6 | type Service interface { 7 | Register(User) (uint, error) 8 | } 9 | 10 | // Repository ... 11 | type Repository interface { 12 | Register(User) (uint, error) 13 | } 14 | 15 | type service struct { 16 | lR Repository 17 | } 18 | 19 | // NewService ... 20 | func NewService(r Repository) Service { 21 | return &service{r} 22 | } 23 | 24 | // Login ... 25 | func (s *service) Register(u User) (uint, error) { 26 | // input validation 27 | if u.Username == "" || u.Password == "" { 28 | return 0, fmt.Errorf("invalid input data") 29 | } 30 | return s.lR.Register(u) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/register/user.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | type User struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/storage/memory/post.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | // Post defines the properties of a Post to be listed 4 | type Post struct { 5 | ID uint `json:"id"` 6 | Content string `json:"content"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/storage/memory/repository.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "github.com/Zulbukharov/golang-ddd-hex/pkg/adding" 5 | "github.com/Zulbukharov/golang-ddd-hex/pkg/listing" 6 | ) 7 | 8 | // Storage Memory keeps data in memory 9 | type Storage struct { 10 | Posts []Post 11 | } 12 | 13 | // AddPost saves the given Post to the repository 14 | func (s *Storage) AddPost(u adding.Post) error { 15 | 16 | for _, e := range s.Posts { 17 | if e.Content == u.Content { 18 | return adding.ErrDuplicate 19 | } 20 | } 21 | 22 | newU := Post{ 23 | ID: uint(len(s.Posts) + 1), 24 | Content: u.Content, 25 | } 26 | s.Posts = append(s.Posts, newU) 27 | return nil 28 | } 29 | 30 | // GetAllPosts returns all Posts from the storage 31 | func (s *Storage) GetAllPosts() ([]listing.Post, error) { 32 | var posts []listing.Post 33 | 34 | for i := range s.Posts { 35 | Post := listing.Post{ 36 | ID: s.Posts[i].ID, 37 | Content: s.Posts[i].Content, 38 | } 39 | 40 | posts = append(posts, Post) 41 | } 42 | 43 | return posts, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/storage/postgres/main.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | _ "github.com/lib/pq" 8 | ) 9 | 10 | // NewStorage returns a new Postgres connection 11 | func NewStorage(host, port, user, password, dbName string) (*sql.DB, error) { 12 | connect := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", 13 | host, port, user, password, dbName) 14 | 15 | db, err := sql.Open("postgres", connect) 16 | if err != nil { 17 | return nil, fmt.Errorf("unable to connect to database: %v", err) 18 | } 19 | 20 | err = db.Ping() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return db, nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/storage/postgres/post.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/Zulbukharov/golang-ddd-hex/pkg/adding" 6 | "github.com/Zulbukharov/golang-ddd-hex/pkg/listing" 7 | ) 8 | 9 | // Post defines the properties of a Post to be listed 10 | type Post struct { 11 | ID uint 12 | Content string 13 | AuthorID uint 14 | } 15 | 16 | // PostRepository keeps data in postgres db 17 | type PostRepository struct { 18 | db *sql.DB 19 | } 20 | 21 | // NewPostRepository return a new repo 22 | func NewPostRepository(db *sql.DB) *PostRepository { 23 | return &PostRepository{db} 24 | } 25 | 26 | // AddPost saves the given Post to the repository 27 | func (s *PostRepository) AddPost(u adding.Post) error { 28 | stmt, err := s.db.Prepare("INSERT INTO posts(content, author_id) VALUES($1, $2);") 29 | if err != nil { 30 | return err 31 | } 32 | defer stmt.Close() 33 | 34 | _, err = stmt.Exec(u.Content, u.AuthorID) 35 | if err != nil { 36 | return adding.ErrDuplicate 37 | } 38 | return nil 39 | } 40 | 41 | // GetAllPosts returns all Posts from the storage 42 | func (s *PostRepository) GetAllPosts() ([]listing.Post, error) { 43 | posts := make([]listing.Post, 0) 44 | 45 | var ( 46 | id uint 47 | content string 48 | authorID uint 49 | ) 50 | 51 | rows, err := s.db.Query("SELECT id, content, author_id FROM posts") 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer rows.Close() 56 | for rows.Next() { 57 | err := rows.Scan(&id, &content, &authorID) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | posts = append(posts, listing.Post{ID: id, Content: content, AuthorID: authorID}) 63 | } 64 | err = rows.Err() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return posts, nil 70 | } 71 | 72 | func (s *PostRepository) DeletePost(id uint) error { 73 | stmt, err := s.db.Prepare("DELETE FROM posts WHERE id = $1;") 74 | if err != nil { 75 | return err 76 | } 77 | defer stmt.Close() 78 | 79 | _, err = stmt.Exec(id) 80 | if err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/storage/postgres/user.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/Zulbukharov/golang-ddd-hex/pkg/login" 6 | "github.com/Zulbukharov/golang-ddd-hex/pkg/register" 7 | ) 8 | 9 | // User defines the properties of a User to be listed 10 | type User struct { 11 | ID uint 12 | Username string 13 | Password string 14 | RoleID uint 15 | } 16 | 17 | // UserRepository keeps data in postgres db 18 | type UserRepository struct { 19 | db *sql.DB 20 | } 21 | 22 | // NewUserRepository returns a new repo 23 | func NewUserRepository(db *sql.DB) *UserRepository { 24 | return &UserRepository{db} 25 | } 26 | 27 | // Login checks the given User 28 | func (s *UserRepository) Login(u login.User) (uint, error) { 29 | stmt, err := s.db.Prepare("SELECT id, username, password FROM users WHERE username = $1 and password = $2") 30 | if err != nil { 31 | return 0, err 32 | } 33 | defer stmt.Close() 34 | 35 | var ( 36 | id uint 37 | username string 38 | password string 39 | ) 40 | err = stmt.QueryRow(u.Username, u.Password).Scan(&id, &username, &password) 41 | if err != nil { 42 | return 0, err 43 | } 44 | return id, err 45 | } 46 | 47 | func (s *UserRepository) Register(u register.User) (uint, error) { 48 | stmt, err := s.db.Prepare("INSERT INTO users(username, password, role_id) VALUES ($1, $2, 1) RETURNING id") 49 | if err != nil { 50 | return 0, err 51 | } 52 | defer stmt.Close() 53 | 54 | var id uint 55 | err = stmt.QueryRow(u.Username, u.Password).Scan(&id) 56 | return id, err 57 | } 58 | -------------------------------------------------------------------------------- /pkg/storage/postgres/userpolicy.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | type UserPolicyRepository struct { 8 | db *sql.DB 9 | } 10 | 11 | // NewUserRepository return a new repo 12 | func NewUserPolicyRepository(db *sql.DB) *UserPolicyRepository { 13 | return &UserPolicyRepository{db} 14 | } 15 | 16 | func (s *UserPolicyRepository) IsOwnerOfPost(userID, postID uint) bool { 17 | stmt, err := s.db.Prepare("SELECT COUNT(1) FROM posts WHERE author_id = $1 and id = $2;") 18 | if err != nil { 19 | return false 20 | } 21 | defer stmt.Close() 22 | 23 | var id uint 24 | err = stmt.QueryRow(userID, postID).Scan(&id) 25 | if id == 0 || err != nil { 26 | return false 27 | } 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /pkg/userpolicy/service.go: -------------------------------------------------------------------------------- 1 | package userpolicy 2 | 3 | // Service ... 4 | type Service interface { 5 | IsOwnerOfPost(userID uint, postID uint) bool 6 | //SetAdminRole(u User) 7 | //SetAuthorRole(u User) 8 | //SetUserRole(u User) 9 | 10 | //CanDeletePost(u User, postID uint) bool 11 | //CanDeleteComment(User) bool 12 | //CanReact(User) bool 13 | } 14 | 15 | type Repository interface { 16 | IsOwnerOfPost(userID uint, postID uint) bool 17 | //CanDeleteComment(User) bool 18 | //CanReact(User) bool 19 | } 20 | 21 | type service struct { 22 | authR Repository 23 | } 24 | 25 | // NewService ... 26 | func NewService(r Repository) Service { 27 | return &service{r} 28 | } 29 | 30 | // CanAddPost ... 31 | func (s *service) IsOwnerOfPost(userID uint, postID uint) bool { 32 | if userID == 0 || postID == 0 { 33 | return false 34 | } 35 | return s.authR.IsOwnerOfPost(userID, postID) 36 | } 37 | --------------------------------------------------------------------------------