├── .gitignore
├── Makefile
├── README.md
├── account-service
├── .env
├── Makefile
├── README.md
├── app
│ ├── app.go
│ └── database
│ │ ├── cache
│ │ └── cache.go
│ │ ├── db
│ │ └── db.go
│ │ └── gorm
│ │ └── gorm.go
├── cmd
│ ├── api
│ │ └── main.go
│ └── migrate
│ │ └── main.go
├── config
│ └── config.go
├── deploy
│ ├── .env
│ ├── Dockerfile
│ ├── bin
│ │ └── init.sh
│ └── prod.Dockerfile
├── doc
│ ├── docs.go
│ ├── swagdto
│ │ └── error.go
│ ├── swagger.json
│ └── swagger.yaml
├── go.mod
├── go.sum
├── locale
│ ├── el-GR
│ │ └── language.yml
│ ├── en-US
│ │ └── language.yml
│ └── zh-CN
│ │ └── language.yml
├── migration
│ ├── 20190805170000_tenant.sql
│ ├── 20210130204915_user_and_role.sql
│ ├── 20210325142152_client.sql
│ └── 20210326132802_role_data.sql
├── module
│ ├── client
│ │ ├── handler.go
│ │ ├── handler_test.go
│ │ ├── inject.go
│ │ ├── mocks
│ │ │ ├── IClientRepo.go
│ │ │ └── IClientService.go
│ │ ├── model
│ │ │ └── client.go
│ │ ├── repo
│ │ │ └── client.go
│ │ ├── routes.go
│ │ ├── service
│ │ │ ├── client_service.go
│ │ │ └── client_service_test.go
│ │ └── swagger
│ │ │ └── client.go
│ ├── module.go
│ ├── tenant
│ │ ├── handler.go
│ │ ├── handler_test.go
│ │ ├── inject.go
│ │ ├── mocks
│ │ │ ├── ITenantRepo.go
│ │ │ └── ITenantService.go
│ │ ├── model
│ │ │ └── tenant.go
│ │ ├── repo
│ │ │ └── tenant.go
│ │ ├── routes.go
│ │ ├── service
│ │ │ ├── tenant_service.go
│ │ │ └── tenant_service_test.go
│ │ └── swagger
│ │ │ └── tenant.go
│ └── user
│ │ ├── handler.go
│ │ ├── handler_test.go
│ │ ├── inject.go
│ │ ├── mocks
│ │ ├── IUserRepo.go
│ │ └── IUserService.go
│ │ ├── model
│ │ ├── token.go
│ │ └── user.go
│ │ ├── repo
│ │ ├── token.go
│ │ └── user.go
│ │ ├── routes.go
│ │ ├── service
│ │ ├── user_service.go
│ │ └── user_service_test.go
│ │ └── swagger
│ │ └── user.go
└── util
│ ├── cache
│ └── redis.go
│ ├── oauth_response.go
│ ├── password
│ └── password.go
│ ├── token
│ └── token.go
│ └── util.go
├── assets
└── golang-monorepo.png
├── docker-compose.yml
├── gateway
├── .env
├── Makefile
├── README.md
├── deploy
│ ├── .env
│ ├── Dockerfile
│ ├── bin
│ │ └── init.sh
│ ├── k8s
│ │ ├── app-configmap.yaml
│ │ ├── app-deployment.yaml
│ │ ├── app-secret.yaml
│ │ └── app-service.yaml
│ └── prod.Dockerfile
├── krakend-out.json
├── krakend.json
├── partials
│ ├── account-host.tmpl
│ ├── product-host.tmpl
│ └── rate-limit-backend.tmpl
├── plugins
│ ├── .gitignore
│ ├── proxy-plugin
│ │ ├── Makefile
│ │ ├── go.mod
│ │ └── plugin.go
│ └── router-plugin
│ │ ├── Makefile
│ │ ├── authorizer.go
│ │ ├── error.go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── plugin.go
│ │ └── public_routes.go
├── settings
│ ├── endpoint.json
│ └── service.json
└── templates
│ └── env.tmpl
└── product-service
├── .env
├── Makefile
├── README.md
├── app
├── app.go
└── database
│ ├── db
│ └── db.go
│ └── gorm
│ └── gorm.go
├── cmd
├── api
│ └── main.go
└── migrate
│ └── main.go
├── config
└── config.go
├── deploy
├── .env
├── Dockerfile
├── bin
│ └── init.sh
└── prod.Dockerfile
├── doc
├── docs.go
├── swagdto
│ └── error.go
├── swagger.json
└── swagger.yaml
├── go.mod
├── go.sum
├── locale
├── el-GR
│ └── language.yml
├── en-US
│ └── language.yml
└── zh-CN
│ └── language.yml
├── migration
└── 20210316142300_create_product.sql
└── module
├── module.go
└── product
├── handler.go
├── handler_test.go
├── inject.go
├── mocks
├── IProductRepo.go
└── IProductService.go
├── model
└── product.go
├── repo
└── product.go
├── routes.go
├── service
├── product_service.go
└── product_service_test.go
└── swagger
└── product.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 | *.log
14 | # Dependency directories (remove the comment below to include it)
15 | vendor/
16 | misc/
17 |
18 | .vscode
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: compose gateway
2 |
3 | compose:
4 | sudo docker-compose up --build --remove-orphans
5 |
6 | gateway:
7 | sudo docker-compose up --build gateway
8 |
9 | account:
10 | sudo docker-compose up --build account-service
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scalable Golang Microservices
2 | ## Requirements
3 |
4 | * Golang - 1.15 or higher recommended
5 | * Mysql - 8.0 or higher
6 | * [Swag cli](https://github.com/swaggo/swag) - For generating swagger docs
7 | * [Mockery](https://github.com/vektra/mockery) - For generating mock classes for testing
8 | * [Docker](https://docs.docker.com/engine/install/) and [Docker-compose](https://docs.docker.com/compose/install/) for the containerized setup - Not mandatory
9 |
10 | Install Swag and Mockery
11 |
12 | ```
13 | go get -u github.com/swaggo/swag/cmd/swag
14 | go get -u github.com/vektra/mockery/cmd/mockery
15 |
16 | ```
17 | ## Featues
18 |
19 | - [x] Multi tenancy support
20 | - [x] Scalable folder structure
21 | - [x] Config using .env
22 | - [x] Database migration
23 | - [x] GORM Integration
24 | - [x] Dependency Injection
25 | - [x] Swagger docs
26 | - [x] Separate handler, service, repository(repo), model
27 | - [x] Multi language support
28 | - [x] Json logger
29 | - [x] Makefile for commands
30 | - [x] Mock object integration
31 | - [x] Unit Test
32 | - [x] Integration Test
33 | - [x] Standard request and response and errors
34 | - [x] Common form validation
35 | - [ ] Health endpoint
36 | - [x] Krakend Gateway integration
37 | - [x] Common error messages
38 | - [x] Docker
39 | - [x] Docker Compose
40 | - [x] Share library across service
41 | - [x] CRUD with pagination support
42 | - [ ] Kubernetes
43 |
44 | ## Overview
45 |
46 | 
47 |
48 | #### Gateway
49 | Which act as a singe entrypoint for all the services
50 | - Handle logs,trace,metrics collection
51 | - Handle Authentication
52 | - Handle ratelimit, Circuit breaker and many more
53 |
54 | #### Common Library
55 | - Common functionality shared accross the microserices , Refer [micro-common](https://github.com/krishnarajvr/micro-common)
56 |
57 | #### Account Service
58 | - Manage tenants, users, authentication and authrorization
59 |
60 | #### Product Service
61 | - Manage product catalog. Demo purpose
62 |
63 |
64 | #### Running Application
65 |
66 | Go to the folders ```./gateway``` ```./account-service``` ```./product-service``` and follow the readme.md files
67 |
68 | ### Eg: Build Account microservice
69 | ```sh
70 | cd account-service
71 | ```
72 |
73 | ### Setup packages locally
74 | ```sh
75 | go mod vendor
76 | ```
77 |
78 | ### Change the config in .env for database and migrate the tables
79 | ```sh
80 | make migrate-up
81 | ```
82 |
83 | ### Generate API document to the ./doc folder using swag cli
84 | ```sh
85 | make doc
86 | ```
87 |
88 | Swagger docs will be available at /swagger/index.html of the api path
89 |
90 | ### Run service
91 | ```sh
92 | make run
93 |
94 | or
95 |
96 | go run cmd/api/main.go
97 | ```
98 |
99 | ### Generate mock files
100 | ```sh
101 | make mock
102 | ```
103 |
104 | ### Test service
105 | ```sh
106 | make test
107 | ```
108 |
109 | ### Swagger
110 | - Access the url http://localhost:8082/swagger/index.html
111 | - or through gateway once krakend is started http://localhost:8080/account/swagger/index.html
112 |
113 | Same steps can be followed for product-service. For gateway steps are different. Please refere readme.md for gateway
114 |
115 | ### Useful go commands
116 | ```sh
117 | go clean --modcache # Clean the mod cache
118 | go mod vendor # Initilize vendor folder locally
119 | go mod download #Download missing packages
120 | ```
121 |
122 | ### Docker Compose setup
123 | If Docker and Docker compose installed. Run below command from root directory
124 |
125 | ```sh
126 | sudo docker-compose up
127 | ```
128 |
129 | ### Create an account that can be used for /adminLogin api for token generation. Refer account service swagger.
130 | ```
131 | curl -X POST "https://localhost:8080/account/v1/tenantRegister" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"domain\": \"eBook\", \"email\": \"tenant1@mail.com\", \"firstName\": \"John\", \"lastName\": \"Doe\", \"name\": \"Tenant1\", \"password\": \"Pass@1\"}"
132 | ```
133 |
134 | Can use the same user email and password for /adminLogin api
135 |
136 | ## Folder Structure
137 | ```sh
138 | ├── app # App Initialization
139 | │ ├── app.go
140 | │ └── database
141 | │ ├── db
142 | │ │ └── db.go
143 | │ └── gorm
144 | │ └── gorm.go
145 | ├── cmd # Starting point for any application
146 | │ ├── api
147 | │ │ └── main.go # Main application start
148 | │ └── migrate
149 | │ └── main.go # Migration start
150 | ├── config
151 | │ └── config.go # App configurations
152 | ├── deploy
153 | │ ├── bin
154 | │ │ └── init.sh
155 | │ ├── Dockerfile
156 | │ └── prod.Dockerfile
157 | ├── doc # Swagger doc - Autogenerated
158 | │ ├── docs.go
159 | │ ├── swagdto
160 | │ │ └── error.go # Common errors used for swagger - Custom
161 | │ ├── swagger.json
162 | │ └── swagger.yaml
163 | ├── go.mod
164 | ├── go.sum
165 | ├── locale # Language files
166 | │ ├── el-GR
167 | │ │ └── language.yml
168 | │ ├── en-US
169 | │ │ └── language.yml
170 | │ └── zh-CN
171 | │ └── language.yml
172 | ├── log
173 | │ ├── micro.log -> micro.log.20210216.log
174 | │ └── micro.log.20210216.log
175 | ├── Makefile
176 | ├── migration # Migration files
177 | │ └── 20210316142300_create_product.sql
178 | ├── module # Application module - Main buisiness logic
179 | │ ├── module.go
180 | │ └── product
181 | │ ├── handler.go
182 | │ ├── handler_test.go
183 | │ ├── inject.go
184 | │ ├── mocks
185 | │ │ ├── IProductRepo.go
186 | │ │ └── IProductService.go
187 | │ ├── model
188 | │ │ └── product.go
189 | │ ├── repo
190 | │ │ └── product.go
191 | │ ├── routes.go
192 | │ ├── service
193 | │ │ ├── product_service.go
194 | │ │ └── product_service_test.go
195 | │ └── swagger
196 | │ └── product.go
197 | ├── README.md
198 | └── util
199 | ```
200 |
--------------------------------------------------------------------------------
/account-service/.env:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 |
3 | SERVER_PORT=8082
4 | SERVER_TIMEOUT_READ=5s
5 | SERVER_TIMEOUT_WRITE=10s
6 | SERVER_TIMEOUT_IDLE=15s
7 |
8 | SERVICE_NAME=Account-Service
9 |
10 | DB_HOST=localhost
11 | DB_PORT=3306
12 | DB_USER=user
13 | DB_PASS=pass
14 | DB_NAME=account_github
15 |
16 | ACCESS_SECRET=jdnf2sdXfksac1
17 | REFRESH_SECRET=mcmvCkmXdnHsdmfdsj3
18 |
19 | API_GATEWAY_URL=localhost:8080
20 | API_GATEWAY_PREFIX=/account/v1
--------------------------------------------------------------------------------
/account-service/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: doc run test mock load-env test-tenant
2 |
3 | GATEWAY_URL =http://localhost:8080
4 | ACCOUNT_PREFIX =/account/v1
5 |
6 | doc:
7 | @echo "---Generate doc files---"
8 | swag init -g module/module.go -o doc
9 |
10 | migrate-create:
11 | @echo "---Creating migration files---"
12 | # another - migrate create -ext sql -dir $(MPATH) -seq -digits 5 $(NAME)
13 | go run cmd/migrate/main.go create $(NAME) sql
14 |
15 | migrate-up:
16 | go run cmd/migrate/main.go up
17 |
18 | migrate-down:
19 | go run cmd/migrate/main.go down
20 |
21 | migrate-force:
22 | go run cmd/migrate/main.go force $(VERSION)
23 |
24 | run:
25 | go run cmd/api/main.go
26 |
27 | load-env:
28 | export $(cat .env | xargs) && echo $(DEBUG)
29 |
30 | test:
31 | go test -v ./module/*/*/
32 |
33 | test-tenant:
34 | go test -v ./module/tenant/service
35 | go test -v ./module/tenant
36 |
37 | test-user:
38 | go test -v ./module/user/service
39 | go test -v ./module/user
40 |
41 | test-client:
42 | go test -v ./module/client/service -cover
43 | go test -v ./module/client -cover
44 |
45 | mock:
46 | mockery --dir=module/tenant/service --name=ITenantService --output=module/tenant/mocks
47 | mockery --dir=module/tenant/repo --name=ITenantRepo --output=module/tenant/mocks
48 | mockery --dir=module/user/service --name=IUserService --output=module/user/mocks
49 | mockery --dir=module/user/repo --name=IUserRepo --output=module/user/mocks
50 | mockery --dir=module/client/service --name=IClientService --output=module/client/mocks
51 | mockery --dir=module/client/repo --name=IClientRepo --output=module/client/mocks
--------------------------------------------------------------------------------
/account-service/README.md:
--------------------------------------------------------------------------------
1 | # Account Service
2 |
3 | ## Requirements
4 |
5 | * Golang - 1.14 recommended
6 | * Mysql - 8.0
7 | * [Swag cli](https://github.com/swaggo/swag) - For generating swagger docs
8 | * [Mockery](https://github.com/vektra/mockery) - For generating mock classes for testing
9 |
10 |
11 | - Note: Once Swag and Mockery installed ```swag``` and ```mockery``` command should work in terminal.
12 | - Tip: can copy the build binary of the package to {home}/go/bin path also works. Also can change the Makefile command path as well.
13 |
14 | ## Steps
15 |
16 | ### Change the config in .env for database and other configuration
17 |
18 |
19 | ### Create a migration file - if required
20 |
21 | ```
22 | make migrate-create NAME=create-user
23 |
24 | OR
25 |
26 | go run cmd/migrate/main.go create create-user sql
27 |
28 | ```
29 |
30 | Modify the migration sql once created in migration folder
31 |
32 |
33 | ```sh
34 | make migrate-up
35 | ```
36 |
37 | ### Generate API document to the ./doc folder using swag cli
38 | ```sh
39 | make doc
40 | ```
41 |
42 | ### Run service
43 | ```sh
44 | make run
45 | ```
46 |
47 | ### Generate mock files
48 | ```sh
49 | make mock
50 | ```
51 |
52 | ### Test service
53 | ```sh
54 | make test
55 | ```
56 |
57 |
--------------------------------------------------------------------------------
/account-service/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "log"
5 | cache "micro/app/database/cache"
6 | lgorm "micro/app/database/gorm"
7 | "micro/config"
8 | utilCache "micro/util/cache"
9 | "os"
10 |
11 | _ "github.com/jinzhu/gorm/dialects/mysql"
12 | "gorm.io/gorm"
13 |
14 | "github.com/gin-gonic/gin"
15 | "github.com/krishnarajvr/micro-common/locale"
16 | "github.com/krishnarajvr/micro-common/middleware"
17 | newrelic "github.com/newrelic/go-agent"
18 | )
19 |
20 | //AppConfig - Application config
21 | type AppConfig struct {
22 | Dbs *Dbs
23 | Lang *locale.Locale
24 | Router *gin.Engine
25 | BaseURL string
26 | Cfg *config.Conf
27 | }
28 |
29 | type Dbs struct {
30 | DB *gorm.DB
31 | Cache *utilCache.RedisClient
32 | }
33 |
34 | func NewrelicMiddleware(appName string, key string) gin.HandlerFunc {
35 | if appName == "" || key == "" {
36 | return func(c *gin.Context) {}
37 | }
38 |
39 | config := newrelic.NewConfig(appName, key)
40 | app, err := newrelic.NewApplication(config)
41 |
42 | if err != nil {
43 | panic(err)
44 | }
45 |
46 | return func(c *gin.Context) {
47 | txn := app.StartTransaction(c.Request.URL.Path, c.Writer, c.Request)
48 | defer txn.End()
49 | c.Next()
50 | }
51 | }
52 |
53 | // InitRouter - Create gin router
54 | func InitRouter(cfg *config.Conf, excludeList map[string]interface{}) (*gin.Engine, error) {
55 | router := gin.Default()
56 | router.Use(middleware.LoggerToFile(cfg.Log.LogFilePath, cfg.Log.LogFileName))
57 |
58 | if len(cfg.App.NewrelicKey) != 0 {
59 | router.Use(NewrelicMiddleware(cfg.App.Name, cfg.App.NewrelicKey))
60 | }
61 |
62 | router.Use(middleware.TenantValidator(excludeList))
63 | router.Use(gin.Recovery())
64 |
65 | return router, nil
66 | }
67 |
68 | // InitLocale - Create locale object
69 | func InitLocale(cfg *config.Conf) (*locale.Locale, error) {
70 | langLocale := locale.Locale{}
71 | dir, err := os.Getwd()
72 |
73 | if err != nil {
74 | log.Print("Not able to get current working director")
75 | panic(err)
76 | }
77 |
78 | lang := langLocale.New(cfg.App.Lang, dir+"/locale/*/*", "en-GR", "en-US", "zh-CN")
79 |
80 | return lang, nil
81 | }
82 |
83 | // InitDS establishes connections to fields in dataSources
84 | func InitDS(config *config.Conf) (*Dbs, error) {
85 | db, err := lgorm.New(config)
86 | redisCache, err := cache.New(config)
87 |
88 | if err != nil {
89 | log.Println("Connection failed")
90 | panic(err)
91 | }
92 |
93 | return &Dbs{
94 | DB: db,
95 | Cache: redisCache,
96 | }, nil
97 | }
98 |
99 | //Close to be used in graceful server shutdown
100 | func Close(d *Dbs) error {
101 | //Todo Check the error
102 | //return d.DB.Close()
103 | return nil
104 | }
105 |
--------------------------------------------------------------------------------
/account-service/app/database/cache/cache.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | redis "github.com/go-redis/redis/v8"
5 |
6 | "micro/config"
7 | "micro/util/cache"
8 | )
9 |
10 | func New(conf *config.Conf) (*cache.RedisClient, error) {
11 | if len(conf.Cache.Host) == 0 {
12 | return nil, nil
13 | }
14 |
15 | rdb := redis.NewClient(&redis.Options{
16 | Addr: conf.Cache.Host,
17 | Password: "", // no password set
18 | DB: 0, // use default DB
19 | })
20 |
21 | redisCache := cache.New(rdb)
22 |
23 | return redisCache, nil
24 | }
25 |
--------------------------------------------------------------------------------
/account-service/app/database/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 |
7 | "github.com/go-sql-driver/mysql"
8 |
9 | "micro/config"
10 | )
11 |
12 | func New(conf *config.Conf) (*sql.DB, error) {
13 | cfg := &mysql.Config{
14 | Net: "tcp",
15 | Addr: fmt.Sprintf("%v:%v", conf.Db.Host, conf.Db.Port),
16 | DBName: conf.Db.DbName,
17 | User: conf.Db.Username,
18 | Passwd: conf.Db.Password,
19 | AllowNativePasswords: true,
20 | ParseTime: true,
21 | }
22 |
23 | return sql.Open("mysql", cfg.FormatDSN())
24 | }
25 |
--------------------------------------------------------------------------------
/account-service/app/database/gorm/gorm.go:
--------------------------------------------------------------------------------
1 | package gorm
2 |
3 | import (
4 | "fmt"
5 |
6 | gosql "github.com/go-sql-driver/mysql"
7 | "gorm.io/driver/mysql"
8 | "gorm.io/gorm"
9 | "gorm.io/gorm/logger"
10 |
11 | "micro/config"
12 | )
13 |
14 | func New(conf *config.Conf) (*gorm.DB, error) {
15 | cfg := &gosql.Config{
16 | Net: "tcp",
17 | Addr: fmt.Sprintf("%v:%v", conf.Db.Host, conf.Db.Port),
18 | DBName: conf.Db.DbName,
19 | User: conf.Db.Username,
20 | Passwd: conf.Db.Password,
21 | AllowNativePasswords: true,
22 | ParseTime: true,
23 | }
24 |
25 | var logLevel logger.LogLevel
26 |
27 | if conf.Debug {
28 | logLevel = logger.Info
29 | } else {
30 | logLevel = logger.Error
31 | }
32 |
33 | return gorm.Open(mysql.Open(cfg.FormatDSN()), &gorm.Config{
34 | Logger: logger.Default.LogMode(logLevel),
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/account-service/cmd/api/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "micro/app"
7 | "micro/config"
8 | "micro/module"
9 | )
10 |
11 | func main() {
12 | cfg := config.AppConfig()
13 |
14 | log.Println("Starting server...")
15 |
16 | // initialize data sources
17 | dbs, err := app.InitDS(cfg)
18 |
19 | if err != nil {
20 | log.Fatalf("Unable to initialize data sources: %v\n", err)
21 | }
22 |
23 | //Close the database connection when stopped
24 | defer app.Close(dbs)
25 |
26 | //Add dependency injection
27 |
28 | //Public routes that don't have tenant checking
29 | excludeList := map[string]interface{}{
30 | "/api/v1/token": true,
31 | "/health": true,
32 | "/api/v1/tenantRegister": true,
33 | "/api/v1/adminLogin": true,
34 | "/api/v1/clientLogin": true,
35 | "/api/v1/rolePermissions": true,
36 | "/api/v1/authorize": true,
37 | "/api/v1/tokenRefresh": true,
38 | "/api/v1/oauth/token": true,
39 | }
40 | router, err := app.InitRouter(cfg, excludeList)
41 |
42 | if err != nil {
43 | log.Fatalf("Unable to initialize routes: %v\n", err)
44 | }
45 |
46 | lang, err := app.InitLocale(cfg)
47 |
48 | if err != nil {
49 | log.Fatalf("Unable to initialize language locale: %v\n", err)
50 | }
51 |
52 | appConfig := app.AppConfig{
53 | Router: router,
54 | BaseURL: cfg.App.BaseURL,
55 | Lang: lang,
56 | Dbs: dbs,
57 | Cfg: cfg,
58 | }
59 |
60 | module.Inject(appConfig)
61 |
62 | if err != nil {
63 | log.Fatalf("Unable to inject dependencies: %v\n", err)
64 | }
65 |
66 | router.Run(":" + cfg.Server.Port)
67 | }
68 |
--------------------------------------------------------------------------------
/account-service/cmd/migrate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | "micro/app/database/db"
10 | "micro/config"
11 |
12 | "github.com/pressly/goose"
13 | )
14 |
15 | const dialect = "mysql"
16 |
17 | var (
18 | flags = flag.NewFlagSet("migrate", flag.ExitOnError)
19 | dir = flags.String("dir", "./migration", "directory with migration files")
20 | )
21 |
22 | func main() {
23 | flags.Usage = usage
24 | flags.Parse(os.Args[1:])
25 |
26 | args := flags.Args()
27 | if len(args) == 0 || args[0] == "-h" || args[0] == "--help" {
28 | flags.Usage()
29 | return
30 | }
31 |
32 | command := args[0]
33 |
34 | switch command {
35 | case "create":
36 | if err := goose.Run("create", nil, *dir, args[1:]...); err != nil {
37 | log.Fatalf("migrate run: %v", err)
38 | }
39 | return
40 | case "fix":
41 | if err := goose.Run("fix", nil, *dir); err != nil {
42 | log.Fatalf("migrate run: %v", err)
43 | }
44 | return
45 | }
46 |
47 | appConf := config.AppConfig()
48 | log.Println("Start migration...")
49 |
50 | // initialize data sources
51 | appDb, err := db.New(appConf)
52 |
53 | if err != nil {
54 | log.Fatalf(err.Error())
55 | }
56 |
57 | defer appDb.Close()
58 |
59 | if err := goose.SetDialect(dialect); err != nil {
60 | log.Fatal(err)
61 | }
62 |
63 | if err := goose.Run(command, appDb, *dir, args[1:]...); err != nil {
64 | log.Fatalf("migrate run: %v", err)
65 | }
66 | }
67 |
68 | func usage() {
69 | fmt.Println(usagePrefix)
70 | flags.PrintDefaults()
71 | fmt.Println(usageCommands)
72 | }
73 |
74 | var (
75 | usagePrefix = `Usage: migrate [OPTIONS] COMMAND
76 | Examples:
77 | migrate status
78 | Options:
79 | `
80 |
81 | usageCommands = `
82 | Commands:
83 | up Migrate the DB to the most recent version available
84 | up-by-one Migrate the DB up by 1
85 | up-to VERSION Migrate the DB to a specific VERSION
86 | down Roll back the version by 1
87 | down-to VERSION Roll back to a specific VERSION
88 | redo Re-run the latest migration
89 | reset Roll back all migrations
90 | status Dump the migration status for the current DB
91 | version Print the current version of the database
92 | create NAME [sql|go] Creates new migration file with the current timestamp
93 | fix Apply sequential ordering to migrations
94 | `
95 | )
96 |
--------------------------------------------------------------------------------
/account-service/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "path"
6 | "path/filepath"
7 | "runtime"
8 | "time"
9 |
10 | "github.com/joeshaw/envdecode"
11 | common "github.com/krishnarajvr/micro-common"
12 | )
13 |
14 | func init() {
15 | common.LoadEnv()
16 | }
17 |
18 | type Conf struct {
19 | Debug bool `env:"DEBUG,required"`
20 | Server serverConf
21 | Db dbConf
22 | Log logConf
23 | App appConf
24 | Gateway gatewayConf
25 | Cache cacheConf
26 | Token tokenConf
27 | }
28 |
29 | type serverConf struct {
30 | Port string `env:"SERVER_PORT,required"`
31 | TimeoutRead time.Duration `env:"SERVER_TIMEOUT_READ,required"`
32 | TimeoutWrite time.Duration `env:"SERVER_TIMEOUT_WRITE,required"`
33 | TimeoutIdle time.Duration `env:"SERVER_TIMEOUT_IDLE,required"`
34 | }
35 |
36 | type logConf struct {
37 | LogFilePath string `env:"Log_FILE_PATH"`
38 | LogFileName string `env:"LOG_FILE_NAME"`
39 | }
40 |
41 | type dbConf struct {
42 | Host string `env:"DB_HOST,required"`
43 | Port int `env:"DB_PORT,required"`
44 | Username string `env:"DB_USER,required"`
45 | Password string `env:"DB_PASS,required"`
46 | DbName string `env:"DB_NAME,required"`
47 | }
48 |
49 | type cacheConf struct {
50 | Host string `env:"REDIS_HOST"`
51 | Username string `env:"REDIS_USER"`
52 | Password string `env:"REDIS_PASS"`
53 | }
54 |
55 | type tokenConf struct {
56 | AccessSecret string `env:"ACCESS_SECRET"`
57 | RefreshSecret string `env:"REFRESH_SECRET"`
58 | AdminAccessExpiry int `env:"ADMIN_ACCESS_EXPIRY"` //In Minutes
59 | AdminRefreshExpiry int `env:"ADMIN_REFRESH_EXPIRY"` //In Minutes
60 | ClientAccessExpiry int `env:"CLIENT_ACCESS_EXPIRY"` //In Minutes
61 | ClientRefreshExpiry int `env:"CLIENT_REFRESH_EXPIRY"` //In Minutes
62 | }
63 |
64 | type appConf struct {
65 | BaseURL string `env:"APP_BASE_URL"`
66 | Lang string `env:"APP_LANG"`
67 | Name string `env:"SERVICE_NAME"`
68 | RootDir string `env:"ROOT_DIR"`
69 | NewrelicKey string `env:"NEWRELIC_KEY"`
70 | }
71 |
72 | type gatewayConf struct {
73 | URL string `env:"API_GATEWAY_URL"`
74 | Prefix string `env:"API_GATEWAY_PREFIX"`
75 | }
76 |
77 | func GetRootDir() string {
78 | _, b, _, _ := runtime.Caller(0)
79 |
80 | d := path.Join(path.Dir(b))
81 | return filepath.Dir(d)
82 | }
83 |
84 | func AppConfig() *Conf {
85 | var c Conf
86 |
87 | if err := envdecode.StrictDecode(&c); err != nil {
88 | log.Fatalf("Failed to decode: %s", err)
89 | }
90 |
91 | if len(c.App.RootDir) <= 0 {
92 | c.App.RootDir = GetRootDir()
93 | }
94 |
95 | if len(c.App.Lang) <= 0 {
96 | c.App.Lang = "en-US"
97 | }
98 |
99 | if len(c.App.BaseURL) <= 0 {
100 | c.App.BaseURL = "api/v1"
101 | }
102 |
103 | if len(c.Log.LogFilePath) <= 0 {
104 | c.Log.LogFilePath = c.App.RootDir + "/log"
105 | }
106 |
107 | if len(c.Log.LogFileName) <= 0 {
108 | c.Log.LogFileName = "micro.log"
109 | }
110 |
111 | if len(c.App.Name) <= 0 {
112 | c.App.Name = "MicroService"
113 | }
114 |
115 | //Access And Refresh Token Expiry in minutes
116 | if c.Token.AdminAccessExpiry == 0 {
117 | c.Token.AdminAccessExpiry = 60 * 2
118 | }
119 |
120 | if c.Token.AdminRefreshExpiry == 0 {
121 | c.Token.AdminRefreshExpiry = 60 * 24 * 2
122 | }
123 |
124 | if c.Token.ClientAccessExpiry == 0 {
125 | c.Token.ClientAccessExpiry = 60 * 3
126 | }
127 |
128 | if c.Token.ClientRefreshExpiry == 0 {
129 | c.Token.ClientRefreshExpiry = 60 * 24 * 3
130 | }
131 |
132 | return &c
133 | }
134 |
--------------------------------------------------------------------------------
/account-service/deploy/.env:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 |
3 | SERVER_PORT=8080
4 | SERVER_TIMEOUT_READ=5s
5 | SERVER_TIMEOUT_WRITE=10s
6 | SERVER_TIMEOUT_IDLE=15s
7 |
8 | DB_HOST=db
9 | DB_PORT=3306
10 | DB_USER=micro_user
11 | DB_PASS=micro_pass
12 | DB_NAME=micro_db
13 | ACCESS_SECRET=jdnf2sdXfksac1
14 | REFRESH_SECRET=mcmvCkmXdnHsdmfdsj3
--------------------------------------------------------------------------------
/account-service/deploy/Dockerfile:
--------------------------------------------------------------------------------
1 | # Development environment
2 | FROM golang:1.15-alpine as build-env
3 | WORKDIR /micro
4 |
5 | RUN apk update && apk add --no-cache gcc musl-dev git
6 |
7 | COPY go.mod go.sum ./
8 | RUN go mod download
9 |
10 | COPY . .
11 |
12 | RUN go build -ldflags '-w -s' -a -o ./bin/api ./cmd/api \
13 | && go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate \
14 | && chmod +x /micro/deploy/bin/*
15 |
16 | EXPOSE 8080
17 |
--------------------------------------------------------------------------------
/account-service/deploy/bin/init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo 'Runing migrations...'
3 | cd micro;./migrate up > /dev/null 2>&1 &
4 |
5 | echo 'Start application...'
6 | /micro/api
7 |
--------------------------------------------------------------------------------
/account-service/deploy/prod.Dockerfile:
--------------------------------------------------------------------------------
1 | # Build environment
2 | # -----------------
3 | FROM golang:1.15-alpine as build-env
4 | WORKDIR /micro
5 |
6 | RUN apk update && apk add --no-cache gcc musl-dev git
7 |
8 | COPY go.mod go.sum ./
9 |
10 | RUN go mod download
11 |
12 | RUN go get -u github.com/swaggo/swag/cmd/swag \
13 | && go get -u github.com/vektra/mockery/cmd/mockery
14 |
15 | RUN pwd
16 |
17 | COPY . .
18 |
19 | RUN go mod vendor \
20 | && swag init -g module/module.go -o doc
21 |
22 | RUN go build -ldflags '-w -s' -a -o ./bin/api ./cmd/api \
23 | && go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate
24 |
25 |
26 | # Deployment environment
27 | # ----------------------
28 | FROM alpine
29 | RUN apk update
30 |
31 | COPY --from=build-env /micro/bin/api /micro/api
32 | COPY --from=build-env /micro/bin/migrate /micro/migrate
33 | COPY --from=build-env /micro/migration /micro/migration
34 | COPY --from=build-env /micro/locale /micro/locale
35 | COPY --from=build-env /micro/deploy/bin/init.sh /micro/bin/init.sh
36 |
37 | RUN chmod +x /micro/bin/*
38 |
39 | EXPOSE 8080
40 |
--------------------------------------------------------------------------------
/account-service/doc/swagdto/error.go:
--------------------------------------------------------------------------------
1 | package swagdto
2 |
3 | type ErrorBadRequest struct {
4 | Code string `json:"code" example:"BAD_REQUEST"`
5 | Message string `json:"message" example:"Bad Request"`
6 | Details []ErrorDetail `json:"details"`
7 | }
8 |
9 | type ErrorUnauthorized struct {
10 | Code string `json:"code" example:"ACCESS_DENIED"`
11 | Message string `json:"message" example:"Unauthorized"`
12 | }
13 |
14 | type ErrorForbidden struct {
15 | Code string `json:"code" example:"ACCESS_DENIED"`
16 | Message string `json:"message" example:"Forbidden"`
17 | }
18 |
19 | type ErrorNotFound struct {
20 | Code string `json:"code" example:"NOT_FOUND"`
21 | Message string `json:"message" example:"Resource not found"`
22 | }
23 |
24 | type ErrorInternalError struct {
25 | Code string `json:"code" example:"INTERNAL_SERVER_ERROR"`
26 | Message string `json:"message" example:"Internal server error"`
27 | }
28 |
29 | type ErrorDetail struct {
30 | Code string `json:"code" example:"Required"`
31 | Target string `json:"target" example:"Name"`
32 | Message string `json:"message" example:"Name field is required"`
33 | }
34 |
35 | type Error400 struct {
36 | Status uint `json:"status" example:"400"`
37 | Error ErrorBadRequest `json:"error"`
38 | Data interface{} `json:"data"`
39 | }
40 |
41 | type Error401 struct {
42 | Status uint `json:"status" example:"401"`
43 | Error ErrorUnauthorized `json:"error"`
44 | Data interface{} `json:"data"`
45 | }
46 |
47 | type Error403 struct {
48 | Status uint `json:"status" example:"403"`
49 | Error ErrorForbidden `json:"error"`
50 | Data interface{} `json:"data"`
51 | }
52 |
53 | type Error404 struct {
54 | Status uint `json:"status" example:"404"`
55 | Error ErrorNotFound `json:"error"`
56 | Data interface{} `json:"data"`
57 | }
58 |
59 | type Error500 struct {
60 | Status uint `json:"status" example:"500"`
61 | Error ErrorInternalError `json:"error"`
62 | Data interface{} `json:"data"`
63 | }
64 |
--------------------------------------------------------------------------------
/account-service/go.mod:
--------------------------------------------------------------------------------
1 | module micro
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/krishnarajvr/micro-common v1.0.0
7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
8 | github.com/astaxie/beego v1.12.3
9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
10 | github.com/gin-gonic/gin v1.6.3
11 | github.com/go-redis/redis/v8 v8.8.2
12 | github.com/go-sql-driver/mysql v1.5.0
13 | github.com/google/uuid v1.2.0
14 | github.com/jinzhu/gorm v1.9.16
15 | github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
16 | github.com/myesui/uuid v1.0.0 // indirect
17 | github.com/newrelic/go-agent v3.11.0+incompatible
18 | github.com/pressly/goose v2.7.0+incompatible
19 | github.com/stretchr/testify v1.7.0
20 | github.com/swaggo/gin-swagger v1.3.0
21 | github.com/swaggo/swag v1.7.0
22 | github.com/twinj/uuid v1.0.0
23 | github.com/unknwon/com v1.0.1
24 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect
25 | golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b
26 | gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
27 | gorm.io/datatypes v1.0.0
28 | gorm.io/driver/mysql v1.0.5
29 | gorm.io/gorm v1.21.3
30 | )
31 |
--------------------------------------------------------------------------------
/account-service/locale/el-GR/language.yml:
--------------------------------------------------------------------------------
1 | hi: "Γειά %s"
2 | intro: "Με λένε {{.Name}} κα είμαι {{.Age}} χρονών"
--------------------------------------------------------------------------------
/account-service/locale/en-US/language.yml:
--------------------------------------------------------------------------------
1 | intro: "My name is {{.Name}} and I am {{.Age}} years old"
2 |
3 | entity_user: "User"
4 | entity_tenant: "Tenant"
5 | entity_token: "Token"
6 | entity_role_permission: "RolePermission"
7 |
8 |
9 | message_not_found: "%s not found"
10 | message_invalid_id: "Invalid %s Id"
11 | message_already_exists: "%s already exists"
12 | message_invalid_data: "Invalid %s"
13 |
14 | message_invalid_client_credentials: "Invalid Client Credentials"
15 |
--------------------------------------------------------------------------------
/account-service/locale/zh-CN/language.yml:
--------------------------------------------------------------------------------
1 | hi: "您好 %s"
2 | intro: "我叫 {{.Name}},今年 {{.Age}} 岁"
--------------------------------------------------------------------------------
/account-service/migration/20190805170000_tenant.sql:
--------------------------------------------------------------------------------
1 | -- +goose Up
2 | -- +goose StatementBegin
3 | CREATE TABLE IF NOT EXISTS tenants
4 | (
5 | id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
6 | name VARCHAR(255) NOT NULL,
7 | code VARCHAR(50) NOT NULL,
8 | domain VARCHAR(20) NOT NULL,
9 | secret VARCHAR(100) NOT NULL COMMENT 'Used for tenant token',
10 | email VARCHAR(255) NOT NULL,
11 | is_active tinyint(1) unsigned DEFAULT '0',
12 | created_at TIMESTAMP NOT NULL,
13 | updated_at TIMESTAMP NULL,
14 | deleted_at TIMESTAMP NULL,
15 | PRIMARY KEY (id),
16 | UNIQUE KEY `unique_code` (`code`),
17 | UNIQUE KEY `unique_name` (`name`),
18 | UNIQUE KEY `unique_email` (`email`)
19 | )ENGINE=InnoDB DEFAULT CHARSET=utf8;
20 | -- +goose StatementEnd
21 |
22 | -- +goose StatementBegin
23 | CREATE TABLE tenant_settings (
24 | id BIGINT UNSIGNED NOT NULL,
25 | tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
26 | settings JSON NOT NULL,
27 | type VARCHAR(20) NOT NULL DEFAULT 'SYSTEM' COMMENT 'Settings type SYSTEM, ACCOUNT, NOTIFICATION etc',
28 | level TINYINT NOT NULL DEFAULT 0 COMMENT 'Level of the seetings 0: Core, 1: Domain, 2: Instance',
29 | created_at TIMESTAMP NOT NULL,
30 | updated_at TIMESTAMP NULL,
31 | deleted_at TIMESTAMP NULL,
32 | PRIMARY KEY (id),
33 | UNIQUE KEY `unique_tenant_id_type_level` (`tenant_id`,`type`,`level`)
34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
35 | -- +goose StatementEnd
36 |
37 | -- +goose Down
38 | -- +goose StatementBegin
39 | SELECT 'down SQL query';
40 | -- +goose StatementEnd
41 |
--------------------------------------------------------------------------------
/account-service/migration/20210130204915_user_and_role.sql:
--------------------------------------------------------------------------------
1 | -- +goose Up
2 | -- +goose StatementBegin
3 | CREATE TABLE IF NOT EXISTS roles
4 | (
5 | id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
6 | tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
7 | name VARCHAR(255) NOT NULL,
8 | code VARCHAR(255) NOT NULL,
9 | created_at TIMESTAMP NOT NULL,
10 | updated_at TIMESTAMP NULL,
11 | deleted_at TIMESTAMP NULL,
12 | PRIMARY KEY (id),
13 | UNIQUE KEY `unique_tenant_id_name` (`tenant_id`,`name`),
14 | UNIQUE KEY `unique_tenant_id_code` (`tenant_id`,`code`)
15 | );
16 | -- +goose StatementEnd
17 | -- +goose StatementBegin
18 | CREATE TABLE IF NOT EXISTS users
19 | (
20 | id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
21 | tenant_id BIGINT UNSIGNED NOT NULL,
22 | name VARCHAR(255) NOT NULL,
23 | email VARCHAR(255) NOT NULL,
24 | first_name VARCHAR(255) NULL,
25 | last_name VARCHAR(255) NULL,
26 | is_active TINYINT(1) UNSIGNED DEFAULT 0 ,
27 | is_root TINYINT(1) UNSIGNED DEFAULT 0 COMMENT 'First User, Cannot delete',
28 | password VARCHAR(255) DEFAULT NULL,
29 | login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
30 | login_count INT DEFAULT 0,
31 | created_user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
32 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
33 | updated_at TIMESTAMP NULL,
34 | deleted_at TIMESTAMP NULL,
35 | PRIMARY KEY (id),
36 | UNIQUE KEY `unique_tenant_id_name` (`tenant_id`,`name`),
37 | UNIQUE KEY `unique_tenant_id_email` (`tenant_id`,`email`)
38 | );
39 | -- +goose StatementEnd
40 | -- +goose StatementBegin
41 | CREATE TABLE IF NOT EXISTS user_audit_log
42 | (
43 | id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
44 | tenant_id BIGINT UNSIGNED NOT NULL,
45 | user_id BIGINT UNSIGNED NOT NULL,
46 | audit_data JSON NOT NULL,
47 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
48 | PRIMARY KEY (id)
49 | )
50 | -- +goose StatementEnd
51 | -- +goose StatementBegin
52 | CREATE TABLE IF NOT EXISTS user_roles
53 | (
54 | id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
55 | user_id BIGINT UNSIGNED NOT NULL,
56 | role_id BIGINT UNSIGNED NOT NULL,
57 | PRIMARY KEY (id),
58 | UNIQUE KEY `unique_user_id_role_id` (`user_id`,`role_id`)
59 | );
60 |
61 | -- +goose StatementEnd
62 |
63 | -- +goose Down
64 | -- +goose StatementBegin
65 | SELECT 'down SQL query';
66 | -- +goose StatementEnd
67 |
--------------------------------------------------------------------------------
/account-service/migration/20210325142152_client.sql:
--------------------------------------------------------------------------------
1 | -- +goose Up
2 | -- +goose StatementBegin
3 | CREATE TABLE `client_credentials` (
4 | `id` bigint unsigned NOT NULL AUTO_INCREMENT,
5 | `tenant_id` bigint unsigned NOT NULL,
6 | `name` varchar(255) DEFAULT NULL,
7 | `code` varchar(100) NOT NULL COMMENT 'AKA client_id',
8 | `secret` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
9 | `reference_id` varchar(100) NOT NULL COMMENT 'Reference id of vendor, tenant, user',
10 | `type` varchar(20) DEFAULT NULL COMMENT 'Possible values are vendor, tenant, user',
11 | `payload` json DEFAULT NULL COMMENT 'Extra payload used for token and other purpose',
12 | `is_active` tinyint unsigned DEFAULT '0',
13 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
14 | `updated_at` timestamp NULL DEFAULT NULL,
15 | `deleted_at` timestamp NULL DEFAULT NULL,
16 | PRIMARY KEY (`id`),
17 | UNIQUE KEY `unique_tenant_id_reference_id_code` (`tenant_id`,`reference_id`,`code`)
18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
19 | -- +goose StatementEnd
20 |
21 | -- +goose StatementBegin
22 | CREATE TABLE `client_credential_roles` (
23 | `id` bigint unsigned NOT NULL AUTO_INCREMENT,
24 | `client_credential_id` bigint unsigned NOT NULL,
25 | `role_id` bigint unsigned NOT NULL,
26 | PRIMARY KEY (`id`),
27 | UNIQUE KEY `unique_client_credential_id_role_id` (`client_credential_id`,`role_id`)
28 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
29 | -- +goose StatementEnd
30 |
31 |
32 | -- +goose StatementBegin
33 | ALTER TABLE `client_credential_roles`
34 | ADD KEY `client_credential_roles_client_credential_cc_id_idx` (`client_credential_id`),
35 | ADD KEY `client_credential_roles_role_id_idx` (`role_id`);
36 | -- +goose StatementEnd
37 |
38 | -- +goose StatementBegin
39 | ALTER TABLE `client_credential_roles`
40 | ADD CONSTRAINT `client_credential_roles_client_credential_cc_id_fk` FOREIGN KEY (`client_credential_id`) REFERENCES `client_credentials` (`id`),
41 | ADD CONSTRAINT `client_credential_roles_roles_role_id_fk` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);
42 | -- +goose StatementEnd
43 |
44 |
45 | -- +goose Down
46 | -- +goose StatementBegin
47 | SELECT 'down SQL query';
48 | -- +goose StatementEnd
49 |
--------------------------------------------------------------------------------
/account-service/migration/20210326132802_role_data.sql:
--------------------------------------------------------------------------------
1 | -- +goose Up
2 | -- +goose StatementBegin
3 | ALTER TABLE `roles`
4 | ADD `level` SMALLINT NOT NULL AFTER `code`;
5 | -- +goose StatementEnd
6 |
7 | -- +goose StatementBegin
8 | INSERT INTO `roles`
9 | (
10 | `id`,
11 | `tenant_id`,
12 | `name`,
13 | `code`,
14 | `level`,
15 | `created_at`,
16 | `updated_at`,
17 | `deleted_at`
18 | ) VALUES
19 | (1, 0, 'Super Admin', 'SUPER_ADMIN', 0, '2021-03-26 07:54:55', NULL, NULL),
20 | (2, 0, 'Tenant Admin', 'ADMIN', 1, '2021-03-26 07:54:55', NULL, NULL),
21 | (3, 0, 'Tenant Admin View', 'ADMIN_VIEW', 1, '2021-03-26 08:00:52', NULL, NULL),
22 | (4, 0, 'Vendor', 'VENDOR', 2, '2021-03-26 08:00:09', NULL, NULL),
23 | (5, 0, 'End User', 'USER', 3, '2021-03-26 08:02:43', NULL, NULL);
24 | -- +goose StatementEnd
25 |
26 | -- +goose Down
27 | -- +goose StatementBegin
28 | SELECT 'down SQL query';
29 | -- +goose StatementEnd
30 |
--------------------------------------------------------------------------------
/account-service/module/client/inject.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "micro/app"
5 | "micro/module/client/repo"
6 | "micro/module/client/service"
7 | userRepo "micro/module/user/repo"
8 | "micro/util/token"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/krishnarajvr/micro-common/locale"
12 | )
13 |
14 | type HandlerConfig struct {
15 | R *gin.Engine
16 | ClientService service.IClientService
17 | BaseURL string
18 | Lang *locale.Locale
19 | }
20 |
21 | //Inject dependencies
22 | func Inject(appConfig app.AppConfig) {
23 | clientRepo := repo.NewClientRepo(appConfig.Dbs.DB)
24 | tokenRepo := userRepo.NewTokenRepo(appConfig.Dbs.DB)
25 |
26 | jwtToken := token.New(token.TokenConfig{
27 | TokenRepo: tokenRepo,
28 | Cache: appConfig.Dbs.Cache,
29 | Config: appConfig.Cfg,
30 | })
31 |
32 | clientService := service.NewService(service.ServiceConfig{
33 | ClientRepo: clientRepo,
34 | Lang: appConfig.Lang,
35 | Token: jwtToken,
36 | })
37 |
38 | InitRoutes(HandlerConfig{
39 | R: appConfig.Router,
40 | ClientService: clientService,
41 | BaseURL: appConfig.BaseURL,
42 | Lang: appConfig.Lang,
43 | })
44 | }
45 |
--------------------------------------------------------------------------------
/account-service/module/client/mocks/IClientRepo.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | common "github.com/krishnarajvr/micro-common"
7 | mock "github.com/stretchr/testify/mock"
8 |
9 | model "micro/module/client/model"
10 | )
11 |
12 | // IClientRepo is an autogenerated mock type for the IClientRepo type
13 | type IClientRepo struct {
14 | mock.Mock
15 | }
16 |
17 | // AddCredential provides a mock function with given fields: form
18 | func (_m *IClientRepo) AddCredential(form *model.ClientCredential) (*model.ClientCredential, error) {
19 | ret := _m.Called(form)
20 |
21 | var r0 *model.ClientCredential
22 | if rf, ok := ret.Get(0).(func(*model.ClientCredential) *model.ClientCredential); ok {
23 | r0 = rf(form)
24 | } else {
25 | if ret.Get(0) != nil {
26 | r0 = ret.Get(0).(*model.ClientCredential)
27 | }
28 | }
29 |
30 | var r1 error
31 | if rf, ok := ret.Get(1).(func(*model.ClientCredential) error); ok {
32 | r1 = rf(form)
33 | } else {
34 | r1 = ret.Error(1)
35 | }
36 |
37 | return r0, r1
38 | }
39 |
40 | // CheckCredentials provides a mock function with given fields: form
41 | func (_m *IClientRepo) CheckCredentials(form *model.ClientLoginForm) (*model.ClientCredential, error) {
42 | ret := _m.Called(form)
43 |
44 | var r0 *model.ClientCredential
45 | if rf, ok := ret.Get(0).(func(*model.ClientLoginForm) *model.ClientCredential); ok {
46 | r0 = rf(form)
47 | } else {
48 | if ret.Get(0) != nil {
49 | r0 = ret.Get(0).(*model.ClientCredential)
50 | }
51 | }
52 |
53 | var r1 error
54 | if rf, ok := ret.Get(1).(func(*model.ClientLoginForm) error); ok {
55 | r1 = rf(form)
56 | } else {
57 | r1 = ret.Error(1)
58 | }
59 |
60 | return r0, r1
61 | }
62 |
63 | // GetClientCredentialRoles provides a mock function with given fields: clientCredentialId
64 | func (_m *IClientRepo) GetClientCredentialRoles(clientCredentialId int) ([]string, error) {
65 | ret := _m.Called(clientCredentialId)
66 |
67 | var r0 []string
68 | if rf, ok := ret.Get(0).(func(int) []string); ok {
69 | r0 = rf(clientCredentialId)
70 | } else {
71 | if ret.Get(0) != nil {
72 | r0 = ret.Get(0).([]string)
73 | }
74 | }
75 |
76 | var r1 error
77 | if rf, ok := ret.Get(1).(func(int) error); ok {
78 | r1 = rf(clientCredentialId)
79 | } else {
80 | r1 = ret.Error(1)
81 | }
82 |
83 | return r0, r1
84 | }
85 |
86 | // GetCredential provides a mock function with given fields: id
87 | func (_m *IClientRepo) GetCredential(id int) (*model.ClientCredential, error) {
88 | ret := _m.Called(id)
89 |
90 | var r0 *model.ClientCredential
91 | if rf, ok := ret.Get(0).(func(int) *model.ClientCredential); ok {
92 | r0 = rf(id)
93 | } else {
94 | if ret.Get(0) != nil {
95 | r0 = ret.Get(0).(*model.ClientCredential)
96 | }
97 | }
98 |
99 | var r1 error
100 | if rf, ok := ret.Get(1).(func(int) error); ok {
101 | r1 = rf(id)
102 | } else {
103 | r1 = ret.Error(1)
104 | }
105 |
106 | return r0, r1
107 | }
108 |
109 | // ListCredentials provides a mock function with given fields: page
110 | func (_m *IClientRepo) ListCredentials(page common.Pagination) (model.ClientCredentials, *common.PageResult, error) {
111 | ret := _m.Called(page)
112 |
113 | var r0 model.ClientCredentials
114 | if rf, ok := ret.Get(0).(func(common.Pagination) model.ClientCredentials); ok {
115 | r0 = rf(page)
116 | } else {
117 | if ret.Get(0) != nil {
118 | r0 = ret.Get(0).(model.ClientCredentials)
119 | }
120 | }
121 |
122 | var r1 *common.PageResult
123 | if rf, ok := ret.Get(1).(func(common.Pagination) *common.PageResult); ok {
124 | r1 = rf(page)
125 | } else {
126 | if ret.Get(1) != nil {
127 | r1 = ret.Get(1).(*common.PageResult)
128 | }
129 | }
130 |
131 | var r2 error
132 | if rf, ok := ret.Get(2).(func(common.Pagination) error); ok {
133 | r2 = rf(page)
134 | } else {
135 | r2 = ret.Error(2)
136 | }
137 |
138 | return r0, r1, r2
139 | }
140 |
141 | // PatchCredential provides a mock function with given fields: form, id
142 | func (_m *IClientRepo) PatchCredential(form *model.ClientCredentialPatchForm, id int) (*model.ClientCredential, error) {
143 | ret := _m.Called(form, id)
144 |
145 | var r0 *model.ClientCredential
146 | if rf, ok := ret.Get(0).(func(*model.ClientCredentialPatchForm, int) *model.ClientCredential); ok {
147 | r0 = rf(form, id)
148 | } else {
149 | if ret.Get(0) != nil {
150 | r0 = ret.Get(0).(*model.ClientCredential)
151 | }
152 | }
153 |
154 | var r1 error
155 | if rf, ok := ret.Get(1).(func(*model.ClientCredentialPatchForm, int) error); ok {
156 | r1 = rf(form, id)
157 | } else {
158 | r1 = ret.Error(1)
159 | }
160 |
161 | return r0, r1
162 | }
163 |
--------------------------------------------------------------------------------
/account-service/module/client/mocks/IClientService.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | common "github.com/krishnarajvr/micro-common"
7 | mock "github.com/stretchr/testify/mock"
8 |
9 | model "micro/module/client/model"
10 |
11 | token "micro/util/token"
12 | )
13 |
14 | // IClientService is an autogenerated mock type for the IClientService type
15 | type IClientService struct {
16 | mock.Mock
17 | }
18 |
19 | // AddCredential provides a mock function with given fields: content, tenantId
20 | func (_m *IClientService) AddCredential(content *model.ClientCredentialForm, tenantId int) (*model.ClientCredentialDto, error) {
21 | ret := _m.Called(content, tenantId)
22 |
23 | var r0 *model.ClientCredentialDto
24 | if rf, ok := ret.Get(0).(func(*model.ClientCredentialForm, int) *model.ClientCredentialDto); ok {
25 | r0 = rf(content, tenantId)
26 | } else {
27 | if ret.Get(0) != nil {
28 | r0 = ret.Get(0).(*model.ClientCredentialDto)
29 | }
30 | }
31 |
32 | var r1 error
33 | if rf, ok := ret.Get(1).(func(*model.ClientCredentialForm, int) error); ok {
34 | r1 = rf(content, tenantId)
35 | } else {
36 | r1 = ret.Error(1)
37 | }
38 |
39 | return r0, r1
40 | }
41 |
42 | // CheckCredentials provides a mock function with given fields: form
43 | func (_m *IClientService) CheckCredentials(form *model.ClientLoginForm) (*model.ClientCredential, error) {
44 | ret := _m.Called(form)
45 |
46 | var r0 *model.ClientCredential
47 | if rf, ok := ret.Get(0).(func(*model.ClientLoginForm) *model.ClientCredential); ok {
48 | r0 = rf(form)
49 | } else {
50 | if ret.Get(0) != nil {
51 | r0 = ret.Get(0).(*model.ClientCredential)
52 | }
53 | }
54 |
55 | var r1 error
56 | if rf, ok := ret.Get(1).(func(*model.ClientLoginForm) error); ok {
57 | r1 = rf(form)
58 | } else {
59 | r1 = ret.Error(1)
60 | }
61 |
62 | return r0, r1
63 | }
64 |
65 | // GetClientCredentialRoles provides a mock function with given fields: clientCredentialId
66 | func (_m *IClientService) GetClientCredentialRoles(clientCredentialId int) ([]string, error) {
67 | ret := _m.Called(clientCredentialId)
68 |
69 | var r0 []string
70 | if rf, ok := ret.Get(0).(func(int) []string); ok {
71 | r0 = rf(clientCredentialId)
72 | } else {
73 | if ret.Get(0) != nil {
74 | r0 = ret.Get(0).([]string)
75 | }
76 | }
77 |
78 | var r1 error
79 | if rf, ok := ret.Get(1).(func(int) error); ok {
80 | r1 = rf(clientCredentialId)
81 | } else {
82 | r1 = ret.Error(1)
83 | }
84 |
85 | return r0, r1
86 | }
87 |
88 | // GetClientToken provides a mock function with given fields: credential, roles
89 | func (_m *IClientService) GetClientToken(credential *model.ClientCredential, roles []string) (*token.TokenDetails, error) {
90 | ret := _m.Called(credential, roles)
91 |
92 | var r0 *token.TokenDetails
93 | if rf, ok := ret.Get(0).(func(*model.ClientCredential, []string) *token.TokenDetails); ok {
94 | r0 = rf(credential, roles)
95 | } else {
96 | if ret.Get(0) != nil {
97 | r0 = ret.Get(0).(*token.TokenDetails)
98 | }
99 | }
100 |
101 | var r1 error
102 | if rf, ok := ret.Get(1).(func(*model.ClientCredential, []string) error); ok {
103 | r1 = rf(credential, roles)
104 | } else {
105 | r1 = ret.Error(1)
106 | }
107 |
108 | return r0, r1
109 | }
110 |
111 | // GetCredential provides a mock function with given fields: id
112 | func (_m *IClientService) GetCredential(id int) (*model.ClientCredentialDto, error) {
113 | ret := _m.Called(id)
114 |
115 | var r0 *model.ClientCredentialDto
116 | if rf, ok := ret.Get(0).(func(int) *model.ClientCredentialDto); ok {
117 | r0 = rf(id)
118 | } else {
119 | if ret.Get(0) != nil {
120 | r0 = ret.Get(0).(*model.ClientCredentialDto)
121 | }
122 | }
123 |
124 | var r1 error
125 | if rf, ok := ret.Get(1).(func(int) error); ok {
126 | r1 = rf(id)
127 | } else {
128 | r1 = ret.Error(1)
129 | }
130 |
131 | return r0, r1
132 | }
133 |
134 | // ListCredentials provides a mock function with given fields: page
135 | func (_m *IClientService) ListCredentials(page common.Pagination) (model.ClientCredentialDtos, *common.PageResult, error) {
136 | ret := _m.Called(page)
137 |
138 | var r0 model.ClientCredentialDtos
139 | if rf, ok := ret.Get(0).(func(common.Pagination) model.ClientCredentialDtos); ok {
140 | r0 = rf(page)
141 | } else {
142 | if ret.Get(0) != nil {
143 | r0 = ret.Get(0).(model.ClientCredentialDtos)
144 | }
145 | }
146 |
147 | var r1 *common.PageResult
148 | if rf, ok := ret.Get(1).(func(common.Pagination) *common.PageResult); ok {
149 | r1 = rf(page)
150 | } else {
151 | if ret.Get(1) != nil {
152 | r1 = ret.Get(1).(*common.PageResult)
153 | }
154 | }
155 |
156 | var r2 error
157 | if rf, ok := ret.Get(2).(func(common.Pagination) error); ok {
158 | r2 = rf(page)
159 | } else {
160 | r2 = ret.Error(2)
161 | }
162 |
163 | return r0, r1, r2
164 | }
165 |
166 | // PatchCredential provides a mock function with given fields: form, id
167 | func (_m *IClientService) PatchCredential(form *model.ClientCredentialPatchForm, id int) (*model.ClientCredentialDto, error) {
168 | ret := _m.Called(form, id)
169 |
170 | var r0 *model.ClientCredentialDto
171 | if rf, ok := ret.Get(0).(func(*model.ClientCredentialPatchForm, int) *model.ClientCredentialDto); ok {
172 | r0 = rf(form, id)
173 | } else {
174 | if ret.Get(0) != nil {
175 | r0 = ret.Get(0).(*model.ClientCredentialDto)
176 | }
177 | }
178 |
179 | var r1 error
180 | if rf, ok := ret.Get(1).(func(*model.ClientCredentialPatchForm, int) error); ok {
181 | r1 = rf(form, id)
182 | } else {
183 | r1 = ret.Error(1)
184 | }
185 |
186 | return r0, r1
187 | }
188 |
189 | // RefreshToken provides a mock function with given fields: refreshToken
190 | func (_m *IClientService) RefreshToken(refreshToken string) (*token.TokenDetails, error) {
191 | ret := _m.Called(refreshToken)
192 |
193 | var r0 *token.TokenDetails
194 | if rf, ok := ret.Get(0).(func(string) *token.TokenDetails); ok {
195 | r0 = rf(refreshToken)
196 | } else {
197 | if ret.Get(0) != nil {
198 | r0 = ret.Get(0).(*token.TokenDetails)
199 | }
200 | }
201 |
202 | var r1 error
203 | if rf, ok := ret.Get(1).(func(string) error); ok {
204 | r1 = rf(refreshToken)
205 | } else {
206 | r1 = ret.Error(1)
207 | }
208 |
209 | return r0, r1
210 | }
211 |
--------------------------------------------------------------------------------
/account-service/module/client/model/client.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/datatypes"
7 | "gorm.io/gorm"
8 | )
9 |
10 | type ClientCredentials []*ClientCredential
11 |
12 | type ClientCredential struct {
13 | gorm.Model
14 | TenantId int
15 | Name string
16 | Code string
17 | Secret string
18 | ReferenceId string
19 | Type string
20 | Payload datatypes.JSON
21 | IsActive int
22 | CreatedAt time.Time
23 | UpdatedAt time.Time
24 | }
25 |
26 | type ClientCredentialRole struct {
27 | ClientCredentialID uint
28 | RoleID uint
29 | }
30 |
31 | type ClientCredentialForm struct {
32 | Name string `json:"name" example:"Client A" valid:"Required;MinSize(2);MaxSize(255)"`
33 | Code string `json:"code" example:"ClientA" valid:"Required;AlphaNumeric;MinSize(1);MaxSize(255)"`
34 | ReferenceId string `json:"referenceId" example:"user_id" label:"ReferenceId" valid:"Required;"`
35 | Type string `json:"type" example:"user" label:"Type" valid:"Required"`
36 | Payload datatypes.JSON `json:"payload" label:"Payload"`
37 | IsActive int `json:"isActive" example:"1" label:"isActive" valid:"Binary"`
38 | }
39 |
40 | type ClientCredentialDtos []*ClientCredentialDto
41 |
42 | type ClientCredentialDto struct {
43 | ID uint `json:"id" example:"1"`
44 | Name string `json:"name" example:"client 1"`
45 | Code string `json:"code" example:"1002"`
46 | Secret string `json:"secret" example:"12-345-567"`
47 | ReferenceId string `json:"referenceId" example:"user_id"`
48 | Type string `json:"type" example:"user"`
49 | Payload datatypes.JSON `json:"payload" example:"{}"`
50 | IsActive int `json:"isActive" example:"1"`
51 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
52 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
53 | }
54 |
55 | func (c ClientCredential) ToCredentialDto() *ClientCredentialDto {
56 | return &ClientCredentialDto{
57 | ID: c.ID,
58 | Name: c.Name,
59 | Code: c.Code,
60 | Secret: c.Secret,
61 | ReferenceId: c.ReferenceId,
62 | Type: c.Type,
63 | Payload: c.Payload,
64 | IsActive: c.IsActive,
65 | CreatedAt: c.CreatedAt,
66 | UpdatedAt: c.UpdatedAt,
67 | }
68 | }
69 |
70 | func (cc ClientCredentials) ToCredentialDtos() ClientCredentialDtos {
71 | dtos := make([]*ClientCredentialDto, len(cc))
72 | for i, b := range cc {
73 | dtos[i] = b.ToCredentialDto()
74 | }
75 |
76 | return dtos
77 | }
78 |
79 | type ClientCredentialPatchForm struct {
80 | Name string `json:"name" example:"Client A" valid:"Required;MinSize(2);MaxSize(255)"`
81 | Code string `json:"code" example:"ClientA" valid:"Required;AlphaNumeric;MinSize(1);MaxSize(255)"`
82 | Secret string `json:"secret" `
83 | Type string `json:"type"`
84 | }
85 |
86 | type ClientLoginForm struct {
87 | GrantType string `json:"grant_type" label:"grant_type" form:"grant_type" example:"client_credentials" valid:"Required;MinSize(2);MaxSize(255)"`
88 | ClientId string `json:"client_id" label:"client_id" form:"client_id" example:"ClientA" valid:"Required;MinSize(1);MaxSize(255)"`
89 | ClientSecret string `json:"client_secret" label:"client_secret" form:"client_secret" example:"@#WERWREWRWERWE" valid:"Required;MinSize(1);MaxSize(255)"`
90 | RefreshToken string `json:"refresh_token,omitempty" label:"refresh_token" form:"refresh_token" example:"@#WERWREWRWERWE" `
91 | }
92 |
93 | func (f *ClientCredentialPatchForm) ToModel() (*ClientCredential, error) {
94 | return &ClientCredential{
95 | Name: f.Name,
96 | Code: f.Code,
97 | Secret: f.Secret,
98 | Type: f.Type,
99 | }, nil
100 | }
101 |
102 | func (f *ClientCredentialForm) ToModel() (*ClientCredential, error) {
103 | return &ClientCredential{
104 | Name: f.Name,
105 | Code: f.Code,
106 | ReferenceId: f.ReferenceId,
107 | Type: f.Type,
108 | Payload: f.Payload,
109 | IsActive: f.IsActive,
110 | }, nil
111 | }
112 |
--------------------------------------------------------------------------------
/account-service/module/client/repo/client.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "micro/module/client/model"
5 | "strings"
6 |
7 | "gorm.io/gorm"
8 |
9 | common "github.com/krishnarajvr/micro-common"
10 | )
11 |
12 | type IClientRepo interface {
13 | ListCredentials(page common.Pagination) (model.ClientCredentials, *common.PageResult, error)
14 | AddCredential(form *model.ClientCredential) (*model.ClientCredential, error)
15 | GetCredential(id int) (*model.ClientCredential, error)
16 | PatchCredential(form *model.ClientCredentialPatchForm, id int) (*model.ClientCredential, error)
17 | CheckCredentials(form *model.ClientLoginForm) (*model.ClientCredential, error)
18 | GetClientCredentialRoles(clientCredentialId int) ([]string, error)
19 | }
20 |
21 | type ClientRepo struct {
22 | DB *gorm.DB
23 | }
24 |
25 | func NewClientRepo(db *gorm.DB) ClientRepo {
26 | return ClientRepo{
27 | DB: db,
28 | }
29 | }
30 |
31 | func (r ClientRepo) ListCredentials(page common.Pagination) (model.ClientCredentials, *common.PageResult, error) {
32 | clientCredentials := make([]*model.ClientCredential, 0)
33 | var totalCount int64
34 |
35 | if err := r.DB.Table("client_credentials").Count(&totalCount).Error; err != nil {
36 | return nil, nil, err
37 | }
38 |
39 | if err := r.DB.Scopes(common.Paginate(page)).Find(&clientCredentials).Error; err != nil {
40 | return nil, nil, err
41 | }
42 |
43 | pageResult := common.PageInfo(page, totalCount)
44 |
45 | if len(clientCredentials) == 0 {
46 | return nil, nil, nil
47 | }
48 |
49 | return clientCredentials, &pageResult, nil
50 | }
51 |
52 | func (r ClientRepo) AddCredential(clientCredential *model.ClientCredential) (*model.ClientCredential, error) {
53 | var id uint
54 | row := r.DB.Table("roles").
55 | Select("id").
56 | Where("code = ?", strings.ToUpper(clientCredential.Type)).
57 | Row()
58 | row.Scan(&id)
59 |
60 | errFromDB := r.DB.Transaction(func(tx *gorm.DB) error {
61 | defer func() {
62 | if r := recover(); r != nil {
63 | tx.Rollback()
64 | }
65 | }()
66 |
67 | if err := tx.Error; err != nil {
68 | return err
69 | }
70 |
71 | if err := tx.Create(&clientCredential).Error; err != nil {
72 | tx.Rollback()
73 | return err
74 | }
75 |
76 | ccr := model.ClientCredentialRole{ClientCredentialID: clientCredential.ID, RoleID: id}
77 |
78 | if err := tx.Create(&ccr).Error; err != nil {
79 | tx.Rollback()
80 | return err
81 | }
82 |
83 | return tx.Error
84 | })
85 |
86 | if errFromDB != nil {
87 | return nil, errFromDB
88 | }
89 |
90 | return clientCredential, nil
91 | }
92 |
93 | func (r ClientRepo) GetCredential(id int) (*model.ClientCredential, error) {
94 | clientCredential := new(model.ClientCredential)
95 |
96 | if err := r.DB.Where("id = ?", id).First(&clientCredential).Error; err != nil {
97 | return nil, err
98 | }
99 |
100 | return clientCredential, nil
101 | }
102 |
103 | func (r ClientRepo) PatchCredential(form *model.ClientCredentialPatchForm, id int) (*model.ClientCredential, error) {
104 | clientCredential, err := form.ToModel()
105 |
106 | if err != nil {
107 | return nil, err
108 | }
109 |
110 | if err := r.DB.Where("id = ?", id).Updates(&clientCredential).Error; err != nil {
111 | return nil, err
112 | }
113 |
114 | return clientCredential, nil
115 | }
116 |
117 | func (r ClientRepo) CheckCredentials(form *model.ClientLoginForm) (*model.ClientCredential, error) {
118 | clientCredential := new(model.ClientCredential)
119 | err := r.DB.Table("client_credentials").
120 | Where("code = ?", form.ClientId).
121 | Where("secret = ?", form.ClientSecret).
122 | First(&clientCredential).Error
123 |
124 | if err != nil {
125 | return nil, err
126 | }
127 |
128 | return clientCredential, nil
129 | }
130 |
131 | func (r ClientRepo) GetClientCredentialRoles(clientCredentialId int) ([]string, error) {
132 | var roles []string
133 | err := r.DB.Table(`roles`).
134 | Select(`LOWER(roles.code) as code`).
135 | Joins(`JOIN client_credential_roles on client_credential_roles.role_id = roles.id`).
136 | Where(`client_credential_roles.client_credential_id = ? `, clientCredentialId).
137 | Find(&roles).Error
138 |
139 | if err != nil {
140 | return nil, err
141 | }
142 |
143 | return roles, nil
144 | }
145 |
--------------------------------------------------------------------------------
/account-service/module/client/routes.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | //InitRoutes for the module
4 | func InitRoutes(c HandlerConfig) {
5 | h := Handler{
6 | ClientService: c.ClientService,
7 | Lang: c.Lang,
8 | }
9 |
10 | //Set api group
11 | g := c.R.Group(c.BaseURL)
12 | g.GET("/clientCredentials/:id", h.GetClientCredential)
13 | g.GET("/clientCredentials", h.ListClientCredentials)
14 | g.POST("/clientCredentials", h.AddClientCredential)
15 | g.PATCH("/clientCredentials/:id", h.PatchClientCredential)
16 | g.POST("/clientLogin", h.ClientLogin)
17 | g.POST("/oauth/token", h.OauthToken)
18 | }
19 |
--------------------------------------------------------------------------------
/account-service/module/client/service/client_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "encoding/json"
5 | "micro/module/client/model"
6 | "micro/module/client/repo"
7 | "micro/util/token"
8 |
9 | common "github.com/krishnarajvr/micro-common"
10 |
11 | "github.com/google/uuid"
12 | "github.com/krishnarajvr/micro-common/locale"
13 | )
14 |
15 | type IClientService interface {
16 | ListCredentials(page common.Pagination) (model.ClientCredentialDtos, *common.PageResult, error)
17 | AddCredential(content *model.ClientCredentialForm, tenantId int) (*model.ClientCredentialDto, error)
18 | GetCredential(id int) (*model.ClientCredentialDto, error)
19 | PatchCredential(form *model.ClientCredentialPatchForm, id int) (*model.ClientCredentialDto, error)
20 | CheckCredentials(form *model.ClientLoginForm) (*model.ClientCredential, error)
21 | GetClientToken(credential *model.ClientCredential, roles []string) (*token.TokenDetails, error)
22 | GetClientCredentialRoles(clientCredentialId int) ([]string, error)
23 | RefreshToken(refreshToken string) (*token.TokenDetails, error)
24 | }
25 |
26 | type ServiceConfig struct {
27 | ClientRepo repo.IClientRepo
28 | Lang *locale.Locale
29 | Token *token.Token
30 | }
31 |
32 | type Service struct {
33 | ClientRepo repo.IClientRepo
34 | Lang *locale.Locale
35 | Token *token.Token
36 | }
37 |
38 | func NewService(c ServiceConfig) IClientService {
39 | return &Service{
40 | ClientRepo: c.ClientRepo,
41 | Lang: c.Lang,
42 | Token: c.Token,
43 | }
44 | }
45 |
46 | func (s *Service) ListCredentials(page common.Pagination) (model.ClientCredentialDtos, *common.PageResult, error) {
47 | clientCredentials, pageResult, err := s.ClientRepo.ListCredentials(page)
48 |
49 | if err != nil {
50 | return nil, nil, err
51 | }
52 |
53 | clientCredentialsDtos := clientCredentials.ToCredentialDtos()
54 |
55 | return clientCredentialsDtos, pageResult, err
56 | }
57 |
58 | func (s *Service) AddCredential(form *model.ClientCredentialForm, tenantId int) (*model.ClientCredentialDto, error) {
59 | clientCredentialModel, err := form.ToModel()
60 | //Todo - Get from token
61 | clientCredentialModel.TenantId = tenantId
62 | clientCredentialModel.Secret = uuid.New().String()
63 |
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | clientCredential, err := s.ClientRepo.AddCredential(clientCredentialModel)
69 | if err != nil {
70 | return nil, err
71 | }
72 |
73 | clientCredntialDto := clientCredential.ToCredentialDto()
74 |
75 | return clientCredntialDto, nil
76 | }
77 |
78 | func (s *Service) GetCredential(id int) (*model.ClientCredentialDto, error) {
79 | clientCredential, err := s.ClientRepo.GetCredential(id)
80 |
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | clientCredntialDto := clientCredential.ToCredentialDto()
86 |
87 | return clientCredntialDto, nil
88 | }
89 |
90 | func (s *Service) PatchCredential(form *model.ClientCredentialPatchForm, id int) (*model.ClientCredentialDto, error) {
91 | clientCredential, err := s.ClientRepo.PatchCredential(form, id)
92 |
93 | if err != nil {
94 | return nil, err
95 | }
96 |
97 | clientCredntialDto := clientCredential.ToCredentialDto()
98 |
99 | return clientCredntialDto, nil
100 | }
101 |
102 | func (s *Service) CheckCredentials(form *model.ClientLoginForm) (*model.ClientCredential, error) {
103 | clientCredential, err := s.ClientRepo.CheckCredentials(form)
104 |
105 | if err != nil {
106 | return nil, err
107 | }
108 |
109 | return clientCredential, nil
110 | }
111 |
112 | func (s *Service) GetClientToken(credential *model.ClientCredential, roles []string) (*token.TokenDetails, error) {
113 | subject := "Vendor"
114 | var payload map[string]interface{}
115 |
116 | if len(credential.Payload) != 0 {
117 | json.Unmarshal(credential.Payload, &payload)
118 |
119 | if payload["name"] != nil {
120 | subject = payload["name"].(string)
121 | }
122 | }
123 |
124 | tokenData := token.TokenData{
125 | ReferenceId: credential.ReferenceId,
126 | Type: credential.Type,
127 | TenantId: int64(credential.TenantId),
128 | Subject: subject,
129 | Admin: false,
130 | Roles: roles,
131 | }
132 |
133 | token, _ := s.Token.CreateToken(&tokenData)
134 |
135 | return token, nil
136 | }
137 |
138 | func (s *Service) GetClientCredentialRoles(clientCredentialId int) ([]string, error) {
139 | roles, err := s.ClientRepo.GetClientCredentialRoles(clientCredentialId)
140 |
141 | if err != nil {
142 | return nil, err
143 | }
144 |
145 | return roles, nil
146 | }
147 |
148 | func (s *Service) RefreshToken(refreshToken string) (*token.TokenDetails, error) {
149 | token, err := s.Token.Refresh(refreshToken, "vendor", "ThirdParty")
150 |
151 | if err != nil {
152 | return nil, err
153 | }
154 |
155 | return token, nil
156 | }
157 |
--------------------------------------------------------------------------------
/account-service/module/client/service/client_service_test.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "fmt"
5 | "micro/app"
6 | "micro/config"
7 | "testing"
8 |
9 | "micro/module/client/mocks"
10 | "micro/module/client/model"
11 |
12 | common "github.com/krishnarajvr/micro-common"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/mock"
15 | )
16 |
17 | func TestClientCredential(t *testing.T) {
18 | t.Run("List ClientCredential Service", func(t *testing.T) {
19 | t.Run("Success", func(t *testing.T) {
20 | mockClientCredentialResp := &model.ClientCredential{
21 | Name: "client 1",
22 | Type: "type",
23 | TenantId: 1,
24 | }
25 | page := common.Pagination{
26 | PageNumber: "ID",
27 | PageOrder: "DESC",
28 | PageOffset: "0",
29 | Search: "",
30 | }
31 | clients := model.ClientCredential{mockClientCredentialResp}
32 | clientsDtos := clients.ToCredentialDto()
33 |
34 | IClientRepo := new(mocks.IClientRepo)
35 |
36 | IClientRepo.On("List", page).Return(clients, nil, nil)
37 |
38 | ps := NewService(ServiceConfig{
39 | ClientRepo: IClientRepo,
40 | })
41 |
42 | u, _, err := ps.ListCredentials(page)
43 |
44 | assert.NoError(t, err)
45 | assert.Equal(t, u, clientsDtos)
46 | })
47 | })
48 |
49 | t.Run("Patch ClientCredential Service", func(t *testing.T) {
50 | t.Run("Success", func(t *testing.T) {
51 | mockClientCredentialResp := &model.ClientCredential{
52 | Name: "client 1",
53 | Type: "type",
54 | TenantId: 1,
55 | Code: "code",
56 | ReferenceId: "anna@gmail.com",
57 | }
58 | mockClientPatchCredentialReq := &model.ClientCredentialPatchForm{
59 | Name: "client 1",
60 | Type: "type",
61 | Code: "code",
62 | }
63 |
64 | IClientRepo := new(mocks.IClientRepo)
65 |
66 | IClientRepo.On("Patch", mock.Anything, 1).Return(mockClientCredentialResp, nil, nil)
67 |
68 | ps := NewService(ServiceConfig{
69 | ClientRepo: IClientRepo,
70 | })
71 |
72 | _, err := ps.PatchCredential(mockClientPatchCredentialReq, 1)
73 |
74 | assert.NoError(t, err)
75 | })
76 | })
77 |
78 | t.Run("Get ClientCredential Service", func(t *testing.T) {
79 | t.Run("Success", func(t *testing.T) {
80 | client := &model.ClientCredential{
81 | Name: "client 1",
82 | Type: "type",
83 | TenantId: 1,
84 | Code: "code",
85 | ReferenceId: "bob@gmail.com",
86 | }
87 |
88 | clientCredntialDto := client.ToCredentialDto()
89 |
90 | IClientRepo := new(mocks.IClientRepo)
91 |
92 | IClientRepo.On("Get", 1).Return(client, nil, nil)
93 |
94 | ps := NewService(ServiceConfig{
95 | ClientRepo: IClientRepo,
96 | })
97 |
98 | u, err := ps.GetCredential(1)
99 |
100 | assert.NoError(t, err)
101 | assert.Equal(t, u, clientCredntialDto)
102 | })
103 | })
104 |
105 | t.Run("Add_ClientCredential_Service", func(t *testing.T) {
106 | t.Run("Success", func(t *testing.T) {
107 | clientForm := &model.ClientCredentialForm{
108 | Name: "client 1",
109 | Type: "type",
110 | Code: "code",
111 | ReferenceId: "bob@gmail.com",
112 | IsActive: 1,
113 | }
114 |
115 | clientModel, _ := clientForm.ToModel()
116 | clientCredntialDto := clientModel.ToCredentialDto()
117 | clientModel.TenantId = 1
118 | IClientRepo := new(mocks.IClientRepo)
119 |
120 | IClientRepo.On("AddCredential", mock.Anything).Return(clientModel, nil, nil)
121 |
122 | ps := NewService(ServiceConfig{
123 | ClientRepo: IClientRepo,
124 | })
125 |
126 | u, err := ps.AddCredential(clientForm, 1)
127 |
128 | assert.NoError(t, err)
129 | assert.Equal(t, u, clientCredntialDto)
130 |
131 | })
132 | t.Run("Failure", func(t *testing.T) {
133 | clientForm := &model.ClientCredentialForm{
134 | Name: "client 1",
135 | Type: "type",
136 | Code: "code",
137 | ReferenceId: "bob@gmail.com",
138 | IsActive: 1,
139 | }
140 |
141 | clientModel, _ := clientForm.ToModel()
142 | clientCredntialDto := clientModel.ToCredentialDto()
143 | clientModel.TenantId = 1
144 | IClientRepo := new(mocks.IClientRepo)
145 |
146 | IClientRepo.On("AddCredential", mock.Anything).Return(clientModel, fmt.Errorf("Some error down call chain"))
147 |
148 | cfg := config.AppConfig()
149 | lang, err := app.InitLocale(cfg)
150 |
151 | assert.NoError(t, err)
152 |
153 | ps := NewService(ServiceConfig{
154 | ClientRepo: IClientRepo,
155 | Lang: lang,
156 | })
157 |
158 | u, _ := ps.AddCredential(clientForm, 1)
159 | assert.NotEqual(t, u, clientCredntialDto)
160 | IClientRepo.AssertExpectations(t)
161 | })
162 | })
163 | }
164 |
--------------------------------------------------------------------------------
/account-service/module/client/swagger/client.go:
--------------------------------------------------------------------------------
1 | package swagger
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type ClientCredentialDtos []*ClientCredentialDto
8 |
9 | type ClientCredentialDto struct {
10 | ID uint `json:"id" example:"1"`
11 | Name string `json:"name" example:"client 1"`
12 | Code string `json:"code" example:"client 1"`
13 | Secret string `json:"secret" example:"12-345-567"`
14 | ReferenceId string `json:"referenceId" example:"user id"`
15 | Type string `json:"type" example:"user"`
16 | Payload Payload `json:"payload" `
17 | IsActive int `json:"isActive" example:"1"`
18 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
19 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
20 | }
21 |
22 | type ClientTokenResponse struct {
23 | AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NXVCJ9.eyJhY2Nlc30IDIiLCJ0eXBlIjoiY2xpZW50In0.XBjAxzruIT"`
24 | TokenType string `json:"token_type" example:"bearer" `
25 | RefreshToken string `json:"refresh_token" example:"eyJhbGciOiJIUzI54XVCJ54.eyJhY2Nlc30IDIiLCJ0eXBlIjoiY2xpZW5045.XBjAxzru45"`
26 | }
27 |
28 | type ClientLoginForm struct {
29 | Type string `json:"grant_type" label:"grant_type" form:"grant_type" example:"client_credentials" valid:"Required;MinSize(2);MaxSize(255)"`
30 | Code string `json:"client_id" label:"client_id" form:"client_id" example:"ClientA" valid:"Required;MinSize(1);MaxSize(255)"`
31 | Secret string `json:"client_secret" label:"client_secret" form:"client_secret" example:"@#WERWREWRWERWE" valid:"Required;MinSize(1);MaxSize(255)"`
32 | }
33 |
34 | type ClientCredentialSampleData struct {
35 | ClientCredentialData ClientCredentialDto `json:"client"`
36 | }
37 |
38 | type ClientCredentialSampleListData struct {
39 | ClientCredentialData ClientCredentialDtos `json:"client"`
40 | }
41 |
42 | type ClientListCredentialsResponse struct {
43 | Status uint `json:"status" example:"200"`
44 | Error interface{} `json:"error"`
45 | Data ClientCredentialSampleListData `json:"data"`
46 | }
47 |
48 | type ClientCredentialResponse struct {
49 | Status uint `json:"status" example:"200"`
50 | Error interface{} `json:"error"`
51 | Data ClientCredentialSampleData `json:"data"`
52 | }
53 |
54 | type ClientCredentialForm struct {
55 | Name string `json:"name" example:"Client A" `
56 | Code string `json:"code" example:"ClientA" `
57 | ReferenceId string `json:"referenceId" example:"client_id" `
58 | Type string `json:"type" example:"user"`
59 | Payload Payload `json:"payload" `
60 | IsActive int `json:"isActive" example:"1" `
61 | }
62 |
63 | type Payload struct {
64 | Key1 string `json:"key1"`
65 | Key2 string `json:"key2"`
66 | Key3 string `json:"key3"`
67 | }
68 |
--------------------------------------------------------------------------------
/account-service/module/module.go:
--------------------------------------------------------------------------------
1 | package module
2 |
3 | import (
4 | "micro/app"
5 | "micro/module/client"
6 | "micro/module/tenant"
7 | "micro/module/user"
8 | "os"
9 |
10 | docs "micro/doc"
11 |
12 | ginSwagger "github.com/swaggo/gin-swagger"
13 | "github.com/swaggo/gin-swagger/swaggerFiles"
14 | )
15 |
16 | // @title Micro Service API Document
17 | // @version 1.0
18 | // @description List of APIs for Micro Service
19 | // @termsOfService http://swagger.io/terms/
20 |
21 | // @securityDefinitions.apikey ApiKeyAuth
22 | // @in header
23 | // @name Authorization
24 |
25 | // @host localhost:8082
26 | // @BasePath /api/v1
27 | func Inject(appConfig app.AppConfig) {
28 | user.Inject(appConfig)
29 | tenant.Inject(appConfig)
30 | client.Inject(appConfig)
31 |
32 | //Swagger Doc details
33 | url := os.Getenv("API_GATEWAY_URL")
34 | prefix := os.Getenv("API_GATEWAY_PREFIX")
35 |
36 | if len(url) == 0 {
37 | url = "localhost:" + appConfig.Cfg.Server.Port
38 | }
39 |
40 | if len(prefix) == 0 {
41 | prefix = appConfig.Cfg.App.BaseURL
42 | }
43 |
44 | docs.SwaggerInfo.Title = "Account Service API Document"
45 | docs.SwaggerInfo.Description = "List of APIs for Account Service."
46 | docs.SwaggerInfo.Version = "1.0"
47 | docs.SwaggerInfo.Host = url
48 | docs.SwaggerInfo.BasePath = prefix
49 | docs.SwaggerInfo.Schemes = []string{"https", "http"}
50 | //Init Swagger routes
51 | appConfig.Router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
52 | }
53 |
--------------------------------------------------------------------------------
/account-service/module/tenant/handler.go:
--------------------------------------------------------------------------------
1 | package tenant
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "micro/module/tenant/model"
7 | "micro/module/tenant/service"
8 |
9 | "github.com/astaxie/beego/validation"
10 | "github.com/gin-gonic/gin"
11 |
12 | common "github.com/krishnarajvr/micro-common"
13 | "github.com/krishnarajvr/micro-common/locale"
14 | "github.com/unknwon/com"
15 | )
16 |
17 | type Handler struct {
18 | TenantService service.ITenantService
19 | Lang *locale.Locale
20 | }
21 |
22 | // ListTenants godoc
23 | // @Summary List all existing tenants
24 | // @Description List all existing tenants
25 | // @Tags Tenant
26 | // @Accept json
27 | // @Produce json
28 | // @Security ApiKeyAuth
29 | // @Failure 404 {object} swagdto.Error404
30 | // @Success 200 {object} swagger.TenantListResponse
31 | // @Router /tenants [get]
32 | func (h *Handler) ListTenants(c *gin.Context) {
33 | page := common.Paginator(c)
34 |
35 | tenants, err := h.TenantService.List(page)
36 |
37 | if err != nil {
38 | log.Printf("Failed to sign up tenant: %v\n", err.Error())
39 | common.BadRequest(c, "Failed")
40 | return
41 | }
42 |
43 | common.SuccessResponse(c, "tenants", tenants)
44 | }
45 |
46 | // @Summary Get a tenant
47 | // @Produce json
48 | // @Tags Tenant
49 | // @Param id path int true "ID"
50 | // @Security ApiKeyAuth
51 | // @Failure 404 {object} swagdto.Error404
52 | // @Success 200 {object} swagger.TenantResponse
53 | // @Router /tenants/{id} [get]
54 | func (h *Handler) GetTenant(c *gin.Context) {
55 | id := com.StrTo(c.Param("id")).MustInt()
56 |
57 | valid := validation.Validation{}
58 | valid.Min(id, 1, "id")
59 |
60 | if valid.HasErrors() {
61 | common.BadRequest(c, valid.Errors)
62 | return
63 | }
64 |
65 | tenant, err := h.TenantService.Get(id)
66 | if err != nil {
67 | common.BadRequest(c, err)
68 | return
69 | }
70 |
71 | common.SuccessResponse(c, "tenant", tenant)
72 | }
73 |
74 | // @Summary Register tenant
75 | // @Produce json
76 | // @Tags Tenant
77 | // @Param user body model.TenantRegisterForm true "Tenant data"
78 | // @Security ApiKeyAuth
79 | // @Failure 404 {object} swagdto.Error404
80 | // @Success 200 {object} swagger.TenantResponse
81 | // @Router /tenantRegister [post]
82 | func (h *Handler) RegisterTenant(c *gin.Context) {
83 | log := c.MustGet("log").(*common.MicroLog)
84 | var form model.TenantRegisterForm
85 |
86 | ok, errorData := common.ValidateForm(c, &form)
87 | if !ok {
88 | common.ErrorResponse(c, errorData)
89 | return
90 | }
91 |
92 | tenant, err := h.TenantService.Register(&form)
93 | if err != nil {
94 | log.Message(err)
95 | errorCode := common.CheckDbError(err)
96 |
97 | if errorCode == common.ALREADY_EXISTS {
98 | common.BadRequestWithMessage(c, h.Lang.Get("message_already_exists", "Tenant"))
99 | return
100 | }
101 |
102 | common.InternalServerError(c, h.Lang.Get("message_internal_error", ""))
103 |
104 | return
105 | }
106 |
107 | common.SuccessResponse(c, "tenant", tenant)
108 | }
109 |
110 | // @Summary Add tenant
111 | // @Produce json
112 | // @Tags Tenant
113 | // @Security ApiKeyAuth
114 | // @Param user body model.TenantForm true "Tenant Data"
115 | // @Failure 404 {object} swagdto.Error404
116 | // @Success 200 {object} swagger.TenantResponse
117 | // @Router /tenants [post]
118 | func (h *Handler) AddTenant(c *gin.Context) {
119 | var form model.TenantForm
120 |
121 | ok, errorData := common.ValidateForm(c, &form)
122 | if !ok {
123 | common.ErrorResponse(c, errorData)
124 | return
125 | }
126 |
127 | tenant, err := h.TenantService.Add(&form)
128 | if err != nil {
129 | common.BadRequest(c, err)
130 | return
131 | }
132 |
133 | common.SuccessResponse(c, "tenant", tenant)
134 | }
135 |
136 | // @Summary Update tenant
137 | // @Produce json
138 | // @Tags Tenant
139 | // @Param id path int true "ID"
140 | // @Security ApiKeyAuth
141 | // @Param user body model.TenantForm true "Tenant ID"
142 | // @Failure 404 {object} swagdto.Error404
143 | // @Success 200 {object} swagger.TenantResponse
144 | // @Router /tenants/{id} [post]
145 | func (h *Handler) UpdateTenant(c *gin.Context) {
146 | id := com.StrTo(c.Param("id")).MustInt()
147 | valid := validation.Validation{}
148 | valid.Min(id, 1, "id")
149 |
150 | if valid.HasErrors() {
151 | common.BadRequest(c, valid.Errors)
152 | return
153 | }
154 |
155 | var form model.TenantForm
156 |
157 | ok, errorData := common.ValidateForm(c, &form)
158 | if !ok {
159 | common.ErrorResponse(c, errorData)
160 | return
161 | }
162 |
163 | tenant, err := h.TenantService.Update(&form, id)
164 | if err != nil {
165 | common.BadRequest(c, err)
166 | return
167 | }
168 |
169 | common.SuccessResponse(c, "tenant", tenant)
170 | }
171 |
172 | // @Summary Patch tenant
173 | // @Produce json
174 | // @Tags Tenant
175 | // @Param id path int true "ID"
176 | // @Security ApiKeyAuth
177 | // @Param user body model.TenantForm true "Tenant ID"
178 | // @Failure 404 {object} swagdto.Error404
179 | // @Success 200 {object} swagger.TenantResponse
180 | // @Router /tenants/{id} [patch]
181 | func (h *Handler) PatchTenant(c *gin.Context) {
182 | id := com.StrTo(c.Param("id")).MustInt()
183 | valid := validation.Validation{}
184 | valid.Min(id, 1, "id")
185 |
186 | if valid.HasErrors() {
187 | common.BadRequest(c, valid.Errors)
188 | return
189 | }
190 |
191 | var form model.TenantPatchForm
192 |
193 | ok, errorData := common.ValidateForm(c, &form)
194 | if !ok {
195 | common.ErrorResponse(c, errorData)
196 | return
197 | }
198 |
199 | tenant, err := h.TenantService.Patch(&form, id)
200 | if err != nil {
201 | common.BadRequest(c, err)
202 | return
203 | }
204 |
205 | common.SuccessResponse(c, "tenant", tenant)
206 | }
207 |
208 | // @Summary Delete a tenant
209 | // @Produce json
210 | // @Tags Tenant
211 | // @Param id path int true "ID"
212 | // @Security ApiKeyAuth
213 | // @Failure 404 {object} swagdto.Error404
214 | // @Success 200 {object} swagger.TenantResponse
215 | // @Router /tenants/{id} [delete]
216 | func (h *Handler) DeleteTenant(c *gin.Context) {
217 | id := com.StrTo(c.Param("id")).MustInt()
218 | valid := validation.Validation{}
219 | valid.Min(id, 1, "id")
220 |
221 | if valid.HasErrors() {
222 | common.BadRequest(c, valid.Errors)
223 | return
224 | }
225 |
226 | tenant, err := h.TenantService.Delete(id)
227 | if err != nil {
228 | common.BadRequest(c, err)
229 | return
230 | }
231 |
232 | common.SuccessResponse(c, "tenant", tenant)
233 | }
234 |
235 | //GetJwks - Return Keys for JWT
236 | func (h *Handler) GetJwks(c *gin.Context) {
237 | //Todo - Get values from env or external file
238 | keys := `{"keys": [
239 | {
240 | "kty": "oct",
241 | "alg": "A128KW",
242 | "k": "GawgguFyGrWKav7AX4VKUg",
243 | "kid": "sim1"
244 | },
245 | {
246 | "kty": "oct",
247 | "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow",
248 | "kid": "sim2",
249 | "alg": "HS256"
250 | }
251 | ]}`
252 |
253 | var result map[string]interface{}
254 |
255 | // Unmarshal or Decode the JSON to the interface.
256 | json.Unmarshal([]byte(keys), &result)
257 |
258 | c.JSON(200, result)
259 | }
260 |
--------------------------------------------------------------------------------
/account-service/module/tenant/handler_test.go:
--------------------------------------------------------------------------------
1 | package tenant
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "micro/module/tenant/mocks"
8 | "micro/module/tenant/model"
9 | "net/http"
10 | "net/http/httptest"
11 | "testing"
12 |
13 | "micro/config"
14 |
15 | "github.com/gin-gonic/gin"
16 | common "github.com/krishnarajvr/micro-common"
17 | "github.com/krishnarajvr/micro-common/middleware"
18 | "github.com/stretchr/testify/assert"
19 | )
20 |
21 | func TestTenant(t *testing.T) {
22 | // Setup
23 | gin.SetMode(gin.TestMode)
24 |
25 | t.Run("List tenant", func(t *testing.T) {
26 |
27 | mockTenantResp := &model.TenantDto{
28 | ID: 1,
29 | Name: "Tenant 1",
30 | Code: "Code 1",
31 | }
32 | dtos := model.TenantDtos{mockTenantResp}
33 |
34 | mockTenantService := new(mocks.ITenantService)
35 |
36 | page := common.Pagination{
37 | Sort: "ID",
38 | Order: "DESC",
39 | Offset: "0",
40 | Limit: "25",
41 | Search: "",
42 | }
43 |
44 | mockTenantService.On("List", page).Return(dtos, nil, nil)
45 |
46 | // a response recorder for getting written http response
47 | rr := httptest.NewRecorder()
48 |
49 | // don't need a middleware as we don't yet have authorized tenant
50 | router := gin.Default()
51 | cfg := config.AppConfig()
52 | router.Use(middleware.LoggerToFile(cfg.Log.LogFilePath, cfg.Log.LogFileName))
53 |
54 | InitRoutes(HandlerConfig{
55 | R: router,
56 | TenantService: mockTenantService,
57 | })
58 |
59 | // create a request body with empty email and password
60 | reqBody, err := json.Marshal(gin.H{
61 | "tenant": "",
62 | })
63 | assert.NoError(t, err)
64 |
65 | // use bytes.NewBuffer to create a reader
66 | request, err := http.NewRequest(http.MethodGet, "/tenants", bytes.NewBuffer(reqBody))
67 | assert.NoError(t, err)
68 |
69 | request.Header.Set("Content-Type", "application/json")
70 |
71 | router.ServeHTTP(rr, request)
72 |
73 | fmt.Println(rr)
74 |
75 | assert.Equal(t, 200, rr.Code)
76 |
77 | })
78 |
79 | t.Run("List tenant Error", func(t *testing.T) {
80 |
81 | mockTenantService := new(mocks.ITenantService)
82 |
83 | page := common.Pagination{
84 | Sort: "ID",
85 | Order: "DESC",
86 | Offset: "0",
87 | Limit: "25",
88 | Search: "",
89 | }
90 |
91 | mockTenantService.On("List", page).Return(nil, nil, fmt.Errorf("Some error down call chain"))
92 |
93 | // a response recorder for getting written http response
94 | rr := httptest.NewRecorder()
95 |
96 | cfg := config.AppConfig()
97 | router := gin.Default()
98 | router.Use(middleware.LoggerToFile(cfg.Log.LogFilePath, cfg.Log.LogFileName))
99 |
100 | InitRoutes(HandlerConfig{
101 | R: router,
102 | TenantService: mockTenantService,
103 | })
104 |
105 | // use bytes.NewBuffer to create a reader
106 | request, err := http.NewRequest(http.MethodGet, "/tenants", bytes.NewBuffer(nil))
107 | assert.NoError(t, err)
108 |
109 | request.Header.Set("Content-Type", "application/json")
110 |
111 | router.ServeHTTP(rr, request)
112 |
113 | fmt.Println(rr)
114 |
115 | assert.Equal(t, 200, rr.Code)
116 |
117 | })
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/account-service/module/tenant/inject.go:
--------------------------------------------------------------------------------
1 | package tenant
2 |
3 | import (
4 | "micro/app"
5 | "micro/module/tenant/repo"
6 | "micro/module/tenant/service"
7 | userRepo "micro/module/user/repo"
8 |
9 | "github.com/krishnarajvr/micro-common/locale"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | type HandlerConfig struct {
15 | R *gin.Engine
16 | TenantService service.ITenantService
17 | BaseURL string
18 | Lang *locale.Locale
19 | }
20 |
21 | //Inject all dependencies
22 | func Inject(appConfig app.AppConfig) {
23 |
24 | tenantRepo := repo.NewTenantRepo(appConfig.Dbs.DB)
25 | userRepo := userRepo.NewUserRepo(appConfig.Dbs.DB)
26 |
27 | tenantService := service.NewService(service.ServiceConfig{
28 | TenantRepo: tenantRepo,
29 | UserRepo: userRepo,
30 | Lang: appConfig.Lang,
31 | })
32 |
33 | InitRoutes(HandlerConfig{
34 | R: appConfig.Router,
35 | TenantService: tenantService,
36 | BaseURL: appConfig.BaseURL,
37 | Lang: appConfig.Lang,
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/account-service/module/tenant/mocks/ITenantRepo.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | common "github.com/krishnarajvr/micro-common"
7 | mock "github.com/stretchr/testify/mock"
8 |
9 | model "micro/module/tenant/model"
10 | )
11 |
12 | // ITenantRepo is an autogenerated mock type for the ITenantRepo type
13 | type ITenantRepo struct {
14 | mock.Mock
15 | }
16 |
17 | // Add provides a mock function with given fields: form
18 | func (_m *ITenantRepo) Add(form *model.TenantForm) (*model.Tenant, error) {
19 | ret := _m.Called(form)
20 |
21 | var r0 *model.Tenant
22 | if rf, ok := ret.Get(0).(func(*model.TenantForm) *model.Tenant); ok {
23 | r0 = rf(form)
24 | } else {
25 | if ret.Get(0) != nil {
26 | r0 = ret.Get(0).(*model.Tenant)
27 | }
28 | }
29 |
30 | var r1 error
31 | if rf, ok := ret.Get(1).(func(*model.TenantForm) error); ok {
32 | r1 = rf(form)
33 | } else {
34 | r1 = ret.Error(1)
35 | }
36 |
37 | return r0, r1
38 | }
39 |
40 | // Delete provides a mock function with given fields: id
41 | func (_m *ITenantRepo) Delete(id int) (*model.Tenant, error) {
42 | ret := _m.Called(id)
43 |
44 | var r0 *model.Tenant
45 | if rf, ok := ret.Get(0).(func(int) *model.Tenant); ok {
46 | r0 = rf(id)
47 | } else {
48 | if ret.Get(0) != nil {
49 | r0 = ret.Get(0).(*model.Tenant)
50 | }
51 | }
52 |
53 | var r1 error
54 | if rf, ok := ret.Get(1).(func(int) error); ok {
55 | r1 = rf(id)
56 | } else {
57 | r1 = ret.Error(1)
58 | }
59 |
60 | return r0, r1
61 | }
62 |
63 | // Get provides a mock function with given fields: id
64 | func (_m *ITenantRepo) Get(id int) (*model.Tenant, error) {
65 | ret := _m.Called(id)
66 |
67 | var r0 *model.Tenant
68 | if rf, ok := ret.Get(0).(func(int) *model.Tenant); ok {
69 | r0 = rf(id)
70 | } else {
71 | if ret.Get(0) != nil {
72 | r0 = ret.Get(0).(*model.Tenant)
73 | }
74 | }
75 |
76 | var r1 error
77 | if rf, ok := ret.Get(1).(func(int) error); ok {
78 | r1 = rf(id)
79 | } else {
80 | r1 = ret.Error(1)
81 | }
82 |
83 | return r0, r1
84 | }
85 |
86 | // List provides a mock function with given fields: page
87 | func (_m *ITenantRepo) List(page common.Pagination) (model.Tenants, error) {
88 | ret := _m.Called(page)
89 |
90 | var r0 model.Tenants
91 | if rf, ok := ret.Get(0).(func(common.Pagination) model.Tenants); ok {
92 | r0 = rf(page)
93 | } else {
94 | if ret.Get(0) != nil {
95 | r0 = ret.Get(0).(model.Tenants)
96 | }
97 | }
98 |
99 | var r1 error
100 | if rf, ok := ret.Get(1).(func(common.Pagination) error); ok {
101 | r1 = rf(page)
102 | } else {
103 | r1 = ret.Error(1)
104 | }
105 |
106 | return r0, r1
107 | }
108 |
109 | // Patch provides a mock function with given fields: form, id
110 | func (_m *ITenantRepo) Patch(form *model.TenantPatchForm, id int) (*model.Tenant, error) {
111 | ret := _m.Called(form, id)
112 |
113 | var r0 *model.Tenant
114 | if rf, ok := ret.Get(0).(func(*model.TenantPatchForm, int) *model.Tenant); ok {
115 | r0 = rf(form, id)
116 | } else {
117 | if ret.Get(0) != nil {
118 | r0 = ret.Get(0).(*model.Tenant)
119 | }
120 | }
121 |
122 | var r1 error
123 | if rf, ok := ret.Get(1).(func(*model.TenantPatchForm, int) error); ok {
124 | r1 = rf(form, id)
125 | } else {
126 | r1 = ret.Error(1)
127 | }
128 |
129 | return r0, r1
130 | }
131 |
132 | // Update provides a mock function with given fields: form, id
133 | func (_m *ITenantRepo) Update(form *model.TenantForm, id int) (*model.Tenant, error) {
134 | ret := _m.Called(form, id)
135 |
136 | var r0 *model.Tenant
137 | if rf, ok := ret.Get(0).(func(*model.TenantForm, int) *model.Tenant); ok {
138 | r0 = rf(form, id)
139 | } else {
140 | if ret.Get(0) != nil {
141 | r0 = ret.Get(0).(*model.Tenant)
142 | }
143 | }
144 |
145 | var r1 error
146 | if rf, ok := ret.Get(1).(func(*model.TenantForm, int) error); ok {
147 | r1 = rf(form, id)
148 | } else {
149 | r1 = ret.Error(1)
150 | }
151 |
152 | return r0, r1
153 | }
154 |
--------------------------------------------------------------------------------
/account-service/module/tenant/mocks/ITenantService.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | common "github.com/krishnarajvr/micro-common"
7 | mock "github.com/stretchr/testify/mock"
8 |
9 | model "micro/module/tenant/model"
10 | )
11 |
12 | // ITenantService is an autogenerated mock type for the ITenantService type
13 | type ITenantService struct {
14 | mock.Mock
15 | }
16 |
17 | // Add provides a mock function with given fields: tenant
18 | func (_m *ITenantService) Add(tenant *model.TenantForm) (*model.TenantDto, error) {
19 | ret := _m.Called(tenant)
20 |
21 | var r0 *model.TenantDto
22 | if rf, ok := ret.Get(0).(func(*model.TenantForm) *model.TenantDto); ok {
23 | r0 = rf(tenant)
24 | } else {
25 | if ret.Get(0) != nil {
26 | r0 = ret.Get(0).(*model.TenantDto)
27 | }
28 | }
29 |
30 | var r1 error
31 | if rf, ok := ret.Get(1).(func(*model.TenantForm) error); ok {
32 | r1 = rf(tenant)
33 | } else {
34 | r1 = ret.Error(1)
35 | }
36 |
37 | return r0, r1
38 | }
39 |
40 | // Delete provides a mock function with given fields: id
41 | func (_m *ITenantService) Delete(id int) (*model.TenantDto, error) {
42 | ret := _m.Called(id)
43 |
44 | var r0 *model.TenantDto
45 | if rf, ok := ret.Get(0).(func(int) *model.TenantDto); ok {
46 | r0 = rf(id)
47 | } else {
48 | if ret.Get(0) != nil {
49 | r0 = ret.Get(0).(*model.TenantDto)
50 | }
51 | }
52 |
53 | var r1 error
54 | if rf, ok := ret.Get(1).(func(int) error); ok {
55 | r1 = rf(id)
56 | } else {
57 | r1 = ret.Error(1)
58 | }
59 |
60 | return r0, r1
61 | }
62 |
63 | // Get provides a mock function with given fields: id
64 | func (_m *ITenantService) Get(id int) (*model.TenantDto, error) {
65 | ret := _m.Called(id)
66 |
67 | var r0 *model.TenantDto
68 | if rf, ok := ret.Get(0).(func(int) *model.TenantDto); ok {
69 | r0 = rf(id)
70 | } else {
71 | if ret.Get(0) != nil {
72 | r0 = ret.Get(0).(*model.TenantDto)
73 | }
74 | }
75 |
76 | var r1 error
77 | if rf, ok := ret.Get(1).(func(int) error); ok {
78 | r1 = rf(id)
79 | } else {
80 | r1 = ret.Error(1)
81 | }
82 |
83 | return r0, r1
84 | }
85 |
86 | // List provides a mock function with given fields: page
87 | func (_m *ITenantService) List(page common.Pagination) (model.TenantDtos, error) {
88 | ret := _m.Called(page)
89 |
90 | var r0 model.TenantDtos
91 | if rf, ok := ret.Get(0).(func(common.Pagination) model.TenantDtos); ok {
92 | r0 = rf(page)
93 | } else {
94 | if ret.Get(0) != nil {
95 | r0 = ret.Get(0).(model.TenantDtos)
96 | }
97 | }
98 |
99 | var r1 error
100 | if rf, ok := ret.Get(1).(func(common.Pagination) error); ok {
101 | r1 = rf(page)
102 | } else {
103 | r1 = ret.Error(1)
104 | }
105 |
106 | return r0, r1
107 | }
108 |
109 | // Patch provides a mock function with given fields: form, id
110 | func (_m *ITenantService) Patch(form *model.TenantPatchForm, id int) (*model.TenantDto, error) {
111 | ret := _m.Called(form, id)
112 |
113 | var r0 *model.TenantDto
114 | if rf, ok := ret.Get(0).(func(*model.TenantPatchForm, int) *model.TenantDto); ok {
115 | r0 = rf(form, id)
116 | } else {
117 | if ret.Get(0) != nil {
118 | r0 = ret.Get(0).(*model.TenantDto)
119 | }
120 | }
121 |
122 | var r1 error
123 | if rf, ok := ret.Get(1).(func(*model.TenantPatchForm, int) error); ok {
124 | r1 = rf(form, id)
125 | } else {
126 | r1 = ret.Error(1)
127 | }
128 |
129 | return r0, r1
130 | }
131 |
132 | // Register provides a mock function with given fields: tenant
133 | func (_m *ITenantService) Register(tenant *model.TenantRegisterForm) (*model.TenantDto, error) {
134 | ret := _m.Called(tenant)
135 |
136 | var r0 *model.TenantDto
137 | if rf, ok := ret.Get(0).(func(*model.TenantRegisterForm) *model.TenantDto); ok {
138 | r0 = rf(tenant)
139 | } else {
140 | if ret.Get(0) != nil {
141 | r0 = ret.Get(0).(*model.TenantDto)
142 | }
143 | }
144 |
145 | var r1 error
146 | if rf, ok := ret.Get(1).(func(*model.TenantRegisterForm) error); ok {
147 | r1 = rf(tenant)
148 | } else {
149 | r1 = ret.Error(1)
150 | }
151 |
152 | return r0, r1
153 | }
154 |
155 | // Update provides a mock function with given fields: form, id
156 | func (_m *ITenantService) Update(form *model.TenantForm, id int) (*model.TenantDto, error) {
157 | ret := _m.Called(form, id)
158 |
159 | var r0 *model.TenantDto
160 | if rf, ok := ret.Get(0).(func(*model.TenantForm, int) *model.TenantDto); ok {
161 | r0 = rf(form, id)
162 | } else {
163 | if ret.Get(0) != nil {
164 | r0 = ret.Get(0).(*model.TenantDto)
165 | }
166 | }
167 |
168 | var r1 error
169 | if rf, ok := ret.Get(1).(func(*model.TenantForm, int) error); ok {
170 | r1 = rf(form, id)
171 | } else {
172 | r1 = ret.Error(1)
173 | }
174 |
175 | return r0, r1
176 | }
177 |
--------------------------------------------------------------------------------
/account-service/module/tenant/model/tenant.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Tenants []*Tenant
10 |
11 | type Tenant struct {
12 | gorm.Model
13 | Name string
14 | Code string
15 | Email string
16 | Domain string
17 | Secret string
18 | IsActive bool
19 | CreatedAt time.Time
20 | UpdatedAt time.Time
21 | }
22 |
23 | type TenantDtos []*TenantDto
24 |
25 | type TenantDto struct {
26 | ID uint `json:"id" example:"123"`
27 | Name string `json:"name" example:"Tenant 1"`
28 | Domain string `json:"domain" example:"EBOOK"`
29 | Code string `json:"code" example:"Tenant1"`
30 | Email string `json:"email" example:"tenant@mail.com"`
31 | IsActive bool `json:"isActive" example:true`
32 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
33 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
34 | }
35 |
36 | type TenantForm struct {
37 | Name string `json:"name" example:"Tenant 1" valid:"Required;MinSize(5);MaxSize(255)"`
38 | Code string `json:"code" example:"Tenant1" valid:"Required;MinSize(3);MaxSize(50)"`
39 | Domain string `json:"domain" example:"EBOOK" valid:"Required;MinSize(3);MaxSize(50)"`
40 | Email string `json:"email" example:"tenant@mail.com" valid:"Required;Email;"`
41 | }
42 |
43 | type TenantPatchForm struct {
44 | Name string `json:"name" example:"Tenant 1" valid:"MinSize(5);MaxSize(255)"`
45 | Code string `json:"code" example:"Tenant1" valid:"MinSize(3);MaxSize(50"`
46 | Domain string `json:"domain" example:"EBOOK" valid:"Required;MinSize(3);MaxSize(50)"`
47 | Email string `json:"email" example:"tenant@mail.com" valid:"Email"`
48 | }
49 |
50 | type TenantRegisterForm struct {
51 | Name string `json:"name" example:"Tenant1" valid:"Required;MinSize(5);MaxSize(255)"`
52 | Password string `json:"password" example:"Pass@1" valid:"Required;MinSize(5);MaxSize(255)"`
53 | Domain string `json:"domain" example:"eBook" valid:"Required;MinSize(3);MaxSize(50)"`
54 | Email string `json:"email" example:"tenant1@mail.com" valid:"Required;Email;"`
55 | FirstName string `json:"firstName" example:"John" valid:"MinSize(2);MaxSize(255)"`
56 | LastName string `json:"lastName" example:"Doe" valid:"MaxSize(255)"`
57 | }
58 |
59 | func (f *TenantForm) ToModel() (*Tenant, error) {
60 | return &Tenant{
61 | Name: f.Name,
62 | Code: f.Code,
63 | Email: f.Email,
64 | Domain: f.Domain,
65 | }, nil
66 | }
67 |
68 | func (f *TenantPatchForm) ToModel() (*Tenant, error) {
69 | return &Tenant{
70 | Name: f.Name,
71 | Code: f.Code,
72 | Email: f.Email,
73 | }, nil
74 | }
75 |
76 | func (b Tenant) ToDto() *TenantDto {
77 | return &TenantDto{
78 | ID: b.ID,
79 | Name: b.Name,
80 | Code: b.Code,
81 | Domain: b.Domain,
82 | Email: b.Email,
83 | IsActive: b.IsActive,
84 | CreatedAt: b.CreatedAt,
85 | UpdatedAt: b.UpdatedAt,
86 | }
87 | }
88 |
89 | func (bs Tenants) ToDto() TenantDtos {
90 | dtos := make([]*TenantDto, len(bs))
91 | for i, b := range bs {
92 | dtos[i] = b.ToDto()
93 | }
94 |
95 | return dtos
96 | }
97 |
98 | func (b TenantRegisterForm) ToTenantForm() *TenantForm {
99 | return &TenantForm{
100 | Name: b.Name,
101 | Domain: b.Domain,
102 | Email: b.Email,
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/account-service/module/tenant/repo/tenant.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "micro/module/tenant/model"
5 |
6 | "gorm.io/gorm"
7 |
8 | guuid "github.com/google/uuid"
9 | common "github.com/krishnarajvr/micro-common"
10 | )
11 |
12 | type ITenantRepo interface {
13 | List(page common.Pagination) (model.Tenants, error)
14 | Get(id int) (*model.Tenant, error)
15 | Add(form *model.TenantForm) (*model.Tenant, error)
16 | Update(form *model.TenantForm, id int) (*model.Tenant, error)
17 | Patch(form *model.TenantPatchForm, id int) (*model.Tenant, error)
18 | Delete(id int) (*model.Tenant, error)
19 | }
20 |
21 | type TenantRepo struct {
22 | DB *gorm.DB
23 | }
24 |
25 | func NewTenantRepo(db *gorm.DB) TenantRepo {
26 | return TenantRepo{
27 | DB: db,
28 | }
29 | }
30 |
31 | func (r TenantRepo) List(page common.Pagination) (model.Tenants, error) {
32 |
33 | tenants := make([]*model.Tenant, 0)
34 | if err := r.DB.Scopes(common.Paginate(page)).Find(&tenants).Error; err != nil {
35 | return nil, err
36 | }
37 |
38 | if len(tenants) == 0 {
39 | return nil, nil
40 | }
41 |
42 | return tenants, nil
43 | }
44 |
45 | func (r TenantRepo) Get(id int) (*model.Tenant, error) {
46 | tenant := new(model.Tenant)
47 | if err := r.DB.Where("id = ?", id).First(&tenant).Error; err != nil {
48 | return nil, err
49 | }
50 |
51 | return tenant, nil
52 | }
53 |
54 | func (r TenantRepo) Delete(id int) (*model.Tenant, error) {
55 | tenant := new(model.Tenant)
56 | if err := r.DB.Where("id = ?", id).Delete(&tenant).Error; err != nil {
57 | return nil, err
58 | }
59 |
60 | return tenant, nil
61 | }
62 |
63 | func (r TenantRepo) Add(form *model.TenantForm) (*model.Tenant, error) {
64 | tenant, err := form.ToModel()
65 | id := guuid.New()
66 | tenant.Secret = id.String()
67 |
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | if err := r.DB.Create(&tenant).Error; err != nil {
73 | return nil, err
74 | }
75 |
76 | return tenant, nil
77 | }
78 |
79 | func (r TenantRepo) Update(form *model.TenantForm, id int) (*model.Tenant, error) {
80 | tenant, err := form.ToModel()
81 |
82 | if err != nil {
83 | return nil, err
84 | }
85 |
86 | if err := r.DB.Where("id = ?", id).Updates(&tenant).Error; err != nil {
87 | return nil, err
88 | }
89 |
90 | return tenant, nil
91 | }
92 |
93 | func (r TenantRepo) Patch(form *model.TenantPatchForm, id int) (*model.Tenant, error) {
94 | tenant, err := form.ToModel()
95 |
96 | if err != nil {
97 | return nil, err
98 | }
99 |
100 | if err := r.DB.Where("id = ?", id).Updates(&tenant).Error; err != nil {
101 | return nil, err
102 | }
103 |
104 | return tenant, nil
105 | }
106 |
--------------------------------------------------------------------------------
/account-service/module/tenant/routes.go:
--------------------------------------------------------------------------------
1 | package tenant
2 |
3 | //InitRoutes - initialize routes for the module
4 | func InitRoutes(c HandlerConfig) {
5 | h := Handler{
6 | TenantService: c.TenantService,
7 | Lang: c.Lang,
8 | }
9 |
10 | // Create service group
11 | g := c.R.Group(c.BaseURL)
12 |
13 | g.GET("/tenants/:id", h.GetTenant)
14 | g.POST("/tenants/:id", h.UpdateTenant)
15 | g.PATCH("/tenants/:id", h.PatchTenant)
16 | g.DELETE("/tenants/:id", h.DeleteTenant)
17 | g.POST("/tenants", h.AddTenant)
18 | g.POST("/tenantRegister", h.RegisterTenant)
19 | g.GET("/tenants", h.ListTenants)
20 | g.GET("/jwks", h.GetJwks)
21 | }
22 |
--------------------------------------------------------------------------------
/account-service/module/tenant/service/tenant_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "log"
5 | "regexp"
6 |
7 | common "github.com/krishnarajvr/micro-common"
8 |
9 | "github.com/krishnarajvr/micro-common/locale"
10 | "micro/module/tenant/model"
11 | "micro/module/tenant/repo"
12 | userModel "micro/module/user/model"
13 | userRepo "micro/module/user/repo"
14 | "micro/util/password"
15 | )
16 |
17 | type ITenantService interface {
18 | List(page common.Pagination) (model.TenantDtos, error)
19 | Get(id int) (*model.TenantDto, error)
20 | Add(tenant *model.TenantForm) (*model.TenantDto, error)
21 | Register(tenant *model.TenantRegisterForm) (*model.TenantDto, error)
22 | Update(form *model.TenantForm, id int) (*model.TenantDto, error)
23 | Patch(form *model.TenantPatchForm, id int) (*model.TenantDto, error)
24 | Delete(id int) (*model.TenantDto, error)
25 | }
26 |
27 | type ServiceConfig struct {
28 | TenantRepo repo.ITenantRepo
29 | UserRepo userRepo.IUserRepo
30 | Lang *locale.Locale
31 | }
32 |
33 | type Service struct {
34 | TenantRepo repo.ITenantRepo
35 | UserRepo userRepo.IUserRepo
36 | Lang *locale.Locale
37 | }
38 |
39 | func NewService(c ServiceConfig) ITenantService {
40 | return &Service{
41 | TenantRepo: c.TenantRepo,
42 | UserRepo: c.UserRepo,
43 | Lang: c.Lang,
44 | }
45 | }
46 |
47 | func (s *Service) List(page common.Pagination) (model.TenantDtos, error) {
48 |
49 | tenants, err := s.TenantRepo.List(page)
50 |
51 | if err != nil {
52 | return nil, err
53 | }
54 |
55 | log.Println(s.Lang.Get("hi", "GetAll"))
56 |
57 | m := map[string]interface{}{
58 | "Name": "John Doe",
59 | "Age": 66,
60 | }
61 |
62 | s.Lang.SetLang("en-US")
63 | log.Println(s.Lang.Get("intro", m))
64 |
65 | tenantsDto := tenants.ToDto()
66 |
67 | return tenantsDto, err
68 | }
69 |
70 | func (s *Service) Get(id int) (*model.TenantDto, error) {
71 |
72 | tenant, err := s.TenantRepo.Get(id)
73 |
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | tenantDto := tenant.ToDto()
79 |
80 | return tenantDto, nil
81 | }
82 |
83 | func (s *Service) Add(form *model.TenantForm) (*model.TenantDto, error) {
84 |
85 | tenant, err := s.TenantRepo.Add(form)
86 |
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | tenantDto := tenant.ToDto()
92 |
93 | return tenantDto, nil
94 | }
95 |
96 | //Register a tenant which will create a root user as well
97 | func (s *Service) Register(form *model.TenantRegisterForm) (*model.TenantDto, error) {
98 |
99 | tenantForm := form.ToTenantForm()
100 |
101 | // Make a Regex to say we only want letters and numbers
102 | reg, err := regexp.Compile("[^a-zA-Z0-9]+")
103 | if err != nil {
104 | log.Fatal(err)
105 | return nil, err
106 | }
107 | code := reg.ReplaceAllString(tenantForm.Name, "")
108 | tenantForm.Code = code
109 |
110 | tenant, err := s.TenantRepo.Add(tenantForm)
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | userForm := userModel.UserForm{
116 | Name: form.Name,
117 | Email: form.Email,
118 | FirstName: form.FirstName,
119 | LastName: form.LastName,
120 | }
121 | userModel, err := userForm.ToModel()
122 | userModel.TenantId = tenant.ID
123 | userModel.Password = password.Encrypt(form.Password)
124 |
125 | _, errUser := s.UserRepo.Add(userModel)
126 |
127 | if errUser != nil {
128 | //Todo - Rollback Added Tenant
129 | log.Println(errUser)
130 | return nil, errUser
131 | }
132 |
133 | tenantDto := tenant.ToDto()
134 |
135 | return tenantDto, nil
136 | }
137 |
138 | func (s *Service) Update(form *model.TenantForm, id int) (*model.TenantDto, error) {
139 |
140 | tenant, err := s.TenantRepo.Update(form, id)
141 |
142 | if err != nil {
143 | return nil, err
144 | }
145 |
146 | tenantDto := tenant.ToDto()
147 |
148 | return tenantDto, nil
149 | }
150 |
151 | func (s *Service) Patch(form *model.TenantPatchForm, id int) (*model.TenantDto, error) {
152 |
153 | tenant, err := s.TenantRepo.Patch(form, id)
154 |
155 | if err != nil {
156 | return nil, err
157 | }
158 |
159 | tenantDto := tenant.ToDto()
160 |
161 | return tenantDto, nil
162 | }
163 |
164 | func (s *Service) Delete(id int) (*model.TenantDto, error) {
165 |
166 | tenant, err := s.TenantRepo.Delete(id)
167 |
168 | if err != nil {
169 | return nil, err
170 | }
171 |
172 | tenantDto := tenant.ToDto()
173 |
174 | return tenantDto, nil
175 | }
176 |
--------------------------------------------------------------------------------
/account-service/module/tenant/service/tenant_service_test.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/krishnarajvr/micro-common/locale"
7 | "micro/config"
8 | "micro/module/tenant/mocks"
9 | "micro/module/tenant/model"
10 |
11 | common "github.com/krishnarajvr/micro-common"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestTenant(t *testing.T) {
16 | t.Run("Success", func(t *testing.T) {
17 |
18 | mockTenantResp := &model.Tenant{
19 | Name: "Tenant 1",
20 | Code: "Code 1",
21 | }
22 |
23 | page := common.Pagination{
24 | Sort: "ID",
25 | Order: "DESC",
26 | Offset: "0",
27 | Limit: "25",
28 | Search: "",
29 | }
30 |
31 | products := model.Tenants{mockTenantResp}
32 | productDtos := products.ToDto()
33 |
34 | ITenantRepo := new(mocks.ITenantRepo)
35 |
36 | ITenantRepo.On("List", page).Return(products, nil)
37 |
38 | appConf := config.AppConfig()
39 | langLocale := locale.Locale{}
40 | lang := langLocale.New(appConf.App.Lang)
41 |
42 | ps := NewService(ServiceConfig{
43 | TenantRepo: ITenantRepo,
44 | Lang: lang,
45 | })
46 |
47 | u, err := ps.List(page)
48 |
49 | assert.NoError(t, err)
50 | assert.Equal(t, u, productDtos)
51 | ITenantRepo.AssertExpectations(t)
52 | })
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/account-service/module/tenant/swagger/tenant.go:
--------------------------------------------------------------------------------
1 | package swagger
2 |
3 | import (
4 | "micro/module/tenant/model"
5 | )
6 |
7 | type TenantSampleData struct {
8 | TenantData model.TenantDto `json:"tenant"`
9 | }
10 |
11 | type TenantSampleListData struct {
12 | TenantData model.TenantDtos `json:"tenants"`
13 | }
14 |
15 | type TenantListResponse struct {
16 | Status uint `json:"status" example:"200"`
17 | Error interface{} `json:"error"`
18 | Data TenantSampleListData `json:"data"`
19 | }
20 |
21 | type TenantResponse struct {
22 | Status uint `json:"status" example:"200"`
23 | Error interface{} `json:"error"`
24 | Data TenantSampleData `json:"data"`
25 | }
26 |
--------------------------------------------------------------------------------
/account-service/module/user/handler_test.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "micro/module/user/mocks"
8 | "micro/module/user/model"
9 | "net/http"
10 | "net/http/httptest"
11 | "testing"
12 |
13 | "micro/config"
14 |
15 | "github.com/gin-gonic/gin"
16 | common "github.com/krishnarajvr/micro-common"
17 | "github.com/krishnarajvr/micro-common/middleware"
18 | "github.com/stretchr/testify/assert"
19 | )
20 |
21 | func TestUser(t *testing.T) {
22 | // Setup
23 | gin.SetMode(gin.TestMode)
24 |
25 | t.Run("List user", func(t *testing.T) {
26 |
27 | mockUserResp := &model.UserDto{
28 | ID: 1,
29 | Name: "User 1",
30 | }
31 | dtos := model.UserDtos{mockUserResp}
32 |
33 | mockUserService := new(mocks.IUserService)
34 |
35 | page := common.Pagination{
36 | Sort: "ID",
37 | Order: "DESC",
38 | Offset: "0",
39 | Limit: "25",
40 | Search: "",
41 | }
42 |
43 | mockUserService.On("List", page).Return(dtos, nil, nil)
44 |
45 | // a response recorder for getting written http response
46 | rr := httptest.NewRecorder()
47 |
48 | cfg := config.AppConfig()
49 | router := gin.Default()
50 | router.Use(middleware.LoggerToFile(cfg.Log.LogFilePath, cfg.Log.LogFileName))
51 |
52 | InitRoutes(HandlerConfig{
53 | R: router,
54 | UserService: mockUserService,
55 | })
56 |
57 | // create a request body with empty email and password
58 | reqBody, err := json.Marshal(gin.H{
59 | "user": "",
60 | })
61 |
62 | assert.NoError(t, err)
63 |
64 | // use bytes.NewBuffer to create a reader
65 | request, err := http.NewRequest(http.MethodGet, "/users", bytes.NewBuffer(reqBody))
66 | assert.NoError(t, err)
67 |
68 | request.Header.Set("Content-Type", "application/json")
69 |
70 | router.ServeHTTP(rr, request)
71 |
72 | assert.Equal(t, 200, rr.Code)
73 | })
74 |
75 | t.Run("List user Error", func(t *testing.T) {
76 |
77 | mockUserService := new(mocks.IUserService)
78 |
79 | page := common.Pagination{
80 | Sort: "ID",
81 | Order: "DESC",
82 | Offset: "0",
83 | Limit: "25",
84 | Search: "",
85 | }
86 |
87 | mockUserService.On("List", page).Return(nil, nil, fmt.Errorf("Some error down call chain"))
88 |
89 | // a response recorder for getting written http response
90 | rr := httptest.NewRecorder()
91 |
92 | cfg := config.AppConfig()
93 | router := gin.Default()
94 | router.Use(middleware.LoggerToFile(cfg.Log.LogFilePath, cfg.Log.LogFileName))
95 |
96 | InitRoutes(HandlerConfig{
97 | R: router,
98 | UserService: mockUserService,
99 | })
100 |
101 | // use bytes.NewBuffer to create a reader
102 | request, err := http.NewRequest(http.MethodGet, "/users", bytes.NewBuffer(nil))
103 | assert.NoError(t, err)
104 |
105 | request.Header.Set("Content-Type", "application/json")
106 |
107 | router.ServeHTTP(rr, request)
108 |
109 | fmt.Println(rr)
110 |
111 | assert.Equal(t, 200, rr.Code)
112 |
113 | })
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/account-service/module/user/inject.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "micro/app"
5 | "micro/module/user/repo"
6 | "micro/module/user/service"
7 | "micro/util/token"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/krishnarajvr/micro-common/locale"
11 | )
12 |
13 | type HandlerConfig struct {
14 | R *gin.Engine
15 | UserService service.IUserService
16 | BaseURL string
17 | Lang *locale.Locale
18 | }
19 |
20 | //Inject dependencies
21 | func Inject(appConfig app.AppConfig) {
22 |
23 | userRepo := repo.NewUserRepo(appConfig.Dbs.DB)
24 | tokenRepo := repo.NewTokenRepo(appConfig.Dbs.DB)
25 |
26 | jwtToken := token.New(token.TokenConfig{
27 | TokenRepo: tokenRepo,
28 | Cache: appConfig.Dbs.Cache,
29 | Config: appConfig.Cfg,
30 | })
31 |
32 | userService := service.NewService(service.ServiceConfig{
33 | UserRepo: userRepo,
34 | Lang: appConfig.Lang,
35 | Token: jwtToken,
36 | })
37 |
38 | InitRoutes(HandlerConfig{
39 | R: appConfig.Router,
40 | UserService: userService,
41 | BaseURL: appConfig.BaseURL,
42 | Lang: appConfig.Lang,
43 | })
44 | }
45 |
--------------------------------------------------------------------------------
/account-service/module/user/mocks/IUserRepo.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | common "github.com/krishnarajvr/micro-common"
7 | mock "github.com/stretchr/testify/mock"
8 |
9 | model "micro/module/user/model"
10 | )
11 |
12 | // IUserRepo is an autogenerated mock type for the IUserRepo type
13 | type IUserRepo struct {
14 | mock.Mock
15 | }
16 |
17 | // Add provides a mock function with given fields: form
18 | func (_m *IUserRepo) Add(form *model.User) (*model.User, error) {
19 | ret := _m.Called(form)
20 |
21 | var r0 *model.User
22 | if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok {
23 | r0 = rf(form)
24 | } else {
25 | if ret.Get(0) != nil {
26 | r0 = ret.Get(0).(*model.User)
27 | }
28 | }
29 |
30 | var r1 error
31 | if rf, ok := ret.Get(1).(func(*model.User) error); ok {
32 | r1 = rf(form)
33 | } else {
34 | r1 = ret.Error(1)
35 | }
36 |
37 | return r0, r1
38 | }
39 |
40 | // Delete provides a mock function with given fields: id
41 | func (_m *IUserRepo) Delete(id int) (*model.User, error) {
42 | ret := _m.Called(id)
43 |
44 | var r0 *model.User
45 | if rf, ok := ret.Get(0).(func(int) *model.User); ok {
46 | r0 = rf(id)
47 | } else {
48 | if ret.Get(0) != nil {
49 | r0 = ret.Get(0).(*model.User)
50 | }
51 | }
52 |
53 | var r1 error
54 | if rf, ok := ret.Get(1).(func(int) error); ok {
55 | r1 = rf(id)
56 | } else {
57 | r1 = ret.Error(1)
58 | }
59 |
60 | return r0, r1
61 | }
62 |
63 | // Get provides a mock function with given fields: tenantId, id
64 | func (_m *IUserRepo) Get(tenantId int, id int) (*model.User, error) {
65 | ret := _m.Called(tenantId, id)
66 |
67 | var r0 *model.User
68 | if rf, ok := ret.Get(0).(func(int, int) *model.User); ok {
69 | r0 = rf(tenantId, id)
70 | } else {
71 | if ret.Get(0) != nil {
72 | r0 = ret.Get(0).(*model.User)
73 | }
74 | }
75 |
76 | var r1 error
77 | if rf, ok := ret.Get(1).(func(int, int) error); ok {
78 | r1 = rf(tenantId, id)
79 | } else {
80 | r1 = ret.Error(1)
81 | }
82 |
83 | return r0, r1
84 | }
85 |
86 | // GetByEmail provides a mock function with given fields: email
87 | func (_m *IUserRepo) GetByEmail(email string) (*model.User, error) {
88 | ret := _m.Called(email)
89 |
90 | var r0 *model.User
91 | if rf, ok := ret.Get(0).(func(string) *model.User); ok {
92 | r0 = rf(email)
93 | } else {
94 | if ret.Get(0) != nil {
95 | r0 = ret.Get(0).(*model.User)
96 | }
97 | }
98 |
99 | var r1 error
100 | if rf, ok := ret.Get(1).(func(string) error); ok {
101 | r1 = rf(email)
102 | } else {
103 | r1 = ret.Error(1)
104 | }
105 |
106 | return r0, r1
107 | }
108 |
109 | // List provides a mock function with given fields: tenantId, page, filters
110 | func (_m *IUserRepo) List(tenantId int, page common.Pagination, filters model.UserFilterList) (model.Users, *common.PageResult, error) {
111 | ret := _m.Called(tenantId, page, filters)
112 |
113 | var r0 model.Users
114 | if rf, ok := ret.Get(0).(func(int, common.Pagination, model.UserFilterList) model.Users); ok {
115 | r0 = rf(tenantId, page, filters)
116 | } else {
117 | if ret.Get(0) != nil {
118 | r0 = ret.Get(0).(model.Users)
119 | }
120 | }
121 |
122 | var r1 *common.PageResult
123 | if rf, ok := ret.Get(1).(func(int, common.Pagination, model.UserFilterList) *common.PageResult); ok {
124 | r1 = rf(tenantId, page, filters)
125 | } else {
126 | if ret.Get(1) != nil {
127 | r1 = ret.Get(1).(*common.PageResult)
128 | }
129 | }
130 |
131 | var r2 error
132 | if rf, ok := ret.Get(2).(func(int, common.Pagination, model.UserFilterList) error); ok {
133 | r2 = rf(tenantId, page, filters)
134 | } else {
135 | r2 = ret.Error(2)
136 | }
137 |
138 | return r0, r1, r2
139 | }
140 |
141 | // Patch provides a mock function with given fields: form, id
142 | func (_m *IUserRepo) Patch(form *model.UserPatchForm, id int) (*model.User, error) {
143 | ret := _m.Called(form, id)
144 |
145 | var r0 *model.User
146 | if rf, ok := ret.Get(0).(func(*model.UserPatchForm, int) *model.User); ok {
147 | r0 = rf(form, id)
148 | } else {
149 | if ret.Get(0) != nil {
150 | r0 = ret.Get(0).(*model.User)
151 | }
152 | }
153 |
154 | var r1 error
155 | if rf, ok := ret.Get(1).(func(*model.UserPatchForm, int) error); ok {
156 | r1 = rf(form, id)
157 | } else {
158 | r1 = ret.Error(1)
159 | }
160 |
161 | return r0, r1
162 | }
163 |
164 | // Update provides a mock function with given fields: form, id
165 | func (_m *IUserRepo) Update(form *model.UserForm, id int) (*model.User, error) {
166 | ret := _m.Called(form, id)
167 |
168 | var r0 *model.User
169 | if rf, ok := ret.Get(0).(func(*model.UserForm, int) *model.User); ok {
170 | r0 = rf(form, id)
171 | } else {
172 | if ret.Get(0) != nil {
173 | r0 = ret.Get(0).(*model.User)
174 | }
175 | }
176 |
177 | var r1 error
178 | if rf, ok := ret.Get(1).(func(*model.UserForm, int) error); ok {
179 | r1 = rf(form, id)
180 | } else {
181 | r1 = ret.Error(1)
182 | }
183 |
184 | return r0, r1
185 | }
186 |
--------------------------------------------------------------------------------
/account-service/module/user/model/token.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Token struct {
10 | gorm.Model
11 | TenantId uint
12 | Name string
13 | Email string
14 | FirstName string
15 | LastName string
16 | Password string
17 | IsActive bool
18 | CreatedAt time.Time
19 | UpdatedAt time.Time
20 | }
21 |
--------------------------------------------------------------------------------
/account-service/module/user/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Users []*User
10 |
11 | type User struct {
12 | gorm.Model
13 | TenantId uint
14 | Name string
15 | Email string
16 | FirstName string
17 | LastName string
18 | Password string
19 | IsActive bool
20 | CreatedAt time.Time
21 | UpdatedAt time.Time
22 | }
23 |
24 | type UserDtos []*UserDto
25 |
26 | type UserDto struct {
27 | ID uint `json:"id" example:"1"`
28 | Name string `json:"name" example:"User 1"`
29 | Email string `json:"email" example:"user@mail.com"`
30 | FirstName string `json:"firstName" example:"John"`
31 | LastName string `json:"lastName" example:"Doe"`
32 | IsActive bool `json:"isActive" example:true`
33 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
34 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
35 | }
36 |
37 | func (b User) ToDto() *UserDto {
38 | return &UserDto{
39 | ID: b.ID,
40 | Name: b.Name,
41 | Email: b.Email,
42 | FirstName: b.FirstName,
43 | LastName: b.LastName,
44 | IsActive: b.IsActive,
45 | CreatedAt: b.CreatedAt,
46 | UpdatedAt: b.UpdatedAt,
47 | }
48 | }
49 |
50 | type UserForm struct {
51 | Name string `json:"name" example:"User 1" valid:"Required;MinSize(2);MaxSize(255)"`
52 | Email string `json:"email" example:"john@mail.com" valid:"Required;Email;"`
53 | FirstName string `json:"firstName" example:"John" valid:"Required;MinSize(2);MaxSize(255)"`
54 | LastName string `json:"lastName" example:"Doe" valid:"MaxSize(255)"`
55 | Password string `json:"password" example:"Pass!23" valid:"MinSize(5);MaxSize(20)"`
56 | }
57 |
58 | type LoginForm struct {
59 | Email string `json:"email" label:"email" example:"john@mail.com" valid:"Required;Email;"`
60 | Password string `json:"password" label:"password" example:"john123" valid:"MinSize(5);MaxSize(20)"`
61 | }
62 |
63 | type UserPatchForm struct {
64 | Name string `json:"name" example:"User 1" valid:"MinSize(2);MaxSize(255)"`
65 | Email string `json:"email" example:"john@mail.com" valid:"Email;"`
66 | FirstName string `json:"firstName" example:"John" valid:"MinSize(2);MaxSize(255)"`
67 | LastName string `json:"lastName" example:"Doe" valid:"MaxSize(255)"`
68 | }
69 |
70 | type UserFilterList struct {
71 | Name string `json:"name" form:"name" example:"John"`
72 | Email string `json:"email" form:"email" example:"john@mail.com"`
73 | }
74 |
75 | type AuthorizeData struct {
76 | Uri string `json:"uri" form:"uri" valid:"MinSize(2);`
77 | Method string `json:"method" form:"method" valid:"MinSize(2);MaxSize(10)"`
78 | }
79 |
80 | type TokenResponse struct {
81 | AccessToken string `json:"access_token" example:"eyJhbGciOsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NfdXVpZCI6ImIyY2Fik5MmItNGZiZi1.v_4EzMc1HG9mJZCGNk0UKnqTveOAjtgIO9Za4tHDBY"`
82 | RefreshToken string `json:"refresh_token" example:"eyJhbGcIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTUzNjA2MzQsInJlZfdXVpZCI6ImIyY2FyZWMzNnVzZXJfaWQiOjF9.hv_4EzMc1HG9mJZCGNk0UKnqTveOAjtgIO9Za4tHDBY"`
83 | }
84 |
85 | type TokenRefresh struct {
86 | RefreshToken string `json:"refresh_token" example:"eyJhbGcIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTUzNjA2MzQsInJlZfdXVpZCI6ImIyY2FyZWMzNnVzZXJfaWQiOjF9.hv_4EzMc1HG9mJZCGNk0UKnqTveOAjtgIO9Za4tHDBY"`
87 | }
88 |
89 | type AuthRespose struct {
90 | TenantId string `json:"tenantId"`
91 | UserId string `json:"userId"`
92 | ReferenceId string `json:"referenceId"`
93 | Type string `json:"type"`
94 | Subject string `json:"subject"`
95 | Roles []string `json:"roles"`
96 | Admin bool `json:"admin"`
97 | }
98 |
99 | func (bs Users) ToDto() UserDtos {
100 | dtos := make([]*UserDto, len(bs))
101 | for i, b := range bs {
102 | dtos[i] = b.ToDto()
103 | }
104 |
105 | return dtos
106 | }
107 |
108 | func (f *UserForm) ToModel() (*User, error) {
109 | return &User{
110 | Name: f.Name,
111 | Email: f.Email,
112 | FirstName: f.FirstName,
113 | LastName: f.LastName,
114 | }, nil
115 | }
116 |
117 | func (f *UserPatchForm) ToModel() (*User, error) {
118 | return &User{
119 | Name: f.Name,
120 | Email: f.Email,
121 | FirstName: f.FirstName,
122 | LastName: f.LastName,
123 | }, nil
124 | }
125 |
--------------------------------------------------------------------------------
/account-service/module/user/repo/token.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "micro/module/user/model"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type ITokenRepo interface {
10 | Get(tenantId int, id int) (*model.Token, error)
11 | }
12 |
13 | type TokenRepo struct {
14 | DB *gorm.DB
15 | }
16 |
17 | func NewTokenRepo(db *gorm.DB) TokenRepo {
18 | return TokenRepo{
19 | DB: db,
20 | }
21 | }
22 |
23 | func (r TokenRepo) Get(tenantId int, id int) (*model.Token, error) {
24 | token := new(model.Token)
25 | err := r.DB.
26 | Where("id = ?", id).
27 | Where("tenant_id = ? ", tenantId).
28 | First(&token).Error
29 |
30 | if err != nil {
31 | return nil, err
32 | }
33 |
34 | return token, nil
35 | }
36 |
--------------------------------------------------------------------------------
/account-service/module/user/repo/user.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "micro/module/user/model"
5 |
6 | "gorm.io/gorm"
7 |
8 | common "github.com/krishnarajvr/micro-common"
9 | )
10 |
11 | type IUserRepo interface {
12 | List(tenantId int, page common.Pagination, filters model.UserFilterList) (model.Users, *common.PageResult, error)
13 | Get(tenantId int, id int) (*model.User, error)
14 | Add(form *model.User) (*model.User, error)
15 | Update(form *model.UserForm, id int) (*model.User, error)
16 | Patch(form *model.UserPatchForm, id int) (*model.User, error)
17 | Delete(id int) (*model.User, error)
18 | GetByEmail(email string) (*model.User, error)
19 | }
20 |
21 | type UserRepo struct {
22 | DB *gorm.DB
23 | }
24 |
25 | func NewUserRepo(db *gorm.DB) UserRepo {
26 | return UserRepo{
27 | DB: db,
28 | }
29 | }
30 |
31 | func (r UserRepo) List(tenantId int, page common.Pagination, filters model.UserFilterList) (model.Users, *common.PageResult, error) {
32 | users := make([]*model.User, 0)
33 | var totalCount int64
34 |
35 | db := r.DB.Scopes(common.Paginate(page)).
36 | Find(&users).
37 | Where("tenant_id = ? ", tenantId)
38 |
39 | if len(filters.Name) > 0 {
40 | db = db.Where("name like ?", filters.Name)
41 | }
42 |
43 | if len(filters.Email) > 0 {
44 | db = db.Where("email like ?", filters.Email)
45 | }
46 |
47 | err := db.Find(&users).Count(&totalCount).Error
48 |
49 | if err != nil {
50 | return nil, nil, err
51 | }
52 |
53 | pageResult := common.PageInfo(page, totalCount)
54 |
55 | if len(users) == 0 {
56 | return nil, nil, nil
57 | }
58 |
59 | return users, &pageResult, nil
60 | }
61 |
62 | func (r UserRepo) Get(tenantId int, id int) (*model.User, error) {
63 | user := new(model.User)
64 | err := r.DB.
65 | Where("id = ?", id).
66 | Where("tenant_id = ? ", tenantId).
67 | First(&user).Error
68 |
69 | if err != nil {
70 | return nil, err
71 | }
72 |
73 | return user, nil
74 | }
75 |
76 | func (r UserRepo) Delete(id int) (*model.User, error) {
77 | user := new(model.User)
78 | if err := r.DB.Where("id = ?", id).Delete(&user).Error; err != nil {
79 | return nil, err
80 | }
81 |
82 | return user, nil
83 | }
84 |
85 | func (r UserRepo) Add(user *model.User) (*model.User, error) {
86 |
87 | if err := r.DB.Create(&user).Error; err != nil {
88 | return nil, err
89 | }
90 |
91 | return user, nil
92 | }
93 |
94 | func (r UserRepo) Update(form *model.UserForm, id int) (*model.User, error) {
95 | user, err := form.ToModel()
96 |
97 | if err != nil {
98 | return nil, err
99 | }
100 |
101 | if err := r.DB.Where("id = ?", id).Updates(&user).Error; err != nil {
102 | return nil, err
103 | }
104 |
105 | return user, nil
106 | }
107 |
108 | func (r UserRepo) Patch(form *model.UserPatchForm, id int) (*model.User, error) {
109 | user, err := form.ToModel()
110 |
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | if err := r.DB.Where("id = ?", id).Updates(&user).Error; err != nil {
116 | return nil, err
117 | }
118 |
119 | return user, nil
120 | }
121 |
122 | func (r UserRepo) GetByEmail(email string) (*model.User, error) {
123 | user := new(model.User)
124 | if err := r.DB.Where("email = ?", email).First(&user).Error; err != nil {
125 | return nil, err
126 | }
127 |
128 | return user, nil
129 | }
130 |
--------------------------------------------------------------------------------
/account-service/module/user/routes.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | //InitRoutes for the module
4 | func InitRoutes(c HandlerConfig) {
5 | h := Handler{
6 | UserService: c.UserService,
7 | Lang: c.Lang,
8 | }
9 | //Set api group
10 | g := c.R.Group(c.BaseURL)
11 |
12 | g.GET("/users/:id", h.GetUser)
13 | g.POST("/users/:id", h.UpdateUser)
14 | g.PATCH("/users/:id", h.PatchUser)
15 | g.DELETE("/users/:id", h.DeleteUser)
16 | g.POST("/users", h.AddUser)
17 | g.GET("/users", h.ListUsers)
18 | g.POST("/adminLogin", h.AdminLogin)
19 | g.POST("/tokenRefresh", h.TokenRefresh)
20 | g.POST("/authorize", h.Authorize)
21 | }
22 |
--------------------------------------------------------------------------------
/account-service/module/user/service/user_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "net/http"
7 | "strings"
8 |
9 | common "github.com/krishnarajvr/micro-common"
10 |
11 | "micro/module/user/model"
12 | "micro/module/user/repo"
13 | util "micro/util"
14 | "micro/util/password"
15 | "micro/util/token"
16 |
17 | "github.com/krishnarajvr/micro-common/locale"
18 | )
19 |
20 | type IUserService interface {
21 | List(tenantId int, page common.Pagination, filters model.UserFilterList) (model.UserDtos, *common.PageResult, error)
22 | Get(tenantId int, id int) (*model.UserDto, error)
23 | Add(form *model.UserForm) (*model.UserDto, error)
24 | Update(form *model.UserForm, id int) (*model.UserDto, error)
25 | Patch(form *model.UserPatchForm, id int) (*model.UserDto, error)
26 | Delete(id int) (*model.UserDto, error)
27 | Login(login *model.LoginForm) (*model.User, error)
28 | GetToken(user *model.User) (*token.TokenDetails, error)
29 | ParseUri(Uri string) (map[string]string, bool)
30 | CheckPermission(role string, module string, resource string, method string) (bool, error)
31 | RefreshToken(refreshToken string) (*token.TokenDetails, error)
32 | ExtractTokenMetadata(r *http.Request) (*token.AccessDetails, error)
33 | }
34 |
35 | type ServiceConfig struct {
36 | UserRepo repo.IUserRepo
37 | Lang *locale.Locale
38 | Token *token.Token
39 | }
40 |
41 | type Service struct {
42 | UserRepo repo.IUserRepo
43 | Lang *locale.Locale
44 | Token *token.Token
45 | }
46 |
47 | func NewService(c ServiceConfig) IUserService {
48 | return &Service{
49 | UserRepo: c.UserRepo,
50 | Lang: c.Lang,
51 | Token: c.Token,
52 | }
53 | }
54 |
55 | func (s *Service) List(tenantId int, page common.Pagination, filters model.UserFilterList) (model.UserDtos, *common.PageResult, error) {
56 | users, pageResult, err := s.UserRepo.List(tenantId, page, filters)
57 |
58 | if err != nil {
59 | return nil, nil, err
60 | }
61 |
62 | usersDto := users.ToDto()
63 |
64 | return usersDto, pageResult, err
65 | }
66 |
67 | func (s *Service) Get(tenantId int, id int) (*model.UserDto, error) {
68 | user, err := s.UserRepo.Get(tenantId, id)
69 |
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | userDto := user.ToDto()
75 |
76 | return userDto, nil
77 | }
78 |
79 | func (s *Service) Add(form *model.UserForm) (*model.UserDto, error) {
80 | userModel, err := form.ToModel()
81 | //Todo - Get from token
82 | userModel.TenantId = 1
83 | userModel.Password = password.Encrypt(form.Password)
84 |
85 | user, err := s.UserRepo.Add(userModel)
86 |
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | userDto := user.ToDto()
92 |
93 | return userDto, nil
94 | }
95 |
96 | func (s *Service) Login(form *model.LoginForm) (*model.User, error) {
97 | user, err := s.UserRepo.GetByEmail(form.Email)
98 |
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | passwordEqual := password.Compare(user.Password, form.Password)
104 |
105 | if passwordEqual != true {
106 | return nil, errors.New("Password not match")
107 | }
108 |
109 | return user, nil
110 | }
111 |
112 | func (s *Service) Update(form *model.UserForm, id int) (*model.UserDto, error) {
113 | user, err := s.UserRepo.Update(form, id)
114 |
115 | if err != nil {
116 | return nil, err
117 | }
118 |
119 | userDto := user.ToDto()
120 |
121 | return userDto, nil
122 | }
123 |
124 | func (s *Service) Patch(form *model.UserPatchForm, id int) (*model.UserDto, error) {
125 | user, err := s.UserRepo.Patch(form, id)
126 |
127 | if err != nil {
128 | return nil, err
129 | }
130 |
131 | userDto := user.ToDto()
132 |
133 | return userDto, nil
134 | }
135 |
136 | func (s *Service) Delete(id int) (*model.UserDto, error) {
137 | user, err := s.UserRepo.Delete(id)
138 |
139 | if err != nil {
140 | return nil, err
141 | }
142 |
143 | userDto := user.ToDto()
144 |
145 | return userDto, nil
146 | }
147 |
148 | func (s *Service) ExtractTokenMetadata(r *http.Request) (*token.AccessDetails, error) {
149 | return s.Token.ExtractTokenMetadata(r)
150 | }
151 |
152 | func (s *Service) GetToken(user *model.User) (*token.TokenDetails, error) {
153 | tokenData := token.TokenData{
154 | UserId: int64(user.ID),
155 | TenantId: int64(user.TenantId),
156 | Subject: "CarePlan",
157 | Admin: true,
158 | Type: "user",
159 | Roles: []string{"admin"},
160 | }
161 |
162 | token, _ := s.Token.CreateToken(&tokenData)
163 |
164 | return token, nil
165 | }
166 |
167 | func (s *Service) RefreshToken(refreshToken string) (*token.TokenDetails, error) {
168 | token, err := s.Token.Refresh(refreshToken, "user", "CarePlan")
169 |
170 | if err != nil {
171 | return nil, err
172 | }
173 |
174 | return token, nil
175 | }
176 |
177 | func (s *Service) ParseUri(uri string) (map[string]string, bool) {
178 | uriWithQueryParts := strings.Split(uri, "?")
179 | uriString := uriWithQueryParts[0]
180 | uriParts := strings.Split(uriString, "/")
181 | partLength := len(uriParts)
182 |
183 | if partLength <= 3 {
184 | return nil, false
185 | }
186 |
187 | module := uriParts[1]
188 | resource := uriParts[3]
189 |
190 | if partLength >= 5 {
191 | resource = resource + "/:id"
192 | }
193 |
194 | if len(module) == 0 || len(resource) == 0 {
195 | return nil, false
196 | }
197 |
198 | return map[string]string{
199 | "module": module,
200 | "resource": resource,
201 | }, true
202 | }
203 |
204 | func (s *Service) CheckPermission(role string, module string, resource string, method string) (bool, error) {
205 | rolePemissionJson := s.getPermissions()
206 |
207 | var rolePermissions map[string]map[string]map[string][]string
208 |
209 | if errJson := json.Unmarshal([]byte(rolePemissionJson), &rolePermissions); errJson != nil {
210 | return false, errJson
211 | }
212 |
213 | methods, ok := rolePermissions[strings.ToLower(role)][strings.ToLower(module)][resource]
214 |
215 | if !ok {
216 | return false, nil
217 | }
218 |
219 | if util.ArrayContains(methods, strings.ToLower(method)) {
220 | return true, nil
221 | }
222 |
223 | return false, nil
224 | }
225 |
226 | //getPermissions Define the role permissions for API
227 | //Todo - Need to move this to database and get from inmemory cache
228 | func (s *Service) getPermissions() string {
229 |
230 | //role->module->url->permissions[]
231 | return `{
232 | "admin": {
233 | "account": {
234 | "users" :["get","post"],
235 | "users/:id" :["get","post","patch"],
236 | "tenants" :["get","post"],
237 | "tenants/:id" :["get","post","patch"],
238 | "clientCredentials" :["get","post"],
239 | "clientCredentials/:id" :["get","post","patch"]
240 | },
241 | "product": {
242 | "products" :["get","post"],
243 | "products/:id" :["get","post","patch"],
244 | "productTypes" :["get","post"],
245 | "productTypes/:id" :["get","post","patch"],
246 | "categories" :["get","post"],
247 | "categories/:id" :["get","post","patch"]
248 | }
249 |
250 | },
251 | "user": {
252 | "site": {
253 | "products" :["get"],
254 | "products/:id" :["get"],
255 | "productTypes" :["get"],
256 | "categories" :["get"],
257 | "categories/:id" :["get"]
258 | }
259 | }
260 | }`
261 | }
262 |
--------------------------------------------------------------------------------
/account-service/module/user/service/user_service_test.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/krishnarajvr/micro-common/locale"
7 | "micro/config"
8 | "micro/module/user/mocks"
9 | "micro/module/user/model"
10 |
11 | common "github.com/krishnarajvr/micro-common"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestUser(t *testing.T) {
16 | t.Run("Success", func(t *testing.T) {
17 |
18 | mockUserResp := &model.User{
19 | Name: "User 1",
20 | Email: "Email",
21 | }
22 |
23 | page := common.Pagination{
24 | Sort: "ID",
25 | Order: "DESC",
26 | Offset: "0",
27 | Limit: "25",
28 | Search: "",
29 | }
30 |
31 | users := model.Users{mockUserResp}
32 | userDtos := users.ToDto()
33 |
34 | IUserRepo := new(mocks.IUserRepo)
35 |
36 | IUserRepo.On("List", page).Return(users, nil, nil)
37 |
38 | appConf := config.AppConfig()
39 | langLocale := locale.Locale{}
40 | lang := langLocale.New(appConf.App.Lang)
41 |
42 | ps := NewService(ServiceConfig{
43 | UserRepo: IUserRepo,
44 | Lang: lang,
45 | })
46 |
47 | u, _, err := ps.List(page)
48 |
49 | assert.NoError(t, err)
50 | assert.Equal(t, u, userDtos)
51 | IUserRepo.AssertExpectations(t)
52 | })
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/account-service/module/user/swagger/user.go:
--------------------------------------------------------------------------------
1 | package swagger
2 |
3 | import (
4 | "micro/module/user/model"
5 | )
6 |
7 | type UserSampleData struct {
8 | UserData model.UserDto `json:"user"`
9 | }
10 |
11 | type UserSampleListData struct {
12 | UserData model.UserDtos `json:"users"`
13 | }
14 |
15 | type UserListResponse struct {
16 | Status uint `json:"status" example:"200"`
17 | Error interface{} `json:"error"`
18 | Data UserSampleListData `json:"data"`
19 | }
20 |
21 | type UserResponse struct {
22 | Status uint `json:"status" example:"200"`
23 | Error interface{} `json:"error"`
24 | Data UserSampleData `json:"data"`
25 | }
26 |
27 | type UserTokenModel struct {
28 | TokenType string `json:"tokenType" example:"bearer" `
29 | AccessToken string `json:"accessToken" example:"eyJhbGciOiJIUzI1NXVCJ9.eyJhY2Nlc30IDIiLCJ0eXBlIjoiY2xpZW50In0.XBjAxzruIT"`
30 | RefreshToken string `json:"refreshToken" example:"eyJhbGciOiJIUzI54XVCJ54.eyJhY2Nlc30IDIiLCJ0eXBlIjoiY2xpZW5045.XBjAxzru45"`
31 | AtExpires string `json:"accessTokenExpiry" example:"2021-04-24T08:40:59Z"`
32 | RtExpires string `json:"refreshTokenExpiry" example:"2021-04-21T12:40:59Z"`
33 | }
34 |
35 | type UserTokenData struct {
36 | TokenData UserTokenModel `json:"token"`
37 | }
38 |
39 | type TokenResponse struct {
40 | Status uint `json:"status" example:"200"`
41 | Error interface{} `json:"error"`
42 | Data UserTokenData `json:"data"`
43 | RequestId string `json:"requestId" example:"3b6272b9-1ef1-45e0"`
44 | }
45 |
--------------------------------------------------------------------------------
/account-service/util/cache/redis.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "time"
7 |
8 | redis "github.com/go-redis/redis/v8"
9 | )
10 |
11 | type RedisClient struct {
12 | Valid bool
13 | Client *redis.Client
14 | Context context.Context
15 | }
16 |
17 | // Setup Initialize the Redis instance
18 | func New(client *redis.Client) *RedisClient {
19 | redisClient := RedisClient{}
20 | ctx := context.Background()
21 | redisClient.Client = client
22 | redisClient.Context = ctx
23 | redisClient.Valid = true
24 |
25 | return &redisClient
26 | }
27 |
28 | // Set a key/value
29 | func (c *RedisClient) Set(key string, data interface{}, duration time.Duration) error {
30 | value, err := json.Marshal(data)
31 |
32 | if err != nil {
33 | return err
34 | }
35 |
36 | err = c.Client.Set(c.Context, key, value, duration).Err()
37 |
38 | if err != nil {
39 | return err
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // Exists check a key
46 | func (c *RedisClient) Exists(key string) bool {
47 | return false
48 | }
49 |
50 | // Get get a key
51 | func (c *RedisClient) Get(key string) (string, error) {
52 | val, err := c.Client.Get(c.Context, key).Result()
53 |
54 | switch {
55 | case err == redis.Nil:
56 | return "", nil
57 | case err != nil:
58 | return "", err
59 | }
60 |
61 | return val, nil
62 | }
63 |
64 | // Delete delete a kye
65 | func (c *RedisClient) Delete(key string) (int64, error) {
66 | deleted, err := c.Client.Del(c.Context, key).Result()
67 |
68 | if err != nil {
69 | return 0, err
70 | }
71 |
72 | return deleted, nil
73 | }
74 |
75 | // LikeDeletes batch delete
76 | func (c *RedisClient) LikeDeletes(key string) error {
77 | return nil
78 | }
79 |
80 | func (c *RedisClient) Ping() bool {
81 | _, err := c.Client.Ping(c.Context).Result()
82 |
83 | if err == nil {
84 | return true
85 | }
86 |
87 | return false
88 | }
89 |
--------------------------------------------------------------------------------
/account-service/util/oauth_response.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | type OauthError struct {
10 | Error string `json:"error" example:"invalid_request"`
11 | Description string `json:"error_description,omitempty" example:"Request was missing type"`
12 | URI string `json:"error_uri,omitempty" example:"{}"`
13 | }
14 |
15 | func OauthBadRequest(c *gin.Context, errorMessage string) {
16 | c.JSON(http.StatusBadRequest, OauthError{
17 | Error: "invalid_request",
18 | Description: errorMessage,
19 | })
20 | }
21 |
22 | func OauthUnauthorized(c *gin.Context, errorMessage string) {
23 | c.JSON(http.StatusUnauthorized, OauthError{
24 | Error: "invalid_client",
25 | Description: errorMessage,
26 | })
27 | }
28 |
29 | func OauthUnsupportedGrantType(c *gin.Context, errorMessage string) {
30 | c.JSON(http.StatusBadRequest, OauthError{
31 | Error: "unsupported_grant_type",
32 | Description: errorMessage,
33 | })
34 | }
35 |
36 | func OauthInternalError(c *gin.Context, errorMessage string) {
37 | c.JSON(http.StatusInternalServerError, OauthError{
38 | Error: "invalid_request",
39 | Description: errorMessage,
40 | })
41 | }
42 |
--------------------------------------------------------------------------------
/account-service/util/password/password.go:
--------------------------------------------------------------------------------
1 | package password
2 |
3 | import (
4 | "log"
5 |
6 | "golang.org/x/crypto/bcrypt"
7 | )
8 |
9 | func Encrypt(password string) string {
10 | bytePassword := []byte(password)
11 | hash, err := bcrypt.GenerateFromPassword(bytePassword, bcrypt.MinCost)
12 |
13 | if err != nil {
14 | log.Println(err)
15 | }
16 |
17 | return string(hash)
18 | }
19 |
20 | func Compare(hashedPwd string, plainPwd string) bool {
21 | byteHash := []byte(hashedPwd)
22 | bytePlainPwd := []byte(plainPwd)
23 | log.Println("Password:", byteHash, bytePlainPwd)
24 | err := bcrypt.CompareHashAndPassword(byteHash, bytePlainPwd)
25 |
26 | if err != nil {
27 | log.Println(err)
28 | return false
29 | }
30 |
31 | return true
32 | }
33 |
--------------------------------------------------------------------------------
/account-service/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | func ArrayContains(s []string, str string) bool {
4 | for _, v := range s {
5 | if v == str {
6 | return true
7 | }
8 | }
9 |
10 | return false
11 | }
12 |
--------------------------------------------------------------------------------
/assets/golang-monorepo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krishnarajvr/microservice-mono-gin-gorm/32115c9902032502169e5950574a781e6154663e/assets/golang-monorepo.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | gateway:
4 | build:
5 | context: ./gateway
6 | dockerfile: ./deploy/prod.Dockerfile
7 | image: micro-services_gateway
8 | env_file:
9 | - ./gateway/deploy/.env
10 | ports:
11 | - 8080:8080
12 | command: /bin/sh -c '/micro/bin/init.sh'
13 |
14 | restart: "no"
15 | account-service:
16 | build:
17 | context: ./account-service
18 | args:
19 | - SSH_PRIVATE_KEY
20 | dockerfile: ./deploy/prod.Dockerfile
21 | image: micro-services_account-service
22 | env_file:
23 | - ./account-service/deploy/.env
24 | ports:
25 | - 8082:8080
26 | depends_on:
27 | - db
28 | command: /bin/sh -c 'while ! nc -z db 3306; do sleep 1; done; /micro/bin/init.sh;'
29 | restart: always
30 | product-service:
31 | build:
32 | context: ./product-service
33 | args:
34 | - SSH_PRIVATE_KEY
35 | dockerfile: ./deploy/prod.Dockerfile
36 | image: micro-services_product-service
37 | env_file:
38 | - ./product-service/deploy/.env
39 | ports:
40 | - 8082:8080
41 | depends_on:
42 | - db
43 | command: /bin/sh -c 'while ! nc -z db 3306; do sleep 1; done; /micro/bin/init.sh;'
44 | restart: always
45 | db:
46 | image: yobasystems/alpine-mariadb:latest
47 | environment:
48 | MYSQL_ROOT_PASSWORD: micro_root_pass
49 | MYSQL_DATABASE: micro_db
50 | MYSQL_USER: micro_user
51 | MYSQL_PASSWORD: micro_pass
52 | ports:
53 | - 3307:3306
54 | restart: always
55 | phpmyadmin:
56 | image: phpmyadmin/phpmyadmin:latest
57 | ports:
58 | - 8000:80
59 | environment:
60 | - PMA_ARBITRARY=1
61 | - PMA_HOST=db
62 | - PMA_PORT=3306
63 | depends_on:
64 | - db
65 | volumes:
66 | mariadb-data:
67 |
--------------------------------------------------------------------------------
/gateway/.env:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 |
3 | SERVER_PORT=8080
4 | SERVER_TIMEOUT_READ=5s
5 | SERVER_TIMEOUT_WRITE=10s
6 | SERVER_TIMEOUT_IDLE=15s
7 | SERVICE_NAME=gateway-service
8 |
--------------------------------------------------------------------------------
/gateway/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build run
2 |
3 | AWS_ACCESS_KEY_ID=dummy-key
4 | AWS_SECRET_ACCESS_KEY=dummy-secret
5 | PERMISSION_URL=http://localhost:8082/api/v1/authorize
6 | ACCOUNT_SERVICE=http://localhost:8082
7 | PRODUCT_SERVICE=http://localhost:8083
8 |
9 | export AWS_ACCESS_KEY_ID
10 | export AWS_SECRET_ACCESS_KEY
11 | export PERMISSION_URL
12 | export ACCOUNT_SERVICE
13 | export PRODUCT_SERVICE
14 |
15 | build:
16 | FC_ENABLE=1 FC_SETTINGS="$(PWD)/settings" \
17 | FC_PARTIALS="$(PWD)/partials" \
18 | FC_TEMPLATES="$(PWD)/templates" \
19 | FC_OUT=$(PWD)/krakend-out.json \
20 | krakend check -c $(PWD)/krakend.json -d
21 |
22 | run:
23 | sed -i 's,http://account-service:8082,$(ACCOUNT_SERVICE),g' $(PWD)/krakend-out.json
24 | sed -i 's,http://product-service:8083,$(PRODUCT_SERVICE),g' $(PWD)/krakend-out.json
25 |
26 | krakend run -c $(PWD)/krakend-out.json -d
27 |
28 | build-docker:
29 | sudo docker run --rm -it -v $(PWD):/etc/krakend -e FC_ENABLE=1 -e FC_SETTINGS="/etc/krakend/settings" -e FC_PARTIALS="/etc/krakend/partials" -e FC_OUT=out.json devopsfaith/krakend run -c /etc/krakend/krakend.json -d
30 |
31 | run-docker:
32 | sudo docker run -p 8080:8080 -v $(PWD):/etc/krakend/ devopsfaith/krakend run -c /etc/krakend/krakend-out.json
33 |
34 | replace:
35 | sed -i 's,http://account-service:8082,$(ACCOUNT_SERVICE),g' $(PWD)/krakend-out.json
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/gateway/README.md:
--------------------------------------------------------------------------------
1 | # Gateway integration using Krakend
2 |
3 | ## Requirements
4 |
5 | * Golang - 1.15 recommended
6 | * [krakend](https://www.krakend.io/)
7 |
8 | ## Build krakend locally
9 |
10 | 1. Download source code from https://github.com/devopsfaith/krakend-ce/
11 | 2. Go to source folder and run ```make build```
12 | 3. Copy the ```krakend``` binary to go bin folder ```{GOPATH}/bin```
13 |
14 | ### Steps
15 |
16 | Build Kradend binary locally
17 |
18 |
19 | ```
20 | git clone https://github.com/devopsfaith/krakend-ce.git
21 |
22 | cd krakend-ce
23 |
24 | git checkout v1.3.0
25 |
26 | go mod vendor
27 |
28 | make build
29 |
30 | ```
31 |
32 | Copy the kradend binary to ```{GOPATH}/bin``` path once created.
33 |
34 | ## Build Plugins
35 |
36 | Build router plugin
37 |
38 | ```
39 | cd plugins/router-plugin
40 |
41 | make build
42 |
43 | ```
44 |
45 | ## dynamic krakend config
46 |
47 | Refer : https://www.krakend.io/docs/configuration/flexible-config/
48 |
49 |
50 | ### Generate Kradend Config from dynamic configuration
51 |
52 | ```
53 | make build
54 | ```
55 | Make sure current directory is ./gateway . Not the plugin directory. ( ``` cd ../../ ``` )
56 |
57 | It will generate krakend-out.json file with final configuration from the templates and settings.
58 |
59 | ### Start the gateway
60 |
61 | #### Change the configurations in Makefile as per the setup of other services
62 | ```
63 | PERMISSION_URL=http://localhost:8082/api/v1/authorize
64 | ACCOUNT_SERVICE=http://localhost:8082
65 | PRODUCT_SERVICE=http://localhost:8083
66 | ```
67 |
68 | #### Run the gateway
69 |
70 | ```
71 | make run
72 | ```
73 |
74 | If there is any configuration change in krakend.json, It need to build the chagnes again using ``` make build ``` in gateway folder. Refer Makefile for the commands that is running.
75 |
76 | Note : check ```{GOPATH}/bin``` should contain krakend binary as per the krakend-ce build
77 |
--------------------------------------------------------------------------------
/gateway/deploy/.env:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 |
3 | SERVER_PORT=8080
4 | SERVER_TIMEOUT_READ=5s
5 | SERVER_TIMEOUT_WRITE=10s
6 | SERVER_TIMEOUT_IDLE=15s
7 | PERMISSION_URL=http://account-service:8080/api/v1/authorize
8 | AWS_ACCESS_KEY_ID=dummy-value
9 | AWS_SECRET_ACCESS_KEY=dummy-value
10 | ACCOUNT_SERVICE=http://account-service:8080
11 | PRODUCT_SERVICE=http://content-service:8080
--------------------------------------------------------------------------------
/gateway/deploy/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.16
2 |
3 | WORKDIR /micro
4 |
5 | RUN apt-get update && \
6 | apt-get install -y ca-certificates && \
7 | update-ca-certificates && \
8 | apt-get install git -y && \
9 | rm -rf /var/lib/apt/lists/*
10 |
11 | RUN git clone https://github.com/devopsfaith/krakend-ce.git
12 |
13 | WORKDIR /micro/krakend-ce
14 |
15 | RUN go mod vendor
16 |
17 | RUN make build
18 |
19 | WORKDIR /micro
20 |
21 | COPY . .
22 |
23 | WORKDIR /micro/plugins
24 |
25 | RUN go build -buildmode=plugin -o router-plugin.so ./router-plugin/*.go && \
26 | chmod +x /micro/deploy/bin/*
27 |
28 | WORKDIR /micro
29 |
30 | EXPOSE 8080
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/gateway/deploy/bin/init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo 'Runing Gateway...'
3 |
4 | sed -i "s,http://account-service:8082,$ACCOUNT_SERVICE,g" /micro/krakend.json
5 | sed -i "s,http://content-service:8083,$PRODUCT_SERVICE,g" /micro/krakend.json
6 | sed -i "s,http://asset-service:8084,$ASSET_SERVICE,g" /micro/krakend.json
7 | sed -i "s,http://vendor-service:8085,$VENDOR_SERVICE,g" /micro/krakend.json
8 | sed -i "s,http://aggregate-service:8086,$AGGREGATE_SERVICE,g" /micro/krakend.json
9 | /micro/krakend run -c /micro/krakend.json
10 |
--------------------------------------------------------------------------------
/gateway/deploy/k8s/app-configmap.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: gateway-config
5 | namespace: micro-env
6 | labels:
7 | app: gateway-config
8 | data:
9 | DEBUG: 'false'
10 | SERVER_PORT: '8080'
11 | SERVER_TIMEOUT_READ: '5s'
12 | SERVER_TIMEOUT_WRITE: '10s'
13 | SERVER_TIMEOUT_IDLE: '15s'
14 | API_KEY: '1234'
15 | PERMISSION_URL: 'http://account-service:8080/api/v1/authorize'
16 | ACCOUNT_SERVICE: 'http://account-service:8080'
17 | PRODUCT_SERVICE: 'http://product-service:8080'
18 | SERVICE_NAME: 'gateway-service'
19 |
--------------------------------------------------------------------------------
/gateway/deploy/k8s/app-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: gateway
5 | namespace: micro-env
6 | spec:
7 | selector:
8 | matchLabels:
9 | app: gateway
10 | replicas: 1
11 | template:
12 | metadata:
13 | labels:
14 | app: gateway
15 | spec:
16 | containers:
17 | - name: gateway
18 | image: GATEWAY_IMAGE
19 | command:
20 | - /bin/sh
21 | - -c
22 | - /micro/bin/init.sh
23 | ports:
24 | - containerPort: 8080
25 | imagePullPolicy: IfNotPresent
26 | envFrom:
27 | - configMapRef:
28 | name: gateway-config
29 | - secretRef:
30 | name: gateway-secret
31 | resources:
32 | requests:
33 | memory: "64Mi"
34 | cpu: "50m"
35 | limits:
36 | memory: "256Mi"
37 | cpu: "400m"
38 |
39 |
40 |
--------------------------------------------------------------------------------
/gateway/deploy/k8s/app-secret.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Secret
3 | metadata:
4 | name: gateway-secret
5 | namespace: micro-env
6 | labels:
7 | app: gateway-secret
8 | type: Opaque
9 | data:
10 | AWS_ACCESS_KEY_ID: 'micro-gateway-xrayaccess'
11 | AWS_SECRET_ACCESS_KEY: 'micro-gateway-xraysecret'
12 |
13 |
--------------------------------------------------------------------------------
/gateway/deploy/k8s/app-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: gateway-service
5 | namespace: micro-env
6 | spec:
7 | type: NodePort
8 | ports:
9 | - port: 8080
10 | targetPort: 8080
11 | protocol: TCP
12 | nodePort: 30445
13 | selector:
14 | app: gateway
15 |
--------------------------------------------------------------------------------
/gateway/deploy/prod.Dockerfile:
--------------------------------------------------------------------------------
1 | # Build environment
2 | # -----------------
3 | FROM golang:1.15-buster as build-env
4 | WORKDIR /micro
5 |
6 | RUN apt-get update && \
7 | apt-get install git -y
8 |
9 | RUN git clone https://github.com/devopsfaith/krakend-ce.git
10 |
11 | WORKDIR /micro/krakend-ce
12 |
13 | RUN go mod vendor
14 |
15 | RUN make build
16 |
17 | WORKDIR /micro
18 |
19 | RUN pwd
20 |
21 | COPY . .
22 |
23 |
24 | RUN FC_ENABLE=1 FC_SETTINGS=settings FC_PARTIALS=partials FC_TEMPLATES=templates FC_OUT=krakend-out.json /micro/krakend-ce/krakend check -c krakend.json -d
25 |
26 | WORKDIR /micro/plugins/router-plugin
27 |
28 | RUN go build -buildmode=plugin -o ../router-plugin.so ./*.go
29 |
30 | WORKDIR /micro
31 |
32 | # Deployment environment
33 | # ----------------------
34 | FROM debian:bullseye-slim
35 | RUN apt-get update && \
36 | apt-get install ca-certificates -y && \
37 | update-ca-certificates
38 |
39 | WORKDIR /micro
40 |
41 | COPY --from=build-env /micro/krakend-ce/krakend /micro/
42 | COPY --from=build-env /micro/plugins/*.so /micro/plugins/
43 | COPY --from=build-env /micro/krakend-out.json /micro/krakend.json
44 | COPY --from=build-env /micro/deploy/bin/init.sh /micro/bin/init.sh
45 |
46 | RUN chmod +x /micro/bin/*
47 |
48 | EXPOSE 8080 8090
49 |
--------------------------------------------------------------------------------
/gateway/partials/account-host.tmpl:
--------------------------------------------------------------------------------
1 | "host": ["http://account-service:8082"]
--------------------------------------------------------------------------------
/gateway/partials/product-host.tmpl:
--------------------------------------------------------------------------------
1 | "host": ["http://product-service:8083"]
--------------------------------------------------------------------------------
/gateway/partials/rate-limit-backend.tmpl:
--------------------------------------------------------------------------------
1 | "github.com/devopsfaith/krakend-ratelimit/juju/proxy": {
2 | "maxRate": "100",
3 | "capacity": "100"
4 | }
--------------------------------------------------------------------------------
/gateway/plugins/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | krakend
--------------------------------------------------------------------------------
/gateway/plugins/proxy-plugin/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: run
2 |
3 | all: run
4 |
5 | run:
6 | go build -buildmode=plugin -o ../proxy-plugin.so ./*.go
--------------------------------------------------------------------------------
/gateway/plugins/proxy-plugin/go.mod:
--------------------------------------------------------------------------------
1 | module proxy-plugin
2 |
3 | go 1.15
--------------------------------------------------------------------------------
/gateway/plugins/proxy-plugin/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | // ClientRegisterer is the symbol the plugin loader will try to load. It must implement the RegisterClient interface
13 | var ClientRegisterer = registerer("proxy-plugin")
14 |
15 | type registerer string
16 |
17 | func (r registerer) RegisterClients(f func(
18 | name string,
19 | handler func(context.Context, map[string]interface{}) (http.Handler, error),
20 | )) {
21 | f(string(r), r.registerClients)
22 | }
23 |
24 | func (r registerer) registerClients(ctx context.Context, extra map[string]interface{}) (http.Handler, error) {
25 | // check the passed configuration and initialize the plugin
26 | name, ok := extra["name"].(string)
27 |
28 | if !ok {
29 | return nil, errors.New("wrong config")
30 | }
31 |
32 | if name != string(r) {
33 | return nil, fmt.Errorf("unknown register %s", name)
34 | }
35 |
36 | // return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http client
37 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
38 | fmt.Println("proxy-plugin called")
39 | fmt.Println("Request: ", req)
40 | fmt.Println("Request Header: ", req.Header)
41 | fmt.Println("Tenant Id: ", req.Header.Get("tenantId"))
42 | fmt.Println("User Agent: ", req.UserAgent())
43 | fmt.Println("Reqeust Context: ", req.Context())
44 | fmt.Println("Context: ", context.Background)
45 | fmt.Println("Extra: ", extra)
46 |
47 | client := &http.Client{
48 | Timeout: time.Second * 10,
49 | }
50 |
51 | newResp, err := client.Do(req)
52 |
53 | if err != nil {
54 | http.Error(w, "", http.StatusBadRequest)
55 | return
56 | }
57 |
58 | defer newResp.Body.Close()
59 |
60 | body, err := ioutil.ReadAll(newResp.Body)
61 |
62 | fmt.Println("End calling proxy plugin")
63 |
64 | w.Write(body)
65 | }), nil
66 | }
67 |
68 | func init() {
69 | fmt.Println("proxy-plugin client plugin loaded!!!")
70 | }
71 |
72 | func main() {}
73 |
--------------------------------------------------------------------------------
/gateway/plugins/router-plugin/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 |
3 | all: build
4 |
5 | build:
6 | go build -buildmode=plugin -o ../router-plugin.so ./*.go
--------------------------------------------------------------------------------
/gateway/plugins/router-plugin/authorizer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "net/url"
11 | "os"
12 | )
13 |
14 | type TokenData struct {
15 | UserId string
16 | TenantId string
17 | ReferenceId string
18 | Subject string
19 | Type string
20 | Roles []string
21 | Admin bool
22 | }
23 |
24 | type AuthErrorData struct {
25 | Code string `json:"code"`
26 | Message string `json:"message"`
27 | }
28 |
29 | type AuthData struct {
30 | Auth TokenData `json:"auth"`
31 | }
32 |
33 | type AuthResponse struct {
34 | Status int `json:"status"`
35 | Data AuthData `json:"data"`
36 | Error AuthErrorData `json:"error"`
37 | RequestId string `json:"requestId"`
38 | }
39 |
40 | func GetAuthorizeContext(r *http.Request) (*AuthResponse, error) {
41 | client := &http.Client{}
42 |
43 | authorizeURL := os.Getenv("PERMISSION_URL")
44 |
45 | if len(authorizeURL) == 0 {
46 | return nil, errors.New("Invalid URL")
47 | }
48 |
49 | data := url.Values{}
50 | data.Add("uri", r.RequestURI)
51 | data.Add("method", r.Method)
52 |
53 | authReq, err := http.NewRequest("POST", authorizeURL, bytes.NewBufferString(data.Encode()))
54 | authReq.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
55 | authReq.Header.Set("Authorization", r.Header.Get("Authorization"))
56 |
57 | if err != nil {
58 | log.Println(err)
59 | return nil, err
60 | }
61 |
62 | authResp, err := client.Do(authReq)
63 |
64 | if err != nil {
65 | log.Println(err)
66 | return nil, err
67 | }
68 |
69 | apiData, err := ioutil.ReadAll(authResp.Body)
70 |
71 | if err != nil {
72 | log.Println(err)
73 | return nil, err
74 | }
75 |
76 | authResp.Body.Close()
77 |
78 | if err != nil {
79 | log.Fatal(err)
80 | return nil, err
81 | }
82 |
83 | var authResponse AuthResponse
84 |
85 | if errJson := json.Unmarshal(apiData, &authResponse); errJson != nil {
86 | return nil, errJson
87 | }
88 |
89 | return &authResponse, nil
90 | }
91 |
--------------------------------------------------------------------------------
/gateway/plugins/router-plugin/error.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/google/uuid"
7 | )
8 |
9 | const (
10 | BAD_REQUEST = "BAD_REQUEST"
11 | INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
12 | INVALID_ARGUMENT = "INVALID_ARGUMENT"
13 | OUT_OF_RANGE = "OUT_OF_RANGE"
14 | UNAUTHENTICATED = "UNAUTHENTICATED"
15 | ACCESS_DENIED = "ACCESS_DENIED"
16 | NOT_FOUND = "NOT_FOUND"
17 | ABORTED = "ABORTED"
18 | ALREADY_EXISTS = "ALREADY_EXISTS"
19 | RESOURCE_EXHAUSTED = "RESOURCE_EXHAUSTED"
20 | CANCELLED = "CANCELLED"
21 | DATA_LOSS = "DATA_LOSS"
22 | UNKNOWN = "UNKNOWN"
23 | NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
24 | UNAVAILABLE = "UNAVAILABLE"
25 | DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED"
26 | )
27 |
28 | type Response struct {
29 | Status int `json:"status" example:"200"`
30 | Data interface{} `json:"data" example:"{data:{products}}"`
31 | Error interface{} `json:"error" example:"{}"`
32 | RequestId string `json:"requestId" example:"3b6272b9-1ef1-45e0"`
33 | }
34 |
35 | type ErrorData struct {
36 | Code string `json:"code" example:"BAD_REQUEST"`
37 | Message string `json:"message" example:"Bad Request"`
38 | Details []ErrorDetail `json:"details,omitempty"`
39 | }
40 |
41 | type ErrorDetail struct {
42 | Code string `json:"code" example:"Required"`
43 | Target string `json:"target" example:"Name"`
44 | Message string `json:"message" example:"Name field is required"`
45 | }
46 |
47 | type Error404 struct {
48 | Status uint `json:"status" example:"404"`
49 | Error ErrorData `json:"error"`
50 | Data interface{} `json:"data"`
51 | }
52 |
53 | func ErrorResponseWitCode(statusCode int, errorData *ErrorData) Response {
54 | //Todo : Get from context
55 | requestId := uuid.New()
56 |
57 | return Response{
58 | Status: statusCode,
59 | Data: "",
60 | Error: errorData,
61 | RequestId: requestId.String(),
62 | }
63 | }
64 |
65 | func BadRequest(errorMessage string) Response {
66 | if len(errorMessage) == 0 {
67 | errorMessage = "Access Denied"
68 | }
69 |
70 | errorData := &ErrorData{
71 | Code: ACCESS_DENIED,
72 | Message: errorMessage,
73 | }
74 |
75 | return ErrorResponseWitCode(http.StatusForbidden, errorData)
76 | }
77 |
78 | func AccessDenied(errorMessage string) Response {
79 | if len(errorMessage) == 0 {
80 | errorMessage = "Access Denied"
81 | }
82 |
83 | errorData := &ErrorData{
84 | Code: ACCESS_DENIED,
85 | Message: errorMessage,
86 | }
87 |
88 | return ErrorResponseWitCode(http.StatusForbidden, errorData)
89 | }
90 |
--------------------------------------------------------------------------------
/gateway/plugins/router-plugin/go.mod:
--------------------------------------------------------------------------------
1 | module router-plugin
2 |
3 | go 1.15
4 |
5 | require github.com/google/uuid v1.2.0
6 |
--------------------------------------------------------------------------------
/gateway/plugins/router-plugin/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
2 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3 |
--------------------------------------------------------------------------------
/gateway/plugins/router-plugin/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "net/http"
9 | )
10 |
11 | // HandlerRegisterer is the symbol the plugin loader will try to load. It must implement the Registerer interface
12 | var HandlerRegisterer = registerer("router-plugin")
13 |
14 | type registerer string
15 |
16 | func (r registerer) RegisterHandlers(f func(
17 | name string,
18 | handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error),
19 | )) {
20 | f(string(r), r.registerHandlers)
21 | }
22 |
23 | func validateToken(token string) bool {
24 | return true
25 | }
26 |
27 | //registerHandlers Executes before every requests
28 | func (r registerer) registerHandlers(ctx context.Context, extra map[string]interface{}, handler http.Handler) (http.Handler, error) {
29 | name, ok := extra["name"].([]interface{})
30 |
31 | if !ok {
32 | return nil, errors.New("wrong config")
33 | }
34 |
35 | if name[0] != string(r) {
36 | return nil, fmt.Errorf("unknown register %s", name)
37 | }
38 |
39 | // return the actual handler wrapping with custom logic
40 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
41 | public := checkPublicRoutes(req)
42 |
43 | if public {
44 | handler.ServeHTTP(w, req)
45 | return
46 | }
47 |
48 | context, authErr := GetAuthorizeContext(req)
49 |
50 | if authErr != nil {
51 | fmt.Println("Auth error:", authErr)
52 | SendError(w, "Invalid User Token", http.StatusForbidden)
53 | return
54 | }
55 |
56 | if context.Status != 200 {
57 | SendError(w, context.Error.Message, http.StatusForbidden)
58 | return
59 | }
60 |
61 | //Set Headers for upstream services
62 | req.Header.Set("X-Tenant-Id", context.Data.Auth.TenantId)
63 |
64 | if len(context.Data.Auth.UserId) > 0 {
65 | req.Header.Set("X-User-Id", context.Data.Auth.UserId)
66 | }
67 |
68 | if len(context.Data.Auth.Type) > 0 {
69 | req.Header.Set("X-Auth-Type", context.Data.Auth.Type)
70 | }
71 |
72 | if len(context.Data.Auth.ReferenceId) > 0 {
73 | req.Header.Set("X-Reference-Id", context.Data.Auth.ReferenceId)
74 | }
75 |
76 | handler.ServeHTTP(w, req)
77 |
78 | return
79 | }), nil
80 | }
81 |
82 | func SendError(w http.ResponseWriter, message string, status int) {
83 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
84 | w.Header().Set("X-Content-Type-Options", "nosniff")
85 | w.WriteHeader(status)
86 | json.NewEncoder(w).Encode(AccessDenied(message))
87 | }
88 |
89 | func init() {
90 | //Todo - debug purpose - need to remove
91 | fmt.Println("router-plugin: Init handler loaded!!!")
92 | }
93 |
94 | func main() {}
95 |
--------------------------------------------------------------------------------
/gateway/plugins/router-plugin/public_routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | )
7 |
8 | func GetPublicRoutes() map[string]interface{} {
9 | return map[string]interface{}{
10 | "/thirdparty/v1/token": "true",
11 | "/account/v1/adminLogin": "true",
12 | "/account/v1/tenantRegister": "true",
13 | "/health": "true",
14 | }
15 | }
16 |
17 | func checkPublicRoutes(req *http.Request) bool {
18 | allowd := GetPublicRoutes()
19 | public := false
20 |
21 | if _, ok := allowd[req.RequestURI]; ok {
22 | public = true
23 | }
24 |
25 | if !public {
26 | public = strings.HasPrefix(req.RequestURI, "/account/swagger/")
27 | }
28 |
29 | if !public {
30 | public = strings.HasPrefix(req.RequestURI, "/product/swagger/")
31 | }
32 |
33 | return public
34 | }
35 |
--------------------------------------------------------------------------------
/gateway/settings/endpoint.json:
--------------------------------------------------------------------------------
1 | {
2 | "accountServiceList": [
3 | {
4 | "route": "/account/v1/users",
5 | "backend": "/api/v1/users"
6 | },
7 | {
8 | "route": "/account/v1/tenants",
9 | "backend": "/api/v1/tenants"
10 | },
11 | {
12 | "route": "/account/v1/clientCredentials",
13 | "backend": "/api/v1/clientCredentials"
14 | }
15 | ],
16 | "accountServiceWithID": [
17 | {
18 | "route": "/account/v1/users/{id}",
19 | "backend": "/api/v1/users/{id}"
20 | },
21 | {
22 | "route": "/account/v1/tenants/{id}",
23 | "backend": "/api/v1/tenants/{id}"
24 | }
25 | ],
26 | "productServiceList": [
27 | {
28 | "route": "/product/v1/products",
29 | "backend": "/api/v1/products"
30 | }
31 | ],
32 | "productServiceWithID": [
33 | {
34 | "route": "/product/v1/products/{id}",
35 | "backend": "/api/v1/products/{id}"
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/gateway/settings/service.json:
--------------------------------------------------------------------------------
1 | {
2 | "extra_config": {
3 | "github_com/devopsfaith/krakend-gologging": {
4 | "format": "default",
5 | "level": "DEBUG",
6 | "prefix": "[KRAKEND]",
7 | "stdout": true,
8 | "syslog": true
9 | },
10 | "github_com/devopsfaith/krakend-cors": {
11 | "allow_origins": [
12 | "*"
13 | ],
14 | "allow_methods": [
15 | "GET",
16 | "HEAD",
17 | "POST",
18 | "PATCH",
19 | "DELETE",
20 | "OPTIONS"
21 | ],
22 | "expose_headers": [
23 | "*"
24 | ],
25 | "max_age": "12h",
26 | "allow_headers": [
27 | "*"
28 | ],
29 | "allow_credentials": false,
30 | "debug": true
31 | },
32 | "github_com/devopsfaith/krakend-opencensus": {
33 | "sample_rate": 100,
34 | "reporting_period": 1
35 | },
36 | "github_com/devopsfaith/krakend/transport/http/server/handler": {
37 | "name": [
38 | "router-plugin"
39 | ]
40 | }
41 | },
42 | "timeout": "3000ms",
43 | "cache_ttl": "300s",
44 | "output_encoding": "json",
45 | "name": "Micro Gateway",
46 | "port": 8080,
47 | "error_backend_alias" : {
48 | "http_status_code": 404,
49 | "http_body": "{\"status\":404}"
50 | }
51 | }
--------------------------------------------------------------------------------
/gateway/templates/env.tmpl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krishnarajvr/microservice-mono-gin-gorm/32115c9902032502169e5950574a781e6154663e/gateway/templates/env.tmpl
--------------------------------------------------------------------------------
/product-service/.env:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 |
3 | SERVER_PORT=8083
4 | SERVER_TIMEOUT_READ=5s
5 | SERVER_TIMEOUT_WRITE=10s
6 | SERVER_TIMEOUT_IDLE=15s
7 |
8 | DB_HOST=localhost
9 | DB_PORT=3306
10 | DB_USER=user
11 | DB_PASS=pass
12 | DB_NAME=product
13 |
14 | SERVICE_NAME=Product-Service
15 |
16 | API_GATEWAY_URL=localhost:8080
17 | API_GATEWAY_PREFIX=/product/v1
--------------------------------------------------------------------------------
/product-service/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: doc run test mock load-env test-tenant
2 |
3 | SERVICE_NAME=Product-Service
4 |
5 | export SERVICE_NAME
6 |
7 | doc:
8 | @echo "---Generate doc files---"
9 | swag init -g module/module.go -o doc
10 |
11 | doc-dep:
12 | @echo "---Generate doc files---"
13 | swag init -g cmd/api/main.go -o doc
14 |
15 | migrate-create:
16 | @echo "---Creating migration files---"
17 | go run cmd/migrate/main.go create $(NAME) sql
18 |
19 | migrate-up:
20 | go run cmd/migrate/main.go up
21 |
22 | migrate-down:
23 | go run cmd/migrate/main.go down
24 |
25 | migrate-force:
26 | go run cmd/migrate/main.go force $(VERSION)
27 |
28 | run:
29 | go run cmd/api/main.go
30 |
31 | load-env:
32 | export $(cat .env | xargs) && echo $DEBUG
33 |
34 | test:
35 | go test -v ./module/*/*/
36 |
37 | test-cover:
38 | go test -v ./module/product/service -cover
39 | go test -v ./module/product -cover
40 |
41 | test-vendor:
42 | go test -v ./module/product/service
43 | go test -v ./module/product
44 |
45 | test-participant:
46 | go test -v ./module/participant/service
47 | go test -v ./module/participant
48 |
49 | mock:
50 | mockery --dir=module/product/service --name=IProductService --output=module/product/mocks
51 | mockery --dir=module/product/repo --name=IProductRepo --output=module/product/mocks
52 |
--------------------------------------------------------------------------------
/product-service/README.md:
--------------------------------------------------------------------------------
1 | # Account Service
2 |
3 | ## Requirements
4 |
5 | * Golang - 1.14 recommended
6 | * Mysql - 8.0
7 | * [Swag cli](https://github.com/swaggo/swag) - For generating swagger docs
8 | * [Mockery](https://github.com/vektra/mockery) - For generating mock classes for testing
9 |
10 |
11 | - Note: Once Swag and Mockery installed ```swag``` and ```mockery``` command should work in terminal.
12 | - Tip: can copy the build binary of the package to {home}/go/bin path also works. Also can change the Makefile command path as well.
13 |
14 | ## Steps
15 |
16 | ### Coniguration
17 | Change the config in .env for database and other configuration
18 |
19 |
20 | ### Database Migration
21 |
22 | Existing migrations
23 |
24 | ```sh
25 | make migrate-up
26 | ```
27 | Create a migration file - if required
28 |
29 | ```
30 | make migrate-create NAME=create-product
31 | ```
32 | Modify the migration sql(create-product-1123213.sql) once created in migration folder and then run again ```make migrate-up```.
33 |
34 |
35 | ### Swagger Document
36 |
37 | Generate API document to the ./doc folder using swag cli
38 | ```sh
39 | make doc
40 | ```
41 |
42 | ### Run service
43 |
44 | ```sh
45 | make run
46 | ```
47 |
48 | Make sure you run ```make migrate-up``` before the running
49 |
50 | ### Testing
51 |
52 | Generate mock files
53 | ```sh
54 | make mock
55 | ```
56 |
57 | Test service
58 | ```sh
59 | make test
60 | ```
61 |
62 |
--------------------------------------------------------------------------------
/product-service/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "log"
5 | lgorm "micro/app/database/gorm"
6 | "micro/config"
7 | "os"
8 |
9 | _ "github.com/jinzhu/gorm/dialects/mysql"
10 | "gorm.io/gorm"
11 |
12 | "github.com/gin-gonic/gin"
13 | "github.com/krishnarajvr/micro-common/locale"
14 | "github.com/krishnarajvr/micro-common/middleware"
15 | )
16 |
17 | //AppConfig - Application config
18 | type AppConfig struct {
19 | Dbs *Dbs
20 | Lang *locale.Locale
21 | Router *gin.Engine
22 | BaseURL string
23 | Cfg *config.Conf
24 | }
25 |
26 | type Dbs struct {
27 | DB *gorm.DB
28 | }
29 |
30 | // InitRouter - Create gin router
31 | func InitRouter(cfg *config.Conf, excludeList map[string]interface{}) (*gin.Engine, error) {
32 | router := gin.Default()
33 | router.Use(middleware.LoggerToFile(cfg.Log.LogFilePath, cfg.Log.LogFileName))
34 | router.Use(middleware.TenantValidator(excludeList))
35 | router.Use(gin.Recovery())
36 |
37 | return router, nil
38 | }
39 |
40 | // InitLocale - Create locale object
41 | func InitLocale(cfg *config.Conf) (*locale.Locale, error) {
42 | langLocale := locale.Locale{}
43 | dir, err := os.Getwd()
44 |
45 | if err != nil {
46 | log.Print("Not able to get current working director")
47 | panic(err)
48 | }
49 |
50 | log.Println("Locale path: " + dir + "/locale/*/*")
51 | lang := langLocale.New(cfg.App.Lang, dir+"/locale/*/*", "en-GR", "en-US", "zh-CN")
52 |
53 | return lang, nil
54 | }
55 |
56 | // InitDS establishes connections to fields in dataSources
57 | func InitDS(config *config.Conf) (*Dbs, error) {
58 | db, err := lgorm.New(config)
59 |
60 | if err != nil {
61 | log.Println("Connection failed")
62 | panic(err)
63 | }
64 |
65 | return &Dbs{
66 | DB: db,
67 | }, nil
68 | }
69 |
70 | //Close to be used in graceful server shutdown
71 | func Close(d *Dbs) error {
72 | //Todo Check the error
73 | //return d.DB.Close()
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/product-service/app/database/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 |
7 | "github.com/go-sql-driver/mysql"
8 |
9 | "micro/config"
10 | )
11 |
12 | func New(conf *config.Conf) (*sql.DB, error) {
13 | cfg := &mysql.Config{
14 | Net: "tcp",
15 | Addr: fmt.Sprintf("%v:%v", conf.Db.Host, conf.Db.Port),
16 | DBName: conf.Db.DbName,
17 | User: conf.Db.Username,
18 | Passwd: conf.Db.Password,
19 | AllowNativePasswords: true,
20 | ParseTime: true,
21 | }
22 |
23 | return sql.Open("mysql", cfg.FormatDSN())
24 | }
25 |
--------------------------------------------------------------------------------
/product-service/app/database/gorm/gorm.go:
--------------------------------------------------------------------------------
1 | package gorm
2 |
3 | import (
4 | "fmt"
5 |
6 | gosql "github.com/go-sql-driver/mysql"
7 | "gorm.io/driver/mysql"
8 | "gorm.io/gorm"
9 | "gorm.io/gorm/logger"
10 |
11 | "micro/config"
12 | )
13 |
14 | func New(conf *config.Conf) (*gorm.DB, error) {
15 | cfg := &gosql.Config{
16 | Net: "tcp",
17 | Addr: fmt.Sprintf("%v:%v", conf.Db.Host, conf.Db.Port),
18 | DBName: conf.Db.DbName,
19 | User: conf.Db.Username,
20 | Passwd: conf.Db.Password,
21 | AllowNativePasswords: true,
22 | ParseTime: true,
23 | }
24 |
25 | var logLevel logger.LogLevel
26 |
27 | if conf.Debug {
28 | logLevel = logger.Info
29 | } else {
30 | logLevel = logger.Error
31 | }
32 |
33 | return gorm.Open(mysql.Open(cfg.FormatDSN()), &gorm.Config{
34 | Logger: logger.Default.LogMode(logLevel),
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/product-service/cmd/api/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "micro/app"
7 | "micro/config"
8 | "micro/module"
9 | )
10 |
11 | func main() {
12 | cfg := config.AppConfig()
13 |
14 | log.Println("Starting server...")
15 |
16 | // initialize data sources
17 | dbs, err := app.InitDS(cfg)
18 |
19 | if err != nil {
20 | log.Fatalf("Unable to initialize data sources: %v\n", err)
21 | }
22 |
23 | //Close the database connection when stopped
24 | defer app.Close(dbs)
25 |
26 | //Add dependency injection
27 |
28 | //Public routes that don't have tenant checking
29 | excludeList := map[string]interface{}{
30 | "/health": "true",
31 | }
32 | router, err := app.InitRouter(cfg, excludeList)
33 |
34 | if err != nil {
35 | log.Fatalf("Unable to initialize routes: %v\n", err)
36 | }
37 |
38 | lang, err := app.InitLocale(cfg)
39 |
40 | if err != nil {
41 | log.Fatalf("Unable to initialize language locale: %v\n", err)
42 | }
43 |
44 | appConfig := app.AppConfig{
45 | Router: router,
46 | BaseURL: cfg.App.BaseURL,
47 | Lang: lang,
48 | Dbs: dbs,
49 | Cfg: cfg,
50 | }
51 |
52 | module.Inject(appConfig)
53 |
54 | if err != nil {
55 | log.Fatalf("Unable to inject dependencies: %v\n", err)
56 | }
57 |
58 | router.Run(":" + cfg.Server.Port)
59 | }
60 |
--------------------------------------------------------------------------------
/product-service/cmd/migrate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | "micro/app/database/db"
10 | "micro/config"
11 |
12 | "github.com/pressly/goose"
13 | )
14 |
15 | const dialect = "mysql"
16 |
17 | var (
18 | flags = flag.NewFlagSet("migrate", flag.ExitOnError)
19 | dir = flags.String("dir", "./migration", "directory with migration files")
20 | )
21 |
22 | func main() {
23 | flags.Usage = usage
24 | flags.Parse(os.Args[1:])
25 |
26 | args := flags.Args()
27 | if len(args) == 0 || args[0] == "-h" || args[0] == "--help" {
28 | flags.Usage()
29 | return
30 | }
31 |
32 | command := args[0]
33 |
34 | switch command {
35 | case "create":
36 | if err := goose.Run("create", nil, *dir, args[1:]...); err != nil {
37 | log.Fatalf("migrate run: %v", err)
38 | }
39 | return
40 | case "fix":
41 | if err := goose.Run("fix", nil, *dir); err != nil {
42 | log.Fatalf("migrate run: %v", err)
43 | }
44 | return
45 | }
46 |
47 | appConf := config.AppConfig()
48 | log.Println("Start migration...")
49 |
50 | // initialize data sources
51 | appDb, err := db.New(appConf)
52 |
53 | if err != nil {
54 | log.Fatalf(err.Error())
55 | }
56 |
57 | defer appDb.Close()
58 |
59 | if err := goose.SetDialect(dialect); err != nil {
60 | log.Fatal(err)
61 | }
62 |
63 | if err := goose.Run(command, appDb, *dir, args[1:]...); err != nil {
64 | log.Fatalf("migrate run: %v", err)
65 | }
66 | }
67 |
68 | func usage() {
69 | fmt.Println(usagePrefix)
70 | flags.PrintDefaults()
71 | fmt.Println(usageCommands)
72 | }
73 |
74 | var (
75 | usagePrefix = `Usage: migrate [OPTIONS] COMMAND
76 | Examples:
77 | migrate status
78 | Options:
79 | `
80 |
81 | usageCommands = `
82 | Commands:
83 | up Migrate the DB to the most recent version available
84 | up-by-one Migrate the DB up by 1
85 | up-to VERSION Migrate the DB to a specific VERSION
86 | down Roll back the version by 1
87 | down-to VERSION Roll back to a specific VERSION
88 | redo Re-run the latest migration
89 | reset Roll back all migrations
90 | status Dump the migration status for the current DB
91 | version Print the current version of the database
92 | create NAME [sql|go] Creates new migration file with the current timestamp
93 | fix Apply sequential ordering to migrations
94 | `
95 | )
96 |
--------------------------------------------------------------------------------
/product-service/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path"
8 | "path/filepath"
9 | "runtime"
10 | "time"
11 |
12 | "github.com/joeshaw/envdecode"
13 | common "github.com/krishnarajvr/micro-common"
14 | )
15 |
16 | func init() {
17 | common.LoadEnv()
18 | }
19 |
20 | type Conf struct {
21 | Debug bool `env:"DEBUG,required"`
22 | Server serverConf
23 | Db dbConf
24 | Log logConf
25 | App appConf
26 | Gateway gatewayConf
27 | }
28 |
29 | type serverConf struct {
30 | Port string `env:"SERVER_PORT,required"`
31 | TimeoutRead time.Duration `env:"SERVER_TIMEOUT_READ,required"`
32 | TimeoutWrite time.Duration `env:"SERVER_TIMEOUT_WRITE,required"`
33 | TimeoutIdle time.Duration `env:"SERVER_TIMEOUT_IDLE,required"`
34 | }
35 |
36 | type logConf struct {
37 | LogFilePath string `env:"Log_FILE_PATH"`
38 | LogFileName string `env:"LOG_FILE_NAME"`
39 | }
40 |
41 | type dbConf struct {
42 | Host string `env:"DB_HOST,required"`
43 | Port int `env:"DB_PORT,required"`
44 | Username string `env:"DB_USER,required"`
45 | Password string `env:"DB_PASS,required"`
46 | DbName string `env:"DB_NAME,required"`
47 | }
48 |
49 | type appConf struct {
50 | BaseURL string `env:"APP_BASE_URL"`
51 | Lang string `env:"APP_LANG"`
52 | Name string `env:"SERVICE_NAME"`
53 | RootDir string
54 | }
55 |
56 | type gatewayConf struct {
57 | URL string `env:"API_GATEWAY_URL"`
58 | Prefix string `env:"API_GATEWAY_PREFIX"`
59 | }
60 |
61 | func GetRootDir() string {
62 | _, b, _, _ := runtime.Caller(0)
63 |
64 | d := path.Join(path.Dir(b))
65 | return filepath.Dir(d)
66 | }
67 |
68 | func AppConfig() *Conf {
69 | var c Conf
70 |
71 | if err := envdecode.StrictDecode(&c); err != nil {
72 | log.Fatalf("Failed to decode: %s", err)
73 | }
74 |
75 | dir, err := os.Getwd()
76 |
77 | if err != nil {
78 | fmt.Print("Not able to get current working director")
79 | }
80 |
81 | if len(c.App.RootDir) <= 0 {
82 | c.App.RootDir = GetRootDir()
83 | }
84 |
85 | if len(c.App.Lang) <= 0 {
86 | c.App.Lang = "en-US"
87 | }
88 |
89 | if len(c.App.BaseURL) <= 0 {
90 | c.App.BaseURL = "api/v1"
91 | }
92 |
93 | if len(c.Log.LogFilePath) <= 0 {
94 | c.Log.LogFilePath = dir + "/log"
95 | }
96 |
97 | if len(c.Log.LogFileName) <= 0 {
98 | c.Log.LogFileName = "micro.log"
99 | }
100 |
101 | if len(c.App.Name) <= 0 {
102 | c.App.Name = "MicroService"
103 | }
104 |
105 | return &c
106 | }
107 |
--------------------------------------------------------------------------------
/product-service/deploy/.env:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 |
3 | SERVER_PORT=8080
4 | SERVER_TIMEOUT_READ=5s
5 | SERVER_TIMEOUT_WRITE=10s
6 | SERVER_TIMEOUT_IDLE=15s
7 |
8 | DB_HOST=db
9 | DB_PORT=3306
10 | DB_USER=micro_user
11 | DB_PASS=micro_pass
12 | DB_NAME=micro_db
13 | ACCESS_SECRET=jdnfksdmfksd
14 | REFRESH_SECRET=mcmvmkmsdnfsdmfdsjf
--------------------------------------------------------------------------------
/product-service/deploy/Dockerfile:
--------------------------------------------------------------------------------
1 | # Development environment
2 | FROM golang:1.15-alpine as build-env
3 | WORKDIR /micro
4 |
5 | RUN apk update && apk add --no-cache gcc musl-dev git
6 |
7 | COPY go.mod go.sum ./
8 | RUN go mod download
9 |
10 | COPY . .
11 |
12 | RUN go build -ldflags '-w -s' -a -o ./bin/api ./cmd/api \
13 | && go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate \
14 | && chmod +x /micro/deploy/bin/*
15 |
16 | EXPOSE 8080
17 |
--------------------------------------------------------------------------------
/product-service/deploy/bin/init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo 'Runing migrations...'
3 | cd micro;./migrate up > /dev/null 2>&1 &
4 |
5 | echo 'Start application...'
6 | /micro/api
7 |
--------------------------------------------------------------------------------
/product-service/deploy/prod.Dockerfile:
--------------------------------------------------------------------------------
1 | # Build environment
2 | # -----------------
3 | FROM golang:1.15-alpine as build-env
4 | WORKDIR /micro
5 |
6 | RUN apk update && apk add --no-cache gcc musl-dev git
7 |
8 | COPY go.mod go.sum ./
9 |
10 | RUN go mod download
11 |
12 | RUN go get -u github.com/swaggo/swag/cmd/swag \
13 | && go get -u github.com/vektra/mockery/cmd/mockery
14 |
15 | RUN pwd
16 |
17 | COPY . .
18 |
19 | RUN go mod vendor \
20 | && swag init -g cmd/api/main.go -o doc
21 |
22 | RUN go build -ldflags '-w -s' -a -o ./bin/api ./cmd/api \
23 | && go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate
24 |
25 |
26 |
27 | # Deployment environment
28 | # ----------------------
29 | FROM alpine
30 | RUN apk update
31 |
32 | COPY --from=build-env /micro/bin/api /micro/api
33 | COPY --from=build-env /micro/bin/migrate /micro/migrate
34 | COPY --from=build-env /micro/migration /micro/migration
35 | COPY --from=build-env /micro/locale /micro/locale
36 | COPY --from=build-env /micro/deploy/bin/init.sh /micro/bin/init.sh
37 |
38 | RUN chmod +x /micro/bin/*
39 |
40 | EXPOSE 8080
41 |
--------------------------------------------------------------------------------
/product-service/doc/swagdto/error.go:
--------------------------------------------------------------------------------
1 | package swagdto
2 |
3 | type ErrorNotFound struct {
4 | Code string `json:"code" example:"NOT_FOUND"`
5 | Message string `json:"message" example:"Resource not found"`
6 | }
7 |
8 | type ErrorBadRequest struct {
9 | Code string `json:"code" example:"BAD_REQUEST"`
10 | Message string `json:"message" example:"Bad Request"`
11 | Details []ErrorDetail `json:"details"`
12 | }
13 |
14 | type ErrorAccessDenied struct {
15 | Code string `json:"code" example:"ACCESS_DENIED"`
16 | Message string `json:"message" example:"Access Denied"`
17 | }
18 |
19 | type ErrorInternalError struct {
20 | Code string `json:"code" example:"INTERNAL_SERVER_ERROR"`
21 | Message string `json:"message" example:"Internal server error"`
22 | }
23 |
24 | type ErrorDetail struct {
25 | Code string `json:"code" example:"Required"`
26 | Target string `json:"target" example:"Name"`
27 | Message string `json:"message" example:"Name field is required"`
28 | }
29 |
30 | type Error400 struct {
31 | Status uint `json:"status" example:"400"`
32 | Error ErrorBadRequest `json:"error"`
33 | Data interface{} `json:"data"`
34 | }
35 |
36 | type Error404 struct {
37 | Status uint `json:"status" example:"404"`
38 | Error ErrorNotFound `json:"error"`
39 | Data interface{} `json:"data"`
40 | }
41 |
42 | type Error403 struct {
43 | Status uint `json:"status" example:"403"`
44 | Error ErrorAccessDenied `json:"error"`
45 | Data interface{} `json:"data"`
46 | }
47 |
48 | type Error500 struct {
49 | Status uint `json:"status" example:"500"`
50 | Error ErrorInternalError `json:"error"`
51 | Data interface{} `json:"data"`
52 | }
53 |
--------------------------------------------------------------------------------
/product-service/go.mod:
--------------------------------------------------------------------------------
1 | module micro
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
7 | github.com/gin-gonic/gin v1.7.1
8 | github.com/go-playground/validator/v10 v10.5.0 // indirect
9 | github.com/go-sql-driver/mysql v1.5.0
10 | github.com/golang/protobuf v1.5.2 // indirect
11 | github.com/jinzhu/gorm v1.9.16
12 | github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
13 | github.com/json-iterator/go v1.1.11 // indirect
14 | github.com/krishnarajvr/micro-common v1.0.0
15 | github.com/leodido/go-urn v1.2.1 // indirect
16 | github.com/magefile/mage v1.11.0 // indirect
17 | github.com/pressly/goose v2.7.0+incompatible
18 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
19 | github.com/sirupsen/logrus v1.8.1 // indirect
20 | github.com/stretchr/testify v1.7.0
21 | github.com/swaggo/gin-swagger v1.3.0
22 | github.com/swaggo/swag v1.7.0
23 | github.com/ugorji/go v1.2.5 // indirect
24 | github.com/unknwon/com v1.0.1 // indirect
25 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
26 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect
27 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
28 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 // indirect
29 | golang.org/x/text v0.3.6 // indirect
30 | gopkg.in/yaml.v2 v2.4.0 // indirect
31 | gorm.io/datatypes v1.0.1
32 | gorm.io/driver/mysql v1.0.5
33 | gorm.io/gorm v1.21.9
34 | )
35 |
--------------------------------------------------------------------------------
/product-service/locale/el-GR/language.yml:
--------------------------------------------------------------------------------
1 | hi: "Γειά %s"
2 | intro: "Με λένε {{.Name}} κα είμαι {{.Age}} χρονών"
--------------------------------------------------------------------------------
/product-service/locale/en-US/language.yml:
--------------------------------------------------------------------------------
1 | hi: "Hi %s"
2 | intro: "My name is {{.Name}} and I am {{.Age}} years old"
3 |
4 | message_not_found: "%s not found"
5 | message_invalid_id: "Invalid %s Id"
6 | message_internal_error: "Internal Server Error %s"
7 | message_already_exists: "%s already exists"
8 | message_unrecognized_field : "Unrecognized %s field"
9 | message_cannot_delete : "%s cannot delete"
10 | message_invalid_data: "%s invalid data"
--------------------------------------------------------------------------------
/product-service/locale/zh-CN/language.yml:
--------------------------------------------------------------------------------
1 | hi: "您好 %s"
2 | intro: "我叫 {{.Name}},今年 {{.Age}} 岁"
--------------------------------------------------------------------------------
/product-service/migration/20210316142300_create_product.sql:
--------------------------------------------------------------------------------
1 | -- +goose Up
2 | -- +goose StatementBegin
3 | CREATE TABLE IF NOT EXISTS products
4 | (
5 | id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
6 | tenant_id BIGINT UNSIGNED NOT NULL,
7 | name VARCHAR(255) NOT NULL,
8 | code VARCHAR(50) NOT NULL,
9 | description TEXT DEFAULT NULL,
10 | meta JSON DEFAULT NULL,
11 | is_active tinyint(1) unsigned DEFAULT '0',
12 | created_at TIMESTAMP NOT NULL,
13 | updated_at TIMESTAMP NULL,
14 | deleted_at TIMESTAMP NULL,
15 | PRIMARY KEY (id),
16 | UNIQUE KEY `unique_tenant_id_name` (`tenant_id`,`name`),
17 | UNIQUE KEY `unique_tenant_id_code` (`tenant_id`,`code`)
18 | )ENGINE=InnoDB DEFAULT CHARSET=utf8;
19 | -- +goose StatementEnd
20 |
21 |
22 | -- +goose Down
23 | -- +goose StatementBegin
24 | SELECT 'down SQL query';
25 | -- +goose StatementEnd
26 |
--------------------------------------------------------------------------------
/product-service/module/module.go:
--------------------------------------------------------------------------------
1 | package module
2 |
3 | import (
4 | "micro/app"
5 | product "micro/module/product"
6 | "os"
7 |
8 | docs "micro/doc"
9 |
10 | ginSwagger "github.com/swaggo/gin-swagger"
11 | "github.com/swaggo/gin-swagger/swaggerFiles"
12 | )
13 |
14 | // @title Micro Service API Document
15 | // @version 1.0
16 | // @description List of APIs for Micro Service
17 | // @termsOfService http://swagger.io/terms/
18 |
19 | // @securityDefinitions.apikey ApiKeyAuth
20 | // @in header
21 | // @name Authorization
22 |
23 | // @host localhost:8083
24 | // @BasePath /api/v1
25 | func Inject(appConfig app.AppConfig) {
26 | product.Inject(appConfig)
27 |
28 | //Swagger Doc details
29 | url := os.Getenv("API_GATEWAY_URL")
30 | prefix := os.Getenv("API_GATEWAY_PREFIX")
31 |
32 | if len(url) == 0 {
33 | url = "localhost:" + appConfig.Cfg.Server.Port
34 | }
35 |
36 | if len(prefix) == 0 {
37 | prefix = appConfig.Cfg.App.BaseURL
38 | }
39 |
40 | docs.SwaggerInfo.Title = "Product Service API Document"
41 | docs.SwaggerInfo.Description = "List of APIs for Product Service."
42 | docs.SwaggerInfo.Version = "1.0"
43 | docs.SwaggerInfo.Host = url
44 | docs.SwaggerInfo.BasePath = prefix
45 | docs.SwaggerInfo.Schemes = []string{"https", "http"}
46 | //Init Swagger routes
47 | appConfig.Router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
48 | }
49 |
--------------------------------------------------------------------------------
/product-service/module/product/inject.go:
--------------------------------------------------------------------------------
1 | package product
2 |
3 | import (
4 | "micro/app"
5 | "micro/module/product/repo"
6 | "micro/module/product/service"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/krishnarajvr/micro-common/locale"
10 | )
11 |
12 | type HandlerConfig struct {
13 | R *gin.Engine
14 | ProductService service.IProductService
15 | BaseURL string
16 | Lang *locale.Locale
17 | }
18 |
19 | //Inject dependencies
20 | func Inject(appConfig app.AppConfig) {
21 |
22 | productRepo := repo.NewProductRepo(appConfig.Dbs.DB)
23 |
24 | productService := service.NewService(service.ServiceConfig{
25 | ProductRepo: productRepo,
26 | Lang: appConfig.Lang,
27 | })
28 |
29 | InitRoutes(HandlerConfig{
30 | R: appConfig.Router,
31 | ProductService: productService,
32 | BaseURL: appConfig.BaseURL,
33 | Lang: appConfig.Lang,
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/product-service/module/product/mocks/IProductRepo.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | common "github.com/krishnarajvr/micro-common"
7 | mock "github.com/stretchr/testify/mock"
8 |
9 | model "micro/module/product/model"
10 | )
11 |
12 | // IProductRepo is an autogenerated mock type for the IProductRepo type
13 | type IProductRepo struct {
14 | mock.Mock
15 | }
16 |
17 | // Add provides a mock function with given fields: form
18 | func (_m *IProductRepo) Add(form *model.Product) (*model.Product, error) {
19 | ret := _m.Called(form)
20 |
21 | var r0 *model.Product
22 | if rf, ok := ret.Get(0).(func(*model.Product) *model.Product); ok {
23 | r0 = rf(form)
24 | } else {
25 | if ret.Get(0) != nil {
26 | r0 = ret.Get(0).(*model.Product)
27 | }
28 | }
29 |
30 | var r1 error
31 | if rf, ok := ret.Get(1).(func(*model.Product) error); ok {
32 | r1 = rf(form)
33 | } else {
34 | r1 = ret.Error(1)
35 | }
36 |
37 | return r0, r1
38 | }
39 |
40 | // Delete provides a mock function with given fields: tenantId, id
41 | func (_m *IProductRepo) Delete(tenantId int, id int) (*model.Product, error) {
42 | ret := _m.Called(tenantId, id)
43 |
44 | var r0 *model.Product
45 | if rf, ok := ret.Get(0).(func(int, int) *model.Product); ok {
46 | r0 = rf(tenantId, id)
47 | } else {
48 | if ret.Get(0) != nil {
49 | r0 = ret.Get(0).(*model.Product)
50 | }
51 | }
52 |
53 | var r1 error
54 | if rf, ok := ret.Get(1).(func(int, int) error); ok {
55 | r1 = rf(tenantId, id)
56 | } else {
57 | r1 = ret.Error(1)
58 | }
59 |
60 | return r0, r1
61 | }
62 |
63 | // Get provides a mock function with given fields: tenantId, id
64 | func (_m *IProductRepo) Get(tenantId int, id int) (*model.ProductDetail, error) {
65 | ret := _m.Called(tenantId, id)
66 |
67 | var r0 *model.ProductDetail
68 | if rf, ok := ret.Get(0).(func(int, int) *model.ProductDetail); ok {
69 | r0 = rf(tenantId, id)
70 | } else {
71 | if ret.Get(0) != nil {
72 | r0 = ret.Get(0).(*model.ProductDetail)
73 | }
74 | }
75 |
76 | var r1 error
77 | if rf, ok := ret.Get(1).(func(int, int) error); ok {
78 | r1 = rf(tenantId, id)
79 | } else {
80 | r1 = ret.Error(1)
81 | }
82 |
83 | return r0, r1
84 | }
85 |
86 | // List provides a mock function with given fields: tenantId, page, filters
87 | func (_m *IProductRepo) List(tenantId int, page common.Pagination, filters model.ProductFilterList) (model.ProductsList, *common.PageResult, error) {
88 | ret := _m.Called(tenantId, page, filters)
89 |
90 | var r0 model.ProductsList
91 | if rf, ok := ret.Get(0).(func(int, common.Pagination, model.ProductFilterList) model.ProductsList); ok {
92 | r0 = rf(tenantId, page, filters)
93 | } else {
94 | if ret.Get(0) != nil {
95 | r0 = ret.Get(0).(model.ProductsList)
96 | }
97 | }
98 |
99 | var r1 *common.PageResult
100 | if rf, ok := ret.Get(1).(func(int, common.Pagination, model.ProductFilterList) *common.PageResult); ok {
101 | r1 = rf(tenantId, page, filters)
102 | } else {
103 | if ret.Get(1) != nil {
104 | r1 = ret.Get(1).(*common.PageResult)
105 | }
106 | }
107 |
108 | var r2 error
109 | if rf, ok := ret.Get(2).(func(int, common.Pagination, model.ProductFilterList) error); ok {
110 | r2 = rf(tenantId, page, filters)
111 | } else {
112 | r2 = ret.Error(2)
113 | }
114 |
115 | return r0, r1, r2
116 | }
117 |
118 | // Patch provides a mock function with given fields: form, id
119 | func (_m *IProductRepo) Patch(form *model.ProductPatchForm, id int) (*model.Product, error) {
120 | ret := _m.Called(form, id)
121 |
122 | var r0 *model.Product
123 | if rf, ok := ret.Get(0).(func(*model.ProductPatchForm, int) *model.Product); ok {
124 | r0 = rf(form, id)
125 | } else {
126 | if ret.Get(0) != nil {
127 | r0 = ret.Get(0).(*model.Product)
128 | }
129 | }
130 |
131 | var r1 error
132 | if rf, ok := ret.Get(1).(func(*model.ProductPatchForm, int) error); ok {
133 | r1 = rf(form, id)
134 | } else {
135 | r1 = ret.Error(1)
136 | }
137 |
138 | return r0, r1
139 | }
140 |
--------------------------------------------------------------------------------
/product-service/module/product/mocks/IProductService.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | common "github.com/krishnarajvr/micro-common"
7 | datatypes "gorm.io/datatypes"
8 |
9 | mock "github.com/stretchr/testify/mock"
10 |
11 | model "micro/module/product/model"
12 | )
13 |
14 | // IProductService is an autogenerated mock type for the IProductService type
15 | type IProductService struct {
16 | mock.Mock
17 | }
18 |
19 | // Add provides a mock function with given fields: tenantId, content
20 | func (_m *IProductService) Add(tenantId int, content *model.ProductForm) (*model.ProductDto, error) {
21 | ret := _m.Called(tenantId, content)
22 |
23 | var r0 *model.ProductDto
24 | if rf, ok := ret.Get(0).(func(int, *model.ProductForm) *model.ProductDto); ok {
25 | r0 = rf(tenantId, content)
26 | } else {
27 | if ret.Get(0) != nil {
28 | r0 = ret.Get(0).(*model.ProductDto)
29 | }
30 | }
31 |
32 | var r1 error
33 | if rf, ok := ret.Get(1).(func(int, *model.ProductForm) error); ok {
34 | r1 = rf(tenantId, content)
35 | } else {
36 | r1 = ret.Error(1)
37 | }
38 |
39 | return r0, r1
40 | }
41 |
42 | // CheckProductSchema provides a mock function with given fields: tenantId, jsonData, validateFor
43 | func (_m *IProductService) CheckProductSchema(tenantId int, jsonData []byte, validateFor string) (map[string]interface{}, *map[string]interface{}, error) {
44 | ret := _m.Called(tenantId, jsonData, validateFor)
45 |
46 | var r0 map[string]interface{}
47 | if rf, ok := ret.Get(0).(func(int, []byte, string) map[string]interface{}); ok {
48 | r0 = rf(tenantId, jsonData, validateFor)
49 | } else {
50 | if ret.Get(0) != nil {
51 | r0 = ret.Get(0).(map[string]interface{})
52 | }
53 | }
54 |
55 | var r1 *map[string]interface{}
56 | if rf, ok := ret.Get(1).(func(int, []byte, string) *map[string]interface{}); ok {
57 | r1 = rf(tenantId, jsonData, validateFor)
58 | } else {
59 | if ret.Get(1) != nil {
60 | r1 = ret.Get(1).(*map[string]interface{})
61 | }
62 | }
63 |
64 | var r2 error
65 | if rf, ok := ret.Get(2).(func(int, []byte, string) error); ok {
66 | r2 = rf(tenantId, jsonData, validateFor)
67 | } else {
68 | r2 = ret.Error(2)
69 | }
70 |
71 | return r0, r1, r2
72 | }
73 |
74 | // ConvertMapToJson provides a mock function with given fields: metaMap
75 | func (_m *IProductService) ConvertMapToJson(metaMap *map[string]interface{}) (datatypes.JSON, error) {
76 | ret := _m.Called(metaMap)
77 |
78 | var r0 datatypes.JSON
79 | if rf, ok := ret.Get(0).(func(*map[string]interface{}) datatypes.JSON); ok {
80 | r0 = rf(metaMap)
81 | } else {
82 | if ret.Get(0) != nil {
83 | r0 = ret.Get(0).(datatypes.JSON)
84 | }
85 | }
86 |
87 | var r1 error
88 | if rf, ok := ret.Get(1).(func(*map[string]interface{}) error); ok {
89 | r1 = rf(metaMap)
90 | } else {
91 | r1 = ret.Error(1)
92 | }
93 |
94 | return r0, r1
95 | }
96 |
97 | // Delete provides a mock function with given fields: tenantId, id
98 | func (_m *IProductService) Delete(tenantId int, id int) (*model.ProductDto, error) {
99 | ret := _m.Called(tenantId, id)
100 |
101 | var r0 *model.ProductDto
102 | if rf, ok := ret.Get(0).(func(int, int) *model.ProductDto); ok {
103 | r0 = rf(tenantId, id)
104 | } else {
105 | if ret.Get(0) != nil {
106 | r0 = ret.Get(0).(*model.ProductDto)
107 | }
108 | }
109 |
110 | var r1 error
111 | if rf, ok := ret.Get(1).(func(int, int) error); ok {
112 | r1 = rf(tenantId, id)
113 | } else {
114 | r1 = ret.Error(1)
115 | }
116 |
117 | return r0, r1
118 | }
119 |
120 | // Get provides a mock function with given fields: tenantId, id
121 | func (_m *IProductService) Get(tenantId int, id int) (*model.ProductDetailDto, error) {
122 | ret := _m.Called(tenantId, id)
123 |
124 | var r0 *model.ProductDetailDto
125 | if rf, ok := ret.Get(0).(func(int, int) *model.ProductDetailDto); ok {
126 | r0 = rf(tenantId, id)
127 | } else {
128 | if ret.Get(0) != nil {
129 | r0 = ret.Get(0).(*model.ProductDetailDto)
130 | }
131 | }
132 |
133 | var r1 error
134 | if rf, ok := ret.Get(1).(func(int, int) error); ok {
135 | r1 = rf(tenantId, id)
136 | } else {
137 | r1 = ret.Error(1)
138 | }
139 |
140 | return r0, r1
141 | }
142 |
143 | // List provides a mock function with given fields: tenantId, page, filters
144 | func (_m *IProductService) List(tenantId int, page common.Pagination, filters model.ProductFilterList) (model.ProductListDtos, *common.PageResult, error) {
145 | ret := _m.Called(tenantId, page, filters)
146 |
147 | var r0 model.ProductListDtos
148 | if rf, ok := ret.Get(0).(func(int, common.Pagination, model.ProductFilterList) model.ProductListDtos); ok {
149 | r0 = rf(tenantId, page, filters)
150 | } else {
151 | if ret.Get(0) != nil {
152 | r0 = ret.Get(0).(model.ProductListDtos)
153 | }
154 | }
155 |
156 | var r1 *common.PageResult
157 | if rf, ok := ret.Get(1).(func(int, common.Pagination, model.ProductFilterList) *common.PageResult); ok {
158 | r1 = rf(tenantId, page, filters)
159 | } else {
160 | if ret.Get(1) != nil {
161 | r1 = ret.Get(1).(*common.PageResult)
162 | }
163 | }
164 |
165 | var r2 error
166 | if rf, ok := ret.Get(2).(func(int, common.Pagination, model.ProductFilterList) error); ok {
167 | r2 = rf(tenantId, page, filters)
168 | } else {
169 | r2 = ret.Error(2)
170 | }
171 |
172 | return r0, r1, r2
173 | }
174 |
175 | // Patch provides a mock function with given fields: form, id
176 | func (_m *IProductService) Patch(form *model.ProductPatchForm, id int) (*model.ProductDto, error) {
177 | ret := _m.Called(form, id)
178 |
179 | var r0 *model.ProductDto
180 | if rf, ok := ret.Get(0).(func(*model.ProductPatchForm, int) *model.ProductDto); ok {
181 | r0 = rf(form, id)
182 | } else {
183 | if ret.Get(0) != nil {
184 | r0 = ret.Get(0).(*model.ProductDto)
185 | }
186 | }
187 |
188 | var r1 error
189 | if rf, ok := ret.Get(1).(func(*model.ProductPatchForm, int) error); ok {
190 | r1 = rf(form, id)
191 | } else {
192 | r1 = ret.Error(1)
193 | }
194 |
195 | return r0, r1
196 | }
197 |
--------------------------------------------------------------------------------
/product-service/module/product/model/product.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/datatypes"
7 | "gorm.io/gorm"
8 | )
9 |
10 | type Products []*Product
11 |
12 | type Product struct {
13 | gorm.Model
14 | TenantId int
15 | Name string
16 | Code string
17 | Description string
18 | Meta datatypes.JSON
19 | IsActive int
20 | CreatedAt time.Time
21 | UpdatedAt time.Time
22 | }
23 |
24 | type ProductForm struct {
25 | Name string `json:"name" example:"John" label:"Name" valid:"Required;MinSize(2);MaxSize(255)"`
26 | Code string `json:"code" example:"1001" label:"Code" valid:"Required;AlphaNumeric;MinSize(1);MaxSize(255)"`
27 | Description string `json:"description" label:"Description" example:"product description"`
28 | Meta datatypes.JSON `json:"meta" label:"Meta" example:"{"businessaccountlead": "smith@gmail.com","technicalcontactlead": "smith@gmail.com"}"`
29 | IsActive int `json:"isActive" valid:"Binary" label:"IsActive" example:"1"`
30 | }
31 |
32 | type ProductDtos []*ProductDto
33 |
34 | type ProductDto struct {
35 | ID uint `json:"id" example:"1"`
36 | Name string `json:"name" example:"John"`
37 | Code string `json:"code" example:"1001"`
38 | Description string `json:"description" example:"product description"`
39 | Meta datatypes.JSON `json:"meta" example:"{country:smith@gmail.com,author:smith@gmail.com}"`
40 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
41 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
42 | IsActive int `json:"isActive" valid:"Binary" label:"IsActive" example:"1"`
43 | }
44 |
45 | func (v Product) ToDto() *ProductDto {
46 | return &ProductDto{
47 | ID: v.ID,
48 | Name: v.Name,
49 | Code: v.Code,
50 | Description: v.Description,
51 | Meta: v.Meta,
52 | CreatedAt: v.CreatedAt,
53 | UpdatedAt: v.UpdatedAt,
54 | IsActive: v.IsActive,
55 | }
56 | }
57 |
58 | func (vs Products) ToDto() ProductDtos {
59 | dtos := make([]*ProductDto, len(vs))
60 | for i, b := range vs {
61 | dtos[i] = b.ToDto()
62 | }
63 |
64 | return dtos
65 | }
66 |
67 | type ProductPatchForm struct {
68 | Name string `json:"name" example:"John" label:"Name"`
69 | Code string `json:"code" example:"1001" label:"Code"`
70 | Description string `json:"description" label:"Description" example:"product description"`
71 | Meta datatypes.JSON `json:"meta" label:"Meta" example:"{"country": "smith@gmail.com","author": "smith@gmail.com"}"`
72 | IsActive int `json:"isActive" example:"1" label:"IsActive" valid:"Binary"`
73 | }
74 |
75 | func (f *ProductPatchForm) ToModel() (*Product, error) {
76 | return &Product{
77 | Name: f.Name,
78 | Code: f.Code,
79 | Description: f.Description,
80 | Meta: f.Meta,
81 | IsActive: f.IsActive,
82 | }, nil
83 | }
84 |
85 | func (f *ProductForm) ToModel() (*Product, error) {
86 | return &Product{
87 | Name: f.Name,
88 | Code: f.Code,
89 | Description: f.Description,
90 | Meta: f.Meta,
91 | IsActive: f.IsActive,
92 | }, nil
93 | }
94 |
95 | type ProductFilterList struct {
96 | Code string `json:"code" example:"1000"`
97 | Name string `json:"name" example:"Product 1"`
98 | }
99 |
100 | type ProductsList []*ProductList
101 | type ProductListDtos []*ProductListDto
102 |
103 | type ProductList struct {
104 | ID uint
105 | Name string
106 | Email string
107 | Code string
108 | Description string
109 | Meta datatypes.JSON
110 | Phone string
111 | IsActive int
112 | CreatedAt time.Time
113 | UpdatedAt time.Time
114 | }
115 |
116 | type ProductListDto struct {
117 | ID uint `json:"id" example:"1"`
118 | Name string `json:"name" example:"John"`
119 | Code string `json:"code" example:"1001"`
120 | Description string `json:"description" label:"Description" example:"product description"`
121 | IsActive int `json:"isActive" example:"1"`
122 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
123 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
124 | }
125 |
126 | type ProductDetail struct {
127 | ID uint
128 | Name string
129 | Code string
130 | Description string
131 | Meta datatypes.JSON
132 | IsActive int
133 | CreatedAt time.Time
134 | UpdatedAt time.Time
135 | }
136 |
137 | type ProductDetailDto struct {
138 | ID uint `json:"id" example:"1"`
139 | Name string `json:"name" example:"John"`
140 | Code string `json:"code" example:"1001"`
141 | Description string `json:"description" label:"Description" example:"product description"`
142 | Meta datatypes.JSON `json:"meta" label:"Meta" example:"{"country": "smith@gmail.com","author": "smith@gmail.com"}"`
143 | IsActive int `json:"isActive" example:"1"`
144 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
145 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
146 | }
147 |
148 | func (v ProductList) ToProductListDto() *ProductListDto {
149 | return &ProductListDto{
150 | ID: v.ID,
151 | Name: v.Name,
152 | Code: v.Code,
153 | Description: v.Description,
154 | IsActive: v.IsActive,
155 | CreatedAt: v.CreatedAt,
156 | UpdatedAt: v.UpdatedAt,
157 | }
158 | }
159 |
160 | func (v ProductDetail) ToProductDetailDto() *ProductDetailDto {
161 | return &ProductDetailDto{
162 | ID: v.ID,
163 | Name: v.Name,
164 | Code: v.Code,
165 | Description: v.Description,
166 | IsActive: v.IsActive,
167 | CreatedAt: v.CreatedAt,
168 | UpdatedAt: v.UpdatedAt,
169 | Meta: v.Meta,
170 | }
171 | }
172 |
173 | func (bsl ProductsList) ToDto() ProductListDtos {
174 | dtos := make([]*ProductListDto, len(bsl))
175 | for i, b := range bsl {
176 | dtos[i] = b.ToProductListDto()
177 | }
178 |
179 | return dtos
180 | }
181 |
--------------------------------------------------------------------------------
/product-service/module/product/repo/product.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "micro/module/product/model"
5 |
6 | "gorm.io/gorm"
7 |
8 | common "github.com/krishnarajvr/micro-common"
9 | )
10 |
11 | type IProductRepo interface {
12 | List(tenantId int, page common.Pagination, filters model.ProductFilterList) (model.ProductsList, *common.PageResult, error)
13 | Add(form *model.Product) (*model.Product, error)
14 | Get(tenantId int, id int) (*model.ProductDetail, error)
15 | Patch(form *model.ProductPatchForm, id int) (*model.Product, error)
16 | Delete(tenantId int, id int) (*model.Product, error)
17 | }
18 |
19 | type ProductRepo struct {
20 | DB *gorm.DB
21 | }
22 |
23 | func NewProductRepo(db *gorm.DB) ProductRepo {
24 | return ProductRepo{
25 | DB: db,
26 | }
27 | }
28 |
29 | func (r ProductRepo) List(tenantId int, page common.Pagination, filters model.ProductFilterList) (model.ProductsList, *common.PageResult, error) {
30 | products := make([]*model.ProductList, 0)
31 | var totalCount int64
32 | var db = r.DB
33 | db = db.Scopes(common.Paginate(page)).
34 | Table("products").
35 | Select(
36 | `products.id,
37 | products.name,
38 | products.code,
39 | products.is_active,
40 | products.created_at, products.updated_at`).
41 | Where("products.tenant_id = ? ", tenantId)
42 |
43 | if len(filters.Code) > 0 {
44 | db = db.Where(`products.code LIKE ? `, filters.Code+"%")
45 | }
46 |
47 | if len(filters.Name) > 0 {
48 | db = db.Where(`products.name LIKE ? `, filters.Name+"%")
49 | }
50 |
51 | err := db.Find(&products).Count(&totalCount).Error
52 |
53 | if err != nil {
54 | return nil, nil, err
55 | }
56 |
57 | pageResult := common.PageInfo(page, totalCount)
58 |
59 | if len(products) == 0 {
60 | return nil, nil, nil
61 | }
62 |
63 | return products, &pageResult, nil
64 | }
65 |
66 | func (r ProductRepo) Add(product *model.Product) (*model.Product, error) {
67 | if err := r.DB.Create(&product).Error; err != nil {
68 | return nil, err
69 | }
70 |
71 | return product, nil
72 | }
73 |
74 | func (r ProductRepo) Get(tenantId int, id int) (*model.ProductDetail, error) {
75 | product := new(model.ProductDetail)
76 |
77 | dbError := r.DB.Table("products").
78 | Select(
79 | `products.id,
80 | products.name,
81 | products.code,
82 | products.description,
83 | products.meta,
84 | products.is_active,
85 | products.created_at, products.updated_at`).
86 | Where(`products.id = ? AND products.tenant_id = ?`, id, tenantId).
87 | First(&product).Error
88 |
89 | if dbError != nil {
90 | return nil, dbError
91 | }
92 |
93 | return product, nil
94 | }
95 |
96 | func (r ProductRepo) Patch(form *model.ProductPatchForm, id int) (*model.Product, error) {
97 | product, err := form.ToModel()
98 |
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | if err := r.DB.Where("id = ?", id).Updates(&product).Error; err != nil {
104 | return nil, err
105 | }
106 |
107 | return product, nil
108 | }
109 |
110 | func (r ProductRepo) Delete(tenantId int, id int) (*model.Product, error) {
111 | product := new(model.Product)
112 | // Unscoped() hard delete operation
113 | if err := r.DB.Unscoped().Where("id = ? and tenant_id = ?", id, tenantId).Delete(&product).Error; err != nil {
114 | return nil, err
115 | }
116 |
117 | return product, nil
118 | }
119 |
--------------------------------------------------------------------------------
/product-service/module/product/routes.go:
--------------------------------------------------------------------------------
1 | package product
2 |
3 | //InitRoutes for the module
4 | func InitRoutes(c HandlerConfig) {
5 | h := Handler{
6 | ProductService: c.ProductService,
7 | Lang: c.Lang,
8 | }
9 |
10 | //Set api group
11 | g := c.R.Group(c.BaseURL)
12 | g.GET("/products/:id", h.GetProduct)
13 | g.GET("/products", h.ListProducts)
14 | g.POST("/products", h.AddProduct)
15 | g.PATCH("/products/:id", h.PatchProducts)
16 | g.DELETE("/products/:id", h.DeleteProduct)
17 | }
18 |
--------------------------------------------------------------------------------
/product-service/module/product/service/product_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "encoding/json"
5 | "micro/module/product/model"
6 | "micro/module/product/repo"
7 |
8 | "gorm.io/datatypes"
9 |
10 | common "github.com/krishnarajvr/micro-common"
11 |
12 | "github.com/krishnarajvr/micro-common/locale"
13 | )
14 |
15 | type IProductService interface {
16 | List(tenantId int, page common.Pagination, filters model.ProductFilterList) (model.ProductListDtos, *common.PageResult, error)
17 | Get(tenantId int, id int) (*model.ProductDetailDto, error)
18 | Add(tenantId int, content *model.ProductForm) (*model.ProductDto, error)
19 | Patch(form *model.ProductPatchForm, id int) (*model.ProductDto, error)
20 | ConvertMapToJson(metaMap *map[string]interface{}) (datatypes.JSON, error)
21 | CheckProductSchema(tenantId int, jsonData []byte, validateFor string) (map[string]interface{}, *map[string]interface{}, error)
22 | Delete(tenantId int, id int) (*model.ProductDto, error)
23 | }
24 |
25 | type ServiceConfig struct {
26 | ProductRepo repo.IProductRepo
27 | Lang *locale.Locale
28 | }
29 |
30 | type Service struct {
31 | ProductRepo repo.IProductRepo
32 | Lang *locale.Locale
33 | }
34 |
35 | func NewService(c ServiceConfig) IProductService {
36 | return &Service{
37 | ProductRepo: c.ProductRepo,
38 | Lang: c.Lang,
39 | }
40 | }
41 |
42 | func (s *Service) List(tenantId int, page common.Pagination, filters model.ProductFilterList) (model.ProductListDtos, *common.PageResult, error) {
43 | productsList, pageResult, err := s.ProductRepo.List(tenantId, page, filters)
44 |
45 | if err != nil {
46 | return nil, nil, err
47 | }
48 |
49 | productListDtos := productsList.ToDto()
50 |
51 | return productListDtos, pageResult, err
52 | }
53 |
54 | func (s *Service) Add(tenantId int, form *model.ProductForm) (*model.ProductDto, error) {
55 | productModel, err := form.ToModel()
56 | productModel.TenantId = tenantId
57 |
58 | if err != nil {
59 | return nil, err
60 | }
61 |
62 | product, err := s.ProductRepo.Add(productModel)
63 |
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | productDto := product.ToDto()
69 |
70 | return productDto, nil
71 | }
72 |
73 | func (s *Service) Get(tenantId int, id int) (*model.ProductDetailDto, error) {
74 | productDetail, err := s.ProductRepo.Get(tenantId, id)
75 |
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | productDetailDto := productDetail.ToProductDetailDto()
81 |
82 | return productDetailDto, nil
83 | }
84 |
85 | func (s *Service) Patch(form *model.ProductPatchForm, id int) (*model.ProductDto, error) {
86 | product, err := s.ProductRepo.Patch(form, id)
87 |
88 | if err != nil {
89 | return nil, err
90 | }
91 |
92 | productDto := product.ToDto()
93 |
94 | return productDto, nil
95 | }
96 |
97 | func (s *Service) CheckProductSchema(tenantId int, jsonData []byte, validateFor string) (map[string]interface{}, *map[string]interface{}, error) {
98 | // currently JSONSCHEMA is hardcoded constant value, feature will read from database
99 | const METAJSONSCHEMA = `{
100 | "$schema": "http://json-schema.org/draft-04/schema#",
101 | "title": "product",
102 | "description": "product creation",
103 | "type": "object",
104 | "properties": {
105 | "author": {
106 | "description": "Product Author",
107 | "type": "string",
108 | "minLength": 1
109 | },
110 | "country": {
111 | "description": "Product Country",
112 | "type": "string",
113 | "minLength": 1
114 | }
115 | },
116 | "required": ["author"]
117 | }`
118 |
119 | var schemaDef map[string]interface{}
120 | var formMeta *map[string]interface{}
121 |
122 | if validateFor == "validateMetaData" {
123 |
124 | // convert jsonschema to map[string]interface{}
125 | err := json.Unmarshal([]byte(METAJSONSCHEMA), &schemaDef)
126 |
127 | if err != nil {
128 | return nil, nil, err
129 | }
130 |
131 | }
132 | err := json.Unmarshal([]byte(jsonData), &formMeta)
133 |
134 | return schemaDef, formMeta, err
135 | }
136 |
137 | func (s *Service) ConvertMapToJson(metaMap *map[string]interface{}) (datatypes.JSON, error) {
138 | metaJson, err := json.Marshal(&metaMap)
139 |
140 | if err != nil {
141 | return nil, err
142 | }
143 | return metaJson, nil
144 | }
145 |
146 | func (s *Service) Delete(tenantId int, id int) (*model.ProductDto, error) {
147 | product, err := s.ProductRepo.Delete(tenantId, id)
148 |
149 | if err != nil {
150 | return nil, err
151 | }
152 |
153 | productDto := product.ToDto()
154 |
155 | return productDto, nil
156 | }
157 |
--------------------------------------------------------------------------------
/product-service/module/product/swagger/product.go:
--------------------------------------------------------------------------------
1 | package swagger
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type ProductDtos []*ProductDto
8 |
9 | type ProductDto struct {
10 | ID uint `json:"id" example:"1"`
11 | Name string `json:"name" example:"Product 1"`
12 | Code string `json:"code" example:"10011" `
13 | Description string `json:"description" example:"product description"`
14 | Meta ProductMeta `json:"meta"`
15 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
16 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
17 | }
18 |
19 | type ProductListDtos []*ProductListDto
20 |
21 | type ProductListDto struct {
22 | ID uint `json:"id" example:"1"`
23 | Name string `json:"name" example:"Product 1"`
24 | Code string `json:"code" example:"10011" `
25 | Description string `json:"description" example:"product description"`
26 | Meta ProductMeta `json:"meta"`
27 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
28 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
29 | }
30 |
31 | type ProductDetailDto struct {
32 | ID uint `json:"id" example:"1"`
33 | Name string `json:"name" example:"Product 1"`
34 | Code string `json:"code" example:"10011" `
35 | Description string `json:"description" example:"product description"`
36 | Meta ProductMeta `json:"meta"`
37 | CreatedAt time.Time `json:"createdAt" example:"2021-02-02T02:52:24Z"`
38 | UpdatedAt time.Time `json:"updatedAt" example:"2021-02-02T02:52:24Z"`
39 | }
40 |
41 | type ProductAddData struct {
42 | ProductData ProductDto `json:"product"`
43 | }
44 |
45 | type ProductupdateData struct {
46 | ProductData ProductDto `json:"product"`
47 | }
48 |
49 | type ProductSampleData struct {
50 | ProductData ProductDetailDto `json:"product"`
51 | }
52 |
53 | type ProductSampleListData struct {
54 | ProductData ProductListDtos `json:"product"`
55 | }
56 | type ProductAddResponse struct {
57 | Status uint `json:"status" example:"200"`
58 | Error interface{} `json:"error"`
59 | Data ProductAddData `json:"data"`
60 | }
61 |
62 | type ProductUpdateResponse struct {
63 | Status uint `json:"status" example:"200"`
64 | Error interface{} `json:"error"`
65 | Data ProductupdateData `json:"data"`
66 | }
67 |
68 | type ProductListResponse struct {
69 | Status uint `json:"status" example:"200"`
70 | Error interface{} `json:"error"`
71 | Data ProductSampleListData `json:"data"`
72 | }
73 |
74 | type ProductResponse struct {
75 | Status uint `json:"status" example:"200"`
76 | Error interface{} `json:"error"`
77 | Data ProductSampleData `json:"data"`
78 | }
79 |
80 | type ProductForm struct {
81 | Name string `json:"name" example:"Product 1" valid:"Required;MinSize(2);MaxSize(255)"`
82 | Code string `json:"code" example:"1001" valid:"Required;AlphaNumeric;MinSize(1);MaxSize(255)"`
83 | Description string `json:"description" example:"product description"`
84 | Meta ProductMeta `json:"meta"`
85 | IsActive int `json:"isActive" example:"1"`
86 | }
87 |
88 | type ProductPatchForm struct {
89 | Name string `json:"name" example:"Product 1" valid:"Required;MinSize(2);MaxSize(255)"`
90 | Code string `json:"code" example:"1001" valid:"Required;AlphaNumeric;MinSize(1);MaxSize(255)"`
91 | Description string `json:"description" example:"product description"`
92 | Meta ProductMeta `json:"meta"`
93 | }
94 |
95 | type ProductMeta struct {
96 | Author string `json:"author" example:"AuthorA"`
97 | Country string `json:"country" example:"USA"`
98 | }
99 |
--------------------------------------------------------------------------------