├── .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 | ![image Architecture](https://raw.githubusercontent.com/krishnarajvr/microservice-mono-gin-gorm/master/assets/golang-monorepo.png) 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 | --------------------------------------------------------------------------------