├── .gitignore ├── Makefile ├── README.md ├── cmd └── api │ └── main.go ├── configs ├── env.config.go └── test.config.go ├── database ├── .gitignore ├── .sequelizerc ├── Makefile ├── migrations │ └── 20250301081607-users.ts ├── package.json ├── seeders │ └── 20250301081607-users.ts ├── sequelize.js ├── tsconfig.build.json └── tsconfig.json ├── diagram.png ├── docker-compose.yml ├── domain ├── entities │ └── users.entitie.go ├── exceptions │ └── users.exception.go ├── repositories │ └── users.repositorie.go └── services │ └── users │ ├── mapper.go │ ├── service.go │ └── service_test.go ├── external ├── deployments │ ├── docker │ │ └── Dockerfile │ └── kubernetes │ │ └── .gitkeep └── scripts │ └── .gitkeep ├── go.mod ├── go.sum ├── internal ├── adapters │ └── http │ │ ├── controllers │ │ └── users.controller.go │ │ ├── middlewares │ │ └── auth.middleware.go │ │ └── routes │ │ └── users.route.go ├── infrastructure │ ├── connections │ │ ├── redis.connection.go │ │ └── sql.connection.go │ ├── providers │ │ └── .gitkeep │ └── templates │ │ └── .gitkeep └── modules │ └── users.module.go ├── shared ├── constants │ ├── cert.constant.go │ ├── common.constant.go │ └── logrus.constant.go ├── dto │ ├── common.dto.go │ ├── config.env.dto.go │ ├── helper.api.dto.go │ ├── pkg.graceful.response.go │ ├── pkg.jose.dto.go │ ├── pkg.jwt.dto.go │ └── su.users.dto.go ├── helpers │ ├── api.helper.go │ ├── cert.helper.go │ ├── cipher.helper.go │ ├── crypto.helper.go │ ├── pagination.helper.go │ ├── parser.helper.go │ └── transform.helper.go ├── interfaces │ ├── common.interface.go │ ├── helper.cert.interface.go │ ├── helper.cipher.interface.go │ ├── helper.crypto.interface.go │ ├── helper.parser.interface.go │ ├── helper.transform.go │ ├── pkg.jose.interface.go │ ├── pkg.jwt.interface.go │ ├── pkg.redis.interface.go │ └── ruem.users.interface.go ├── output │ ├── config.env.output.go │ ├── helper.api.output.go │ ├── helper.pagination.output.go │ ├── pkg.jose.output.go │ └── pkg.jwt.output.go └── pkg │ ├── graceful.pkg.go │ ├── jose.pkg.go │ ├── jwt.pkg.go │ ├── logrus.pkg.go │ └── redis.pkg.go └── usecases └── users.usecase.go /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist/ 3 | node_modules/ 4 | build/ 5 | npm-debug.log 6 | yarn-error.log 7 | .DS_Store 8 | .env 9 | .env.* 10 | .idea/ 11 | .vscode/ 12 | *.swp 13 | *.swo 14 | out/ 15 | .next/ 16 | .nuxt/ 17 | .docusaurus/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | pnpm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | lerna-debug.log* 27 | 28 | # Tests 29 | /coverage 30 | /.nyc_output 31 | 32 | # IDEs and editors 33 | /.idea 34 | .project 35 | .classpath 36 | .c9/ 37 | *.launch 38 | .settings/ 39 | *.sublime-workspace 40 | 41 | # IDE - VSCode 42 | .vscode/* 43 | !.vscode/settings.json 44 | !.vscode/tasks.json 45 | !.vscode/launch.json 46 | !.vscode/extensions.json 47 | 48 | # dotenv environment variable files 49 | .env 50 | .env.local 51 | .env.development 52 | .env.test 53 | .env.production 54 | .env.example 55 | 56 | # temp directory 57 | .temp 58 | .tmp 59 | 60 | # Runtime data 61 | pids 62 | *.pid 63 | *.seed 64 | *.pid.lock 65 | *.pem 66 | *.csr 67 | *.key 68 | *.txt 69 | *.crt 70 | 71 | # Diagnostic reports (https://nodejs.org/api/report.html) 72 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 73 | 74 | ## Coverage ## 75 | coverage/ 76 | .nyc_output/ 77 | 78 | ## OS generated files ## 79 | Thumbs.db 80 | ehthumbs.db 81 | Desktop.ini 82 | ._* -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO = @go 2 | NPM = @npm 3 | NODEMON = @nodemon 4 | DOCKER = @docker 5 | COMPOSE = @docker-compose 6 | 7 | ################################# 8 | # Application Territory 9 | ################################# 10 | .PHONY: install 11 | install: 12 | ${GO} get . 13 | ${GO} mod verify 14 | ${NPM} i nodemon@latest -g 15 | 16 | .PHONY: dev 17 | dev: 18 | ${NODEMON} -V -e .go,.env -w . -x go run ./cmd/api --count=1 --race -V --signal SIGTERM 19 | 20 | .PHONY: build 21 | build: 22 | ${GO} mod tidy 23 | ${GO} mod verify 24 | ${GO} vet --race -v . 25 | ${GO} build --race -v -o ${type} . 26 | 27 | .PHONY: test 28 | test: 29 | ${GO} test -v ./domain/services/** 30 | 31 | ################################# 32 | # Docker Territory 33 | ################################# 34 | build: 35 | ${DOCKER} build -t go-api:latest --compress . 36 | 37 | up: 38 | ${COMPOSE} up -d --remove-orphans --no-deps --build 39 | 40 | down: 41 | ${COMPOSE} down -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Clean Architecture 2 | 3 | The following is a folder structure pattern that I usually use, although I don't use all of them because of the project I'm working on only small projects that are not too big, so if you are interested in the pattern I made, you can use it if you think it's good, check this link for new update for this architecture [here](https://github.com/restuwahyu13/go-trakteer-api). 4 | 5 | ## Table Of Content 6 | 7 | - [What Are The Benefits](#what-are-the-benefits-) 8 | - [Flow Diagram](#flow-diagram) 9 | - [Folder Structure Pattern](#folder-structure-pattern) 10 | 11 | ## What Are The Benefits ? 12 | 13 | - [x] Easy to maintance 14 | - [x] Easy to scalable you project 15 | - [x] Readable code 16 | - [x] Suitable for large projects or small projects 17 | - [x] Easy to understand for junior or senior 18 | - [x] And more 19 | 20 | ## Flow Diagram 21 | 22 | flow-diagram 23 | 24 | ## Folder Structure Pattern 25 | 26 | ``` 27 | / todo-list 28 | │ 29 | ├── /cmd 30 | │ ├── /api 31 | │ │ └── main.go 32 | │ ├── /grpc 33 | | │ └── main.go 34 | │ ├── /worker 35 | | │ └── main.go 36 | | 37 | | 38 | ├── /configs 39 | │ ├── env.config.go 40 | │ ├── test.config.go 41 | | 42 | | 43 | ├── /database 44 | │ ├── /migrations 45 | │ | └── 000123456789_create_users_table.up.sql 46 | │ ├── /seeds 47 | │ | └── 000123456789_create_users_table.seed.sql 48 | │ 49 | | 50 | ├── /domain 51 | │ ├── /entities 52 | | │ └── users.entitie.go 53 | │ ├── /exceptions 54 | | │ └── users.exception.go 55 | │ ├── /repositories 56 | | │ └── users.repositorie.go 57 | │ ├── /services 58 | | │ ├── /http 59 | | │ │ └── /users 60 | | │ │ └── service.go 61 | | │ │ └── service_test.go 62 | | │ │ └── mapper.go 63 | | │ ├── /grpc 64 | | │ │ └── /users 65 | | │ │ └── service.go 66 | | │ │ └── service_test.go 67 | | │ │ └── mapper.go 68 | | 69 | | 70 | ├── /internal 71 | │ ├── /adapters 72 | | │ ├── /http 73 | | │ │ └── /controllers 74 | | │ │ └── users.controller.go 75 | | │ │ └── /routes 76 | | │ │ └── users.route.go 77 | | │ │ └── /middlewares 78 | | │ │ └── auth.middleware.go 79 | | │ │ └── role.middleware.go 80 | | │ ├── /grpc 81 | | │ │ └── /schemas 82 | | │ │ └── users.pb.go 83 | | │ │ └── users_grpc.pb.go 84 | | | │ 85 | │ ├── /infrastructure 86 | | │ ├── /connections 87 | | │ │ └── database.connection.go 88 | | │ │ └── redis.connection.go 89 | | │ ├── /providers 90 | | │ │ └── email.provider.go 91 | | │ │ └── sms.provider.go 92 | | │ ├── /templates 93 | | │ │ └── email.template.go 94 | | │ │ └── sms.template.go 95 | | │ │ 96 | │ ├── /modules 97 | | │ ├── /http 98 | | │ │ └── users.module.go 99 | | │ ├── /grpc 100 | | │ │ └── users.module.go 101 | | 102 | | 103 | ├── /external 104 | │ ├── /deployments 105 | | │ ├── /docker 106 | | │ │ └── /golang 107 | | │ │ └── Dockerfile 108 | | │ │ └── /redis 109 | | │ │ └── Dockerfile 110 | | │ │ └── /postgres 111 | | │ │ └── Dockerfile 112 | | │ ├── /kubernetes 113 | | │ │ └── /deployment.yaml 114 | | │ │ └── /service.yaml 115 | | │ │ └── /ingress.yaml 116 | | │ │ └── /configmap.yaml 117 | | │ ├── /terraform 118 | | │ │ └── /main.tf 119 | | │ │ └── /variables.tf 120 | | │ │ └── /outputs.tf 121 | | │ │ └── /providers.tf 122 | | │ ├── /cicd 123 | | │ │ └── /github 124 | | │ │ └── /workflow 125 | | │ │ └── ci.yaml 126 | | │ │ 127 | │ ├── /scripts 128 | | │ │ └── /build.sh 129 | | │ │ └── /run.sh 130 | | │ │ └── /test.sh 131 | | │ │ └── /deploy.sh 132 | | │ │ └── /rollback.sh 133 | | │ │ 134 | │ ├── /documentations 135 | | │ ├── /readmes 136 | | │ │ └── /api.md 137 | | │ │ └── /grpc.md 138 | | │ │ └── /database.md 139 | | │ │ └── /infrastructure.md 140 | | │ ├── /swaggers 141 | | │ │ └── /api.swagger.json 142 | | │ │ └── /grpc.swagger.json 143 | | 144 | | 145 | ├── /shared 146 | │ ├── /constants 147 | | │ └── users.constant.go 148 | │ ├── /dto 149 | | │ └── users.dto.go 150 | │ ├── /output 151 | | │ └── users.output.go 152 | │ ├── /helpers 153 | | │ └── api.helper.go 154 | │ ├── /interfaces 155 | | │ └── users.interface.go 156 | │ ├── /pkg 157 | | │ └── jwt.pkg.go 158 | | 159 | | 160 | ├── /usecases 161 | │ ├── /http 162 | | │ └── users.usecase.go 163 | | ├── /grpc 164 | | │ └── users.usecase.go 165 | | 166 | ├── .gitignore 167 | ├── .env.local 168 | ├── .env.example 169 | ├── README.md 170 | ├── README.md 171 | ├── docker-compose.yml 172 | ├── .dockerignore 173 | ├── Makefile 174 | ├── go.sum 175 | └── go.mod 176 | ``` -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/zlib" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-chi/chi/middleware" 9 | "github.com/go-chi/chi/v5" 10 | "github.com/go-chi/cors" 11 | "github.com/jmoiron/sqlx" 12 | "github.com/oxequa/grace" 13 | "github.com/redis/go-redis/v9" 14 | config "github.com/restuwahyu13/go-clean-architecture/configs" 15 | con "github.com/restuwahyu13/go-clean-architecture/internal/infrastructure/connections" 16 | module "github.com/restuwahyu13/go-clean-architecture/internal/modules" 17 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 18 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 19 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 20 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 21 | "github.com/restuwahyu13/go-clean-architecture/shared/pkg" 22 | "github.com/unrolled/secure" 23 | ) 24 | 25 | type ( 26 | IApi interface { 27 | Middleware() 28 | Module() 29 | Listener() 30 | } 31 | 32 | Api struct { 33 | ENV dto.Environtment 34 | ROUTER *chi.Mux 35 | DB *sqlx.DB 36 | RDS *redis.Client 37 | } 38 | ) 39 | 40 | var ( 41 | err error 42 | env dto.Environtment 43 | ) 44 | 45 | func init() { 46 | transform := helper.NewTransform() 47 | 48 | env_res, err := config.NewEnvirontment(".env", ".", "env") 49 | if err != nil { 50 | pkg.Logrus("fatal", err) 51 | return 52 | } 53 | 54 | if env_res != nil { 55 | if err := transform.ResToReq(env_res, &env); err != nil { 56 | pkg.Logrus("fatal", err) 57 | } 58 | } 59 | } 60 | 61 | func main() { 62 | ctx := context.Background() 63 | router := chi.NewRouter() 64 | 65 | db, err := con.SqlConnection(ctx, env) 66 | if err != nil { 67 | pkg.Logrus("fatal", err) 68 | return 69 | } 70 | 71 | rds, err := con.RedisConnection(env) 72 | if err != nil { 73 | pkg.Logrus("fatal", err) 74 | return 75 | } 76 | 77 | app := NewApi(Api{ENV: env, ROUTER: router, DB: db, RDS: rds}) 78 | app.Middleware() 79 | app.Module() 80 | app.Listener() 81 | } 82 | 83 | func NewApi(options Api) IApi { 84 | return Api{ 85 | ENV: options.ENV, 86 | ROUTER: options.ROUTER, 87 | DB: options.DB, 88 | RDS: options.RDS, 89 | } 90 | } 91 | 92 | func (i Api) Middleware() { 93 | if i.ENV.APP.ENV != cons.PROD { 94 | i.ROUTER.Use(middleware.Logger) 95 | } 96 | 97 | i.ROUTER.Use(middleware.Recoverer) 98 | i.ROUTER.Use(middleware.RealIP) 99 | i.ROUTER.Use(middleware.NoCache) 100 | i.ROUTER.Use(middleware.GetHead) 101 | i.ROUTER.Use(middleware.Compress(zlib.BestCompression)) 102 | i.ROUTER.Use(middleware.AllowContentType("application/json")) 103 | i.ROUTER.Use(cors.Handler(cors.Options{ 104 | AllowedOrigins: []string{"*"}, 105 | AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, 106 | AllowedHeaders: []string{"Accept", "Content-Type", "Authorization"}, 107 | AllowCredentials: true, 108 | OptionsPassthrough: true, 109 | MaxAge: 900, 110 | })) 111 | i.ROUTER.Use(secure.New(secure.Options{ 112 | FrameDeny: true, 113 | ContentTypeNosniff: true, 114 | BrowserXssFilter: true, 115 | STSIncludeSubdomains: true, 116 | STSPreload: true, 117 | STSSeconds: 900, 118 | }).Handler) 119 | } 120 | 121 | func (i Api) Module() { 122 | module.NewUsersModule[inf.IUsersService](dto.ModuleOptions{ 123 | ENV: i.ENV, 124 | DB: i.DB, 125 | RDS: i.RDS, 126 | ROUTER: i.ROUTER, 127 | }) 128 | } 129 | 130 | func (i Api) Listener() { 131 | err := pkg.Graceful(func() *dto.GracefulConfig { 132 | return &dto.GracefulConfig{HANDLER: i.ROUTER, ENV: i.ENV} 133 | }) 134 | 135 | recover := grace.Recover(&err) 136 | recover.Stack() 137 | 138 | if err != nil { 139 | pkg.Logrus("fatal", err) 140 | os.Exit(1) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /configs/env.config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | genv "github.com/caarlos0/env" 7 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 8 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 9 | 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func NewEnvirontment(name, path, ext string) (*opt.Environtment, error) { 14 | cfg := dto.Config{} 15 | 16 | if _, ok := os.LookupEnv("GO_ENV"); !ok { 17 | viper.SetConfigName(name) 18 | viper.SetConfigType(ext) 19 | viper.AddConfigPath(path) 20 | viper.AutomaticEnv() 21 | 22 | if err := viper.ReadInConfig(); err != nil { 23 | return nil, err 24 | } 25 | 26 | if err := viper.Unmarshal(&cfg); err != nil { 27 | return nil, err 28 | } 29 | } else { 30 | if err := genv.Parse(&cfg); err != nil { 31 | return nil, err 32 | } 33 | } 34 | 35 | return &opt.Environtment{ 36 | APP: &opt.Application{ 37 | ENV: cfg.ENV, 38 | PORT: cfg.PORT, 39 | INBOUND_SIZE: cfg.INBOUND_SIZE, 40 | }, 41 | REDIS: &opt.Redis{ 42 | URL: cfg.CSN, 43 | }, 44 | POSTGRES: &opt.Postgres{ 45 | URL: cfg.DSN, 46 | }, 47 | JWT: &opt.Jwt{ 48 | SECRET: cfg.JWT_SECRET_KEY, 49 | EXPIRED: cfg.JWT_EXPIRED, 50 | }, 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /configs/test.config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmoiron/sqlx" 7 | "github.com/redis/go-redis/v9" 8 | con "github.com/restuwahyu13/go-clean-architecture/internal/infrastructure/connections" 9 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 10 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 11 | "github.com/restuwahyu13/go-clean-architecture/shared/pkg" 12 | ) 13 | 14 | type ( 15 | Test struct { 16 | CTX context.Context 17 | ENV dto.Environtment 18 | DB *sqlx.DB 19 | RDS *redis.Client 20 | } 21 | ) 22 | 23 | var ( 24 | err error 25 | env dto.Environtment 26 | ) 27 | 28 | func init() { 29 | transform := helper.NewTransform() 30 | 31 | env_res, err := NewEnvirontment(".env", ".", "env") 32 | if err != nil { 33 | pkg.Logrus("fatal", err) 34 | return 35 | } 36 | 37 | if env_res != nil { 38 | if err := transform.ResToReq(env_res, &env); err != nil { 39 | pkg.Logrus("fatal", err) 40 | } 41 | } 42 | } 43 | 44 | func NewTest() Test { 45 | ctx := context.Background() 46 | 47 | db, err := con.SqlConnection(ctx, env) 48 | if err != nil { 49 | pkg.Logrus("fatal", err) 50 | } 51 | 52 | rds, err := con.RedisConnection(env) 53 | if err != nil { 54 | pkg.Logrus("fatal", err) 55 | } 56 | 57 | return Test{ 58 | CTX: ctx, 59 | ENV: env, 60 | DB: db, 61 | RDS: rds, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist/ 3 | node_modules/ 4 | build/ 5 | npm-debug.log 6 | yarn-error.log 7 | .DS_Store 8 | .env 9 | .env.* 10 | .idea/ 11 | .vscode/ 12 | *.swp 13 | *.swo 14 | out/ 15 | .next/ 16 | .nuxt/ 17 | .docusaurus/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | pnpm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | lerna-debug.log* 27 | 28 | # Tests 29 | /coverage 30 | /.nyc_output 31 | 32 | # IDEs and editors 33 | /.idea 34 | .project 35 | .classpath 36 | .c9/ 37 | *.launch 38 | .settings/ 39 | *.sublime-workspace 40 | 41 | # IDE - VSCode 42 | .vscode/* 43 | !.vscode/settings.json 44 | !.vscode/tasks.json 45 | !.vscode/launch.json 46 | !.vscode/extensions.json 47 | 48 | # dotenv environment variable files 49 | .env 50 | .env.local 51 | .env.development 52 | .env.test 53 | .env.production 54 | .env.example 55 | 56 | # temp directory 57 | .temp 58 | .tmp 59 | 60 | # Runtime data 61 | pids 62 | *.pid 63 | *.seed 64 | *.pid.lock 65 | *.pem 66 | *.csr 67 | *.key 68 | *.txt 69 | *.crt 70 | 71 | # Diagnostic reports (https://nodejs.org/api/report.html) 72 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 73 | 74 | ## Coverage ## 75 | coverage/ 76 | .nyc_output/ 77 | 78 | ## OS generated files ## 79 | Thumbs.db 80 | ehthumbs.db 81 | Desktop.ini 82 | ._* -------------------------------------------------------------------------------- /database/.sequelizerc: -------------------------------------------------------------------------------- 1 | require('dotenv/config') 2 | const path = require('node:path') 3 | 4 | module.exports = { 5 | 'config': path.resolve(__dirname, 'sequelize.js'), 6 | 'migrations-path': JSON.parse(process.env.DB_MIGRATE || 'false') ? path.resolve(__dirname, 'dist/migrations/') : path.resolve(__dirname, 'migrations/'), 7 | 'seeders-path': JSON.parse(process.env.DB_MIGRATE || 'false') ? path.resolve(__dirname, 'dist/seeders/') : path.resolve(__dirname, 'seeders/') 8 | } 9 | -------------------------------------------------------------------------------- /database/Makefile: -------------------------------------------------------------------------------- 1 | 2 | DB_DIR := $(realpath ./) 3 | SQLC := $(realpath ${DB_DIR}/node_modules/.bin/sequelize-cli) 4 | BUILD := npm run build 5 | 6 | ################################## 7 | # Database Migration Territory 8 | ################################## 9 | sqlc: 10 | ${SQLC} -h 11 | 12 | mig-build: 13 | rm -r ${DB_DIR}/dist; ${BUILD} 14 | 15 | mig-create: 16 | ifdef f 17 | ${SQLC} migration:create --name ${f} 18 | endif 19 | 20 | mig-rollback: 21 | ifdef f 22 | ${SQLC} db:migrate:undo --name ${f} 23 | endif 24 | 25 | mig-status: 26 | ${SQLC} db:migrate:status --debug 27 | 28 | mig-upf: 29 | ifdef f 30 | rm -r ${DB_DIR}/dist; ${BUILD} 31 | ${SQLC} db:migrate --name ${DB_DIR}/dist/migrations/${f} 32 | endif 33 | 34 | mig-up: 35 | rm -r ${DB_DIR}/dist; ${BUILD} 36 | ${SQLC} db:migrate 37 | 38 | mig-down: # DANGER COMMAND, BE CAREFUL CAN DELETE ALL TABLES 39 | ${SQLC} db:migrate:undo:all 40 | 41 | seed-create: 42 | ifdef f 43 | ${SQLC} seed:create --name ${f} 44 | endif 45 | 46 | seed-upf: 47 | ifdef f 48 | rm -r ${DB_DIR}/dist; ${BUILD} 49 | ${SQLC} db:seed --seed ${DB_DIR}/dist/seeders/${f} 50 | endif 51 | 52 | seed-up: 53 | rm -r ${DB_DIR}/dist; ${BUILD} 54 | ${SQLC} db:seed:all 55 | 56 | seed-down: 57 | rm -r ${DB_DIR}/dist; ${BUILD} 58 | ${SQLC} db:seed:undo:all -------------------------------------------------------------------------------- /database/migrations/20250301081607-users.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, Sequelize, DataTypes } from 'sequelize' 2 | 3 | module.exports = { 4 | up: async (queryInterface: QueryInterface, sequelize: Sequelize) => { 5 | const tablExist: boolean = await queryInterface.tableExists('users') 6 | if (!tablExist) { 7 | await queryInterface.createTable( 8 | 'users', 9 | { 10 | id: { type: DataTypes.UUID, primaryKey: true, allowNull: false, unique: true, defaultValue: sequelize.literal('uuid_generate_v4()') }, 11 | name: { type: DataTypes.STRING(200), allowNull: false }, 12 | email: { type: DataTypes.STRING(50), allowNull: false }, 13 | status: { type: DataTypes.STRING(25), allowNull: false, defaultValue: 'active' }, 14 | password: { type: DataTypes.TEXT, allowNull: false }, 15 | created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: sequelize.literal('CURRENT_TIMESTAMP') }, 16 | updated_at: { type: DataTypes.DATE }, 17 | deleted_at: { type: DataTypes.DATE } 18 | }, 19 | { 20 | logging: true 21 | } 22 | ) 23 | } 24 | }, 25 | down: async (queryInterface: QueryInterface, _sequelize: Sequelize) => { 26 | const tableExist: boolean = await queryInterface.tableExists('users') 27 | if (tableExist) { 28 | return queryInterface.dropTable('users') 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "migration", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "Migration database", 6 | "scripts": { 7 | "ts-bin:prod": "tsc -P tsconfig.json", 8 | "cleanup": "rimraf dist", 9 | "build": "npm run cleanup && npm run ts-bin:prod" 10 | }, 11 | "author": { 12 | "name": "Restu Wahyu Saputra", 13 | "url": "https://github.com/restuwahyu13" 14 | }, 15 | "maintainers": [ 16 | "Restu Wahyu Saputra" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/restuwahyu13/go-shopping" 21 | }, 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">= 20.x.x", 25 | "npm": ">= 10.x.x" 26 | }, 27 | "devDependencies": { 28 | "bcrypt": "^6.0.0", 29 | "dotenv": "^16.5.0", 30 | "pg": "^8.13.3", 31 | "rimraf": "^6.0.1", 32 | "sequelize": "^6.37.5", 33 | "sequelize-cli": "^6.6.3", 34 | "tslib": "^2.8.1", 35 | "typescript": "^5.6.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/seeders/20250301081607-users.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, Sequelize } from 'sequelize' 2 | import bcrypt from 'bcrypt' 3 | 4 | module.exports = { 5 | up: async (queryInterface: QueryInterface, _sequelize: Sequelize) => { 6 | const hashPassword: string = bcrypt.hashSync('@Qwerty12', 12) 7 | const users: Record[] = [ 8 | { 9 | name: 'Mat Metal', 10 | email: 'matmetal13@gmail.com', 11 | password: hashPassword 12 | }, 13 | { 14 | name: 'Anto Killer', 15 | email: 'antokiller13@gmail.com', 16 | password: hashPassword 17 | }, 18 | { 19 | name: 'Jamal Cavalera', 20 | email: 'jamal13@gmail.com', 21 | password: hashPassword 22 | }, 23 | { 24 | name: 'Santoso', 25 | email: 'santoso13@gmail.com', 26 | password: hashPassword 27 | } 28 | ] 29 | 30 | return queryInterface.bulkInsert('users', users, { logging: true }) 31 | }, 32 | down: async (queryInterface: QueryInterface, _sequelize: Sequelize) => { 33 | return queryInterface.bulkDelete('users', { logging: true }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/sequelize.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | module.exports = { 4 | [process.env.NODE_ENV]: { 5 | dialect: process.env.DB_DIALECT, 6 | host: process.env.DB_HOST, 7 | port: +process.env.DB_PORT, 8 | username: process.env.DB_USERNAME, 9 | password: process.env.DB_PASSWORD, 10 | database: process.env.DB_NAME, 11 | dialectOptions: { 12 | ssl: JSON.parse(process.env.DB_SSL || 'false') 13 | ? { 14 | require: JSON.parse(process.env.DB_SSL || 'false'), 15 | rejectUnauthorized: JSON.parse(process.env.DB_SSL || 'false') 16 | } 17 | : JSON.parse(process.env.DB_SSL || 'false') 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"], 5 | "ts-node": { 6 | "compilerOptions": { 7 | "module": "CommonJS" 8 | }, 9 | "require": ["tsconfig-paths/register"], 10 | "transpiler": "ts-node/transpilers/swc", 11 | "compiler": "typescript", 12 | "compilerHost": true, 13 | "preferTsExts": true, 14 | "typeCheck": true, 15 | "logError": true, 16 | "files": true, 17 | "swc": true, 18 | } 19 | } -------------------------------------------------------------------------------- /database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "database", 4 | "outDir": "dist", 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "moduleDetection": "auto", 8 | "target": "ESNext", 9 | "typeRoots": ["node_modules/@types/"], 10 | "paths": { 11 | "~/*": ["*"], 12 | }, 13 | "strict": true, 14 | "alwaysStrict": true, 15 | "esModuleInterop": true, 16 | "importHelpers": true, 17 | "skipLibCheck": true, 18 | "downlevelIteration": true, 19 | "allowSyntheticDefaultImports": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noUnusedParameters": true, 22 | "noUnusedLocals": true, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true, 25 | "removeComments": true, 26 | "allowJs": true, 27 | "sourceMap": true, 28 | "useDefineForClassFields":false, 29 | "strictNullChecks": false, 30 | "noImplicitAny": false, 31 | "noImplicitReturns": false 32 | } 33 | } -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restuwahyu13/go-clean-architecture/39f723ce2ca13c20ae4236233e1358f1b82f0ef9/diagram.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ### =================================== 3 | ### API SERVICE TERITORY 4 | ### =================================== 5 | api: 6 | container_name: go-api 7 | restart: always 8 | build: 9 | context: . 10 | dockerfile: external/deployments/Dockerfile 11 | healthcheck: 12 | interval: 120ms 13 | start_period: 60ms 14 | timeout: 30ms 15 | retries: 3 16 | test: env | grep $HOME 17 | env_file: 18 | - .env 19 | depends_on: 20 | - db 21 | - cache 22 | ports: 23 | - 3000:3000 24 | networks: 25 | - go-network 26 | ### =================================== 27 | ### DATABASE SERVICE TERITORY 28 | ### =================================== 29 | db: 30 | image: postgres:14-alpine 31 | restart: always 32 | healthcheck: 33 | interval: 120ms 34 | start_period: 60ms 35 | timeout: 30ms 36 | retries: 3 37 | test: env | grep $HOME 38 | env_file: 39 | - .env 40 | ports: 41 | - 5432:5432 42 | networks: 43 | - go-network 44 | ### =================================== 45 | ### CACHING SERVICE TERITORY 46 | ### =================================== 47 | cache: 48 | image: redis:7-alpine 49 | restart: always 50 | healthcheck: 51 | interval: 120ms 52 | start_period: 60ms 53 | timeout: 30ms 54 | retries: 3 55 | test: env | grep $HOME 56 | env_file: 57 | - .env 58 | ports: 59 | - 6379:6379 60 | networks: 61 | - go-network 62 | ### =================================== 63 | ### NETWORKS SHARING GROUP TERITORY 64 | ### =================================== 65 | networks: 66 | go-network: -------------------------------------------------------------------------------- /domain/entities/users.entitie.go: -------------------------------------------------------------------------------- 1 | package entitie 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/guregu/null/v6/zero" 7 | ) 8 | 9 | type UsersEntitie struct { 10 | ID string `db:"id"` 11 | Name string `db:"name"` 12 | Email string `db:"email"` 13 | Status string `db:"status"` 14 | Password string `db:"password"` 15 | CreatedAt time.Time `db:"created_at"` 16 | UpdatedAt zero.Time `db:"updated_at"` 17 | DeletedAt zero.Time `db:"deleted_at"` 18 | } 19 | -------------------------------------------------------------------------------- /domain/exceptions/users.exception.go: -------------------------------------------------------------------------------- 1 | package exc 2 | 3 | import inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 4 | 5 | type usersException struct{} 6 | 7 | func NewUsersException() inf.IUsersException { 8 | return usersException{} 9 | } 10 | 11 | func (u usersException) Login(key string) string { 12 | err := make(map[string]string) 13 | 14 | err["user_not_found"] = "User is not exist in our system" 15 | err["invalid_password"] = "Invalid email or password" 16 | 17 | return err[key] 18 | } 19 | -------------------------------------------------------------------------------- /domain/repositories/users.repositorie.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmoiron/sqlx" 7 | entitie "github.com/restuwahyu13/go-clean-architecture/domain/entities" 8 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 9 | ) 10 | 11 | type usersRepositorie struct { 12 | ctx context.Context 13 | db *sqlx.DB 14 | entitie *entitie.UsersEntitie 15 | } 16 | 17 | func NewUsersRepositorie(ctx context.Context, db *sqlx.DB) inf.IUsersRepositorie { 18 | return &usersRepositorie{ctx: ctx, db: db, entitie: new(entitie.UsersEntitie)} 19 | } 20 | 21 | func (r usersRepositorie) Find(query string, args ...any) ([]entitie.UsersEntitie, error) { 22 | users := []entitie.UsersEntitie{} 23 | 24 | if args == nil { 25 | if err := r.db.SelectContext(r.ctx, &users, sqlx.Rebind(sqlx.DOLLAR, query)); err != nil { 26 | return nil, err 27 | } 28 | } else { 29 | if err := r.db.SelectContext(r.ctx, &users, sqlx.Rebind(sqlx.DOLLAR, query), args...); err != nil { 30 | return nil, err 31 | } 32 | } 33 | 34 | return users, nil 35 | } 36 | 37 | func (r usersRepositorie) FindOne(query string, args ...any) (*entitie.UsersEntitie, error) { 38 | if err := r.db.GetContext(r.ctx, r.entitie, sqlx.Rebind(sqlx.DOLLAR, query), args...); err != nil { 39 | return nil, err 40 | } 41 | 42 | return r.entitie, nil 43 | } 44 | 45 | func (r usersRepositorie) Create(dest any, query string, args ...any) error { 46 | if err := r.db.GetContext(r.ctx, dest, query, args...); err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (r usersRepositorie) Update(dest any, query string, args ...any) error { 54 | if err := r.db.GetContext(r.ctx, dest, query, args...); err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (r usersRepositorie) Delete(dest any, query string, args ...any) error { 62 | if err := r.db.GetContext(r.ctx, dest, query, args...); err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /domain/services/users/mapper.go: -------------------------------------------------------------------------------- 1 | package users_service 2 | 3 | import ( 4 | entitie "github.com/restuwahyu13/go-clean-architecture/domain/entities" 5 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 6 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 7 | ) 8 | 9 | func toLoginMapper(enttie *entitie.UsersEntitie) dto.Users { 10 | return dto.Users{ 11 | ID: enttie.ID, 12 | Name: enttie.Name, 13 | Status: enttie.Status, 14 | CreatedAt: enttie.CreatedAt.Format(cons.DATE_TIME_FORMAT), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /domain/services/users/service.go: -------------------------------------------------------------------------------- 1 | package users_service 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/jmoiron/sqlx" 8 | "github.com/redis/go-redis/v9" 9 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 10 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 11 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 12 | ) 13 | 14 | type usersService struct { 15 | env dto.Environtment 16 | db *sqlx.DB 17 | rds *redis.Client 18 | } 19 | 20 | func NewUsersService(options dto.ServiceOptions) inf.IUsersService { 21 | return &usersService{env: options.ENV, db: options.DB, rds: options.RDS} 22 | } 23 | 24 | func (s usersService) Ping(ctx context.Context) opt.Response { 25 | res := opt.Response{} 26 | 27 | res.StatCode = http.StatusOK 28 | res.Message = "Ping!" 29 | 30 | return res 31 | } 32 | -------------------------------------------------------------------------------- /domain/services/users/service_test.go: -------------------------------------------------------------------------------- 1 | package users_service 2 | 3 | import ( 4 | "net/http" 5 | 6 | "testing" 7 | 8 | config "github.com/restuwahyu13/go-clean-architecture/configs" 9 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestPing(r *testing.T) { 14 | con := config.NewTest() 15 | service := NewUsersService(dto.ServiceOptions{ENV: con.ENV, DB: con.DB, RDS: con.RDS}) 16 | 17 | r.Run("Test Ping", func(t *testing.T) { 18 | res := service.Ping(con.CTX) 19 | 20 | if !assert.Equal(t, int(res.StatCode), http.StatusOK) { 21 | t.FailNow() 22 | 23 | } else if !assert.Equal(t, res.Message, "Ping!") { 24 | t.FailNow() 25 | } 26 | 27 | t.Log(res.Message) 28 | }) 29 | 30 | r.Run("Database connected", func(t *testing.T) { 31 | if err := con.DB.Ping(); err != nil { 32 | t.FailNow() 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /external/deployments/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # ====================== 2 | # GO STAGE 3 | # ====================== 4 | FROM golang:latest AS builder 5 | ENV GO111MODULE=on \ 6 | CGO_ENABLED=0 \ 7 | GOOS=linux 8 | 9 | WORKDIR /app 10 | COPY go.mod go.sum ./ 11 | RUN go mod download 12 | 13 | COPY . . 14 | RUN go build -ldflags="-s -w" -o main ./cmd/api 15 | 16 | # ====================== 17 | # ALPINE STAGE 18 | # ====================== 19 | FROM alpine:latest 20 | WORKDIR /usr/src/app 21 | 22 | COPY --from=builder /app/main . 23 | 24 | EXPOSE 3000 25 | ENTRYPOINT ["./main"] -------------------------------------------------------------------------------- /external/deployments/kubernetes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restuwahyu13/go-clean-architecture/39f723ce2ca13c20ae4236233e1358f1b82f0ef9/external/deployments/kubernetes/.gitkeep -------------------------------------------------------------------------------- /external/scripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restuwahyu13/go-clean-architecture/39f723ce2ca13c20ae4236233e1358f1b82f0ef9/external/scripts/.gitkeep -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/restuwahyu13/go-clean-architecture 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/goccy/go-json v0.10.5 7 | github.com/google/uuid v1.6.0 8 | github.com/sirupsen/logrus v1.9.3 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/gorilla/schema v1.4.1 // indirect 14 | github.com/huandu/go-sqlbuilder v1.35.0 // indirect 15 | github.com/huandu/xstrings v1.4.0 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/stretchr/testify v1.10.0 // indirect 18 | ) 19 | 20 | require ( 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 24 | github.com/fsnotify/fsnotify v1.8.0 // indirect 25 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 26 | github.com/go-playground/locales v0.14.1 // indirect 27 | github.com/go-playground/universal-translator v0.18.1 // indirect 28 | github.com/go-playground/validator/v10 v10.26.0 // indirect 29 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 30 | github.com/guregu/null/v6 v6.0.0 // indirect 31 | github.com/leodido/go-urn v1.4.0 // indirect 32 | github.com/lestrrat-go/blackmagic v1.0.3 // indirect 33 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 34 | github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 // indirect 35 | github.com/lestrrat-go/jwx/v3 v3.0.1 36 | github.com/lestrrat-go/option v1.0.1 // indirect 37 | github.com/lib/pq v1.10.9 // indirect 38 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 39 | github.com/pkg/errors v0.9.1 // indirect 40 | github.com/restuwahyu13/go-playground-converter v1.0.3 // indirect 41 | github.com/sagikazarmark/locafero v0.7.0 // indirect 42 | github.com/segmentio/asm v1.2.0 // indirect 43 | github.com/sourcegraph/conc v0.3.0 // indirect 44 | github.com/spf13/afero v1.12.0 // indirect 45 | github.com/spf13/cast v1.7.1 // indirect 46 | github.com/spf13/pflag v1.0.6 // indirect 47 | github.com/subosito/gotenv v1.6.0 // indirect 48 | go.uber.org/atomic v1.9.0 // indirect 49 | go.uber.org/multierr v1.9.0 // indirect 50 | golang.org/x/crypto v0.38.0 // indirect 51 | golang.org/x/net v0.40.0 // indirect 52 | golang.org/x/text v0.25.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | 56 | require ( 57 | github.com/caarlos0/env v3.5.0+incompatible 58 | github.com/go-chi/chi v1.5.5 59 | github.com/go-chi/chi/v5 v5.2.1 60 | github.com/go-chi/cors v1.2.1 61 | github.com/jmoiron/sqlx v1.4.0 62 | github.com/ory/graceful v0.1.3 63 | github.com/oxequa/grace v0.0.0-20180330101621-d1b62e904ab2 64 | github.com/redis/go-redis/v9 v9.8.0 65 | github.com/spf13/viper v1.20.1 66 | github.com/unrolled/secure v1.17.0 67 | golang.org/x/sys v0.33.0 // indirect 68 | ) 69 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 2 | github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= 3 | github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= 4 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 5 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 10 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 12 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 13 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 14 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 15 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 16 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 17 | github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= 18 | github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= 19 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 20 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 21 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 22 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 27 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 28 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 29 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 30 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 31 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 32 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 33 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 34 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 37 | github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= 38 | github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= 39 | github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs= 40 | github.com/huandu/go-sqlbuilder v1.35.0/go.mod h1:mS0GAtrtW+XL6nM2/gXHRJax2RwSW1TraavWDFAc1JA= 41 | github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 42 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 43 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 44 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 45 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 46 | github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= 47 | github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= 48 | github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 49 | github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 50 | github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY= 51 | github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE= 52 | github.com/lestrrat-go/jwx/v3 v3.0.1 h1:fH3T748FCMbXoF9UXXNS9i0q6PpYyJZK/rKSbkt2guY= 53 | github.com/lestrrat-go/jwx/v3 v3.0.1/go.mod h1:XP2WqxMOSzHSyf3pfibCcfsLqbomxakAnNqiuaH8nwo= 54 | github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 55 | github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 56 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 57 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 58 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 59 | github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc= 60 | github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA= 61 | github.com/oxequa/grace v0.0.0-20180330101621-d1b62e904ab2 h1:dzVjSCzXR7REzqxyB5OPgcXu3AR99isM/LdKsSL4WoM= 62 | github.com/oxequa/grace v0.0.0-20180330101621-d1b62e904ab2/go.mod h1:CWQBeDssj8lgsWGnWhGZUq3DAVD5GsTlvuHIxWhzthE= 63 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 64 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 65 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 66 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= 70 | github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 71 | github.com/restuwahyu13/go-playground-converter v1.0.3 h1:5X0TWv1XgJQX+xptBu5dn/kEtVH/HnLe4mCBiU5b4us= 72 | github.com/restuwahyu13/go-playground-converter v1.0.3/go.mod h1:pG4uAu79zfp0h4WeIgqrxY20/wVAckreKfAGe3d3t8E= 73 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 74 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 75 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 76 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 77 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 78 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 79 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 80 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 81 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 82 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 83 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 84 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 85 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 86 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 87 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 88 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 89 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 90 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 91 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 92 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 93 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 94 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 96 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 97 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 98 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 99 | github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= 100 | github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= 101 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 102 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 103 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 104 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 105 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 106 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 107 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 108 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 109 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 111 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 112 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 113 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 114 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 115 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 116 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 117 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 118 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | -------------------------------------------------------------------------------- /internal/adapters/http/controllers/users.controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | 6 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 7 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 8 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 9 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 10 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 11 | "github.com/restuwahyu13/go-clean-architecture/shared/pkg" 12 | ) 13 | 14 | type usersController struct { 15 | usecase inf.IUsersUsecase 16 | } 17 | 18 | func NewUsersController(options dto.ControllerOptions[inf.IUsersUsecase]) inf.IUsersController { 19 | return &usersController{usecase: options.USECASE} 20 | } 21 | 22 | func (c usersController) Ping(rw http.ResponseWriter, r *http.Request) { 23 | ctx := r.Context() 24 | res := opt.Response{} 25 | 26 | if res = c.usecase.Ping(ctx); res.StatCode >= http.StatusBadRequest { 27 | if res.StatCode >= http.StatusInternalServerError { 28 | pkg.Logrus(cons.ERROR, res.ErrMsg) 29 | res.ErrMsg = cons.DEFAULT_ERR_MSG 30 | } 31 | 32 | helper.Api(rw, r, res) 33 | return 34 | } 35 | 36 | helper.Api(rw, r, res) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /internal/adapters/http/middlewares/auth.middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "fmt" 7 | "math" 8 | "net/http" 9 | 10 | "strings" 11 | 12 | "github.com/lestrrat-go/jwx/v3/jwt" 13 | "github.com/redis/go-redis/v9" 14 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 15 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 16 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 17 | "github.com/restuwahyu13/go-clean-architecture/shared/pkg" 18 | ) 19 | 20 | func Auth(expired int, con *redis.Client) func(http.Handler) http.Handler { 21 | return func(h http.Handler) http.Handler { 22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | ctx := r.Context() 24 | res := opt.Response{} 25 | 26 | jose := pkg.NewJose(ctx) 27 | crypto := helper.NewCrypto() 28 | 29 | headers := r.Header.Get("Authorization") 30 | if !strings.Contains(headers, "Bearer") { 31 | res.StatCode = http.StatusUnauthorized 32 | res.ErrMsg = "Authorization is required" 33 | 34 | helper.Api(w, r, res) 35 | return 36 | } 37 | 38 | token := strings.Split(headers, "Bearer ")[1] 39 | 40 | if len(strings.Split(token, ".")) != 3 { 41 | res.StatCode = http.StatusUnauthorized 42 | res.ErrMsg = "Invalid token format" 43 | 44 | helper.Api(w, r, res) 45 | return 46 | } 47 | 48 | tokenMetadata, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithVerify(false)) 49 | if err != nil { 50 | pkg.Logrus(cons.ERROR, err) 51 | res.StatCode = http.StatusUnauthorized 52 | res.ErrMsg = "Invalid access token" 53 | 54 | helper.Api(w, r, res) 55 | return 56 | } 57 | 58 | aud, ok := tokenMetadata.Audience() 59 | if !ok { 60 | res.StatCode = http.StatusUnauthorized 61 | res.ErrMsg = "Invalid access token" 62 | 63 | helper.Api(w, r, res) 64 | return 65 | } 66 | 67 | iss, ok := tokenMetadata.Issuer() 68 | if !ok { 69 | res.StatCode = http.StatusUnauthorized 70 | res.ErrMsg = "Invalid access token" 71 | 72 | helper.Api(w, r, res) 73 | return 74 | } 75 | 76 | sub, ok := tokenMetadata.Subject() 77 | if !ok { 78 | res.StatCode = http.StatusUnauthorized 79 | res.ErrMsg = "Invalid access token" 80 | 81 | helper.Api(w, r, res) 82 | return 83 | } 84 | 85 | jti, ok := tokenMetadata.JwtID() 86 | if !ok { 87 | res.StatCode = http.StatusUnauthorized 88 | res.ErrMsg = "Invalid access token" 89 | 90 | helper.Api(w, r, res) 91 | return 92 | } 93 | 94 | timestamp := "" 95 | if err := tokenMetadata.Get("timestamp", ×tamp); err != nil { 96 | pkg.Logrus(cons.ERROR, err) 97 | res.StatCode = http.StatusUnauthorized 98 | res.ErrMsg = "Invalid access token" 99 | 100 | helper.Api(w, r, res) 101 | return 102 | } 103 | 104 | suffix := int(math.Pow(float64(expired), float64(len(aud[0])+len(iss)+len(sub)))) 105 | secretKey := fmt.Sprintf("%s:%s:%s:%s:%d", aud[0], iss, sub, timestamp, suffix) 106 | secretData := hex.EncodeToString([]byte(secretKey)) 107 | 108 | key, err := crypto.AES256Decrypt(secretData, jti) 109 | if err != nil { 110 | pkg.Logrus(cons.ERROR, err) 111 | res.StatCode = http.StatusUnauthorized 112 | res.ErrMsg = "Invalid access token" 113 | 114 | helper.Api(w, r, res) 115 | return 116 | } 117 | 118 | rds, err := pkg.NewRedis(ctx, con) 119 | if err != nil { 120 | pkg.Logrus(cons.ERROR, err) 121 | res.StatCode = http.StatusUnauthorized 122 | res.ErrMsg = "Invalid access token" 123 | 124 | helper.Api(w, r, res) 125 | return 126 | } 127 | 128 | if _, err = jose.JwtVerify(key, token, rds); err != nil { 129 | pkg.Logrus(cons.ERROR, err) 130 | res.StatCode = http.StatusUnauthorized 131 | res.ErrMsg = "Invalid access token" 132 | 133 | helper.Api(w, r, res) 134 | return 135 | } 136 | 137 | sharingCtx := context.WithValue(r.Context(), "user_id", key) 138 | h.ServeHTTP(w, r.WithContext(sharingCtx)) 139 | 140 | return 141 | }) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /internal/adapters/http/routes/users.route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 6 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 7 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 8 | ) 9 | 10 | type usersRoute struct { 11 | router chi.Router 12 | controller inf.IUsersController 13 | } 14 | 15 | func NewUsersRoute(options dto.RouteOptions[inf.IUsersController]) { 16 | route := usersRoute{router: options.ROUTER, controller: options.CONTROLLER} 17 | 18 | route.router.Route(helper.Version("users"), func(r chi.Router) { 19 | r.Get("/", route.controller.Ping) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /internal/infrastructure/connections/redis.connection.go: -------------------------------------------------------------------------------- 1 | package con 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/redis/go-redis/v9" 7 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 8 | ) 9 | 10 | func RedisConnection(env dto.Environtment) (*redis.Client, error) { 11 | parseURL, err := redis.ParseURL(env.REDIS.URL) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | return redis.NewClient(&redis.Options{ 17 | Addr: parseURL.Addr, 18 | MaxRetries: 10, 19 | PoolSize: 20, 20 | PoolFIFO: true, 21 | ReadTimeout: time.Duration(time.Second * 30), 22 | WriteTimeout: time.Duration(time.Second * 30), 23 | DialTimeout: time.Duration(time.Second * 60), 24 | MinRetryBackoff: time.Duration(time.Second * 60), 25 | MaxRetryBackoff: time.Duration(time.Second * 120), 26 | }), nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/infrastructure/connections/sql.connection.go: -------------------------------------------------------------------------------- 1 | package con 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmoiron/sqlx" 7 | _ "github.com/lib/pq" 8 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 9 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 10 | ) 11 | 12 | func SqlConnection(ctx context.Context, env dto.Environtment) (*sqlx.DB, error) { 13 | sqlx.BindDriver(cons.POSTGRES, sqlx.DOLLAR) 14 | 15 | return sqlx.ConnectContext(ctx, cons.POSTGRES, env.POSTGRES.URL) 16 | } 17 | -------------------------------------------------------------------------------- /internal/infrastructure/providers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restuwahyu13/go-clean-architecture/39f723ce2ca13c20ae4236233e1358f1b82f0ef9/internal/infrastructure/providers/.gitkeep -------------------------------------------------------------------------------- /internal/infrastructure/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restuwahyu13/go-clean-architecture/39f723ce2ca13c20ae4236233e1358f1b82f0ef9/internal/infrastructure/templates/.gitkeep -------------------------------------------------------------------------------- /internal/modules/users.module.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | users_service "github.com/restuwahyu13/go-clean-architecture/domain/services/users" 5 | controller "github.com/restuwahyu13/go-clean-architecture/internal/adapters/http/controllers" 6 | route "github.com/restuwahyu13/go-clean-architecture/internal/adapters/http/routes" 7 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 8 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 9 | usecase "github.com/restuwahyu13/go-clean-architecture/usecases" 10 | ) 11 | 12 | type usersModule[IService any] struct { 13 | service IService 14 | } 15 | 16 | func NewUsersModule[IService any](options dto.ModuleOptions) inf.IUsersModule[IService] { 17 | service := users_service.NewUsersService(dto.ServiceOptions{ENV: options.ENV, DB: options.DB, RDS: options.RDS}) 18 | 19 | usecase := usecase.NewUsersUsecase(dto.UsecaseOptions[inf.IUsersService]{SERVICE: service}) 20 | 21 | controller := controller.NewUsersController(dto.ControllerOptions[inf.IUsersUsecase]{USECASE: usecase}) 22 | 23 | route.NewUsersRoute(dto.RouteOptions[inf.IUsersController]{ROUTER: options.ROUTER, CONTROLLER: controller}) 24 | 25 | return usersModule[IService]{service: any(service).(IService)} 26 | } 27 | 28 | func (m usersModule[IService]) Service() IService { 29 | return m.service 30 | } 31 | -------------------------------------------------------------------------------- /shared/constants/cert.constant.go: -------------------------------------------------------------------------------- 1 | package cons 2 | 3 | const ( 4 | PRIVPKCS1 = "RSA PRIVATE KEY" 5 | PRIVPKCS8 = "PRIVATE KEY" 6 | 7 | PUBPKCS1 = "RSA PUBLIC KEY" 8 | PUBPKCS8 = "PUBLIC KEY" 9 | CERTIFICATE = "CERTIFICATE" 10 | ) 11 | -------------------------------------------------------------------------------- /shared/constants/common.constant.go: -------------------------------------------------------------------------------- 1 | package cons 2 | 3 | import "errors" 4 | 5 | var ( 6 | NO_ROWS_AFFECTED error = errors.New("sql: no rows affected") 7 | ) 8 | 9 | const ( 10 | DEV = "development" 11 | STAG = "staging" 12 | PROD = "production" 13 | TEST = "test" 14 | 15 | API = "/api/v1" 16 | 17 | EMPTY = "" 18 | Nil = iota 19 | InvalidUUID = "00000000-0000-0000-0000-000000000000" 20 | DEFAULT_ERR_MSG = "API is busy please try again later!" 21 | 22 | DATE_TIME_FORMAT = "2006-01-02 15:04:05" 23 | ) 24 | 25 | const ( 26 | POSTGRES = "postgres" 27 | ) 28 | -------------------------------------------------------------------------------- /shared/constants/logrus.constant.go: -------------------------------------------------------------------------------- 1 | package cons 2 | 3 | const ( 4 | INFO = "info" 5 | ERROR = "error" 6 | PRINT = "print" 7 | FATAL = "fatal" 8 | DEBUG = "debug" 9 | PANIC = "panic" 10 | ) 11 | -------------------------------------------------------------------------------- /shared/dto/common.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/jmoiron/sqlx" 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | type ( 10 | ServiceOptions struct { 11 | ENV Environtment 12 | DB *sqlx.DB 13 | RDS *redis.Client 14 | } 15 | 16 | UsecaseOptions[T any] struct { 17 | SERVICE T 18 | } 19 | 20 | ControllerOptions[T any] struct { 21 | USECASE T 22 | } 23 | 24 | RouteOptions[T any] struct { 25 | ENV Environtment 26 | RDS *redis.Client 27 | ROUTER chi.Router 28 | CONTROLLER T 29 | } 30 | 31 | ModuleOptions struct { 32 | ENV Environtment 33 | DB *sqlx.DB 34 | RDS *redis.Client 35 | ROUTER chi.Router 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /shared/dto/config.env.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 4 | 5 | type Config struct { 6 | ENV string `env:"GO_ENV" mapstructure:"GO_ENV"` 7 | PORT string `env:"PORT" mapstructure:"PORT"` 8 | INBOUND_SIZE int `env:"INBOUND_SIZE" mapstructure:"INBOUND_SIZE"` 9 | DSN string `env:"PG_DSN" mapstructure:"PG_DSN"` 10 | CSN string `env:"REDIS_CSN" mapstructure:"REDIS_CSN"` 11 | JWT_SECRET_KEY string `env:"JWT_SECRET_KEY" mapstructure:"JWT_SECRET_KEY"` 12 | JWT_EXPIRED int `env:"JWT_EXPIRED" mapstructure:"JWT_EXPIRED"` 13 | } 14 | 15 | type ( 16 | Environtment struct { 17 | APP *opt.Application 18 | REDIS *opt.Redis 19 | POSTGRES *opt.Postgres 20 | JWT *opt.Jwt 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /shared/dto/helper.api.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ( 4 | Request[T any] struct { 5 | Req T 6 | Body T 7 | Param T 8 | Query T 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /shared/dto/pkg.graceful.response.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | ) 6 | 7 | type GracefulConfig struct { 8 | HANDLER *chi.Mux 9 | ENV Environtment 10 | } 11 | -------------------------------------------------------------------------------- /shared/dto/pkg.jose.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/lestrrat-go/jwx/v3/jwk" 4 | 5 | type ( 6 | JweEncryptMetadata struct { 7 | CipherText string `json:"ciphertext"` 8 | EncryptedKey string `json:"encrypted_key"` 9 | Header map[string]any `json:"header"` 10 | IV string `json:"iv"` 11 | Protected string `json:"protected"` 12 | Tag string `json:"tag"` 13 | } 14 | 15 | JwkRawMetadata struct { 16 | D string `json:"d"` 17 | Dp string `json:"dp"` 18 | Dq string `json:"dq"` 19 | E string `json:"e"` 20 | Kty string `json:"kty"` 21 | N string `json:"n"` 22 | P string `json:"p"` 23 | Q string `json:"q"` 24 | Qi string `json:"qi"` 25 | } 26 | 27 | JwkMetadata struct { 28 | KeyRaw JwkRawMetadata 29 | Key jwk.Key 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /shared/dto/pkg.jwt.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "crypto/rsa" 5 | "time" 6 | 7 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 8 | ) 9 | 10 | type ( 11 | JwtSignOption struct { 12 | PrivateKey *rsa.PrivateKey 13 | Claim interface{} 14 | Kid string 15 | SecretKey string 16 | Iss string 17 | Sub string 18 | Aud []string 19 | Exp time.Time 20 | Nbf float64 21 | Iat time.Time 22 | Jti string 23 | } 24 | 25 | SecretMetadata struct { 26 | PrivKeyRaw string `json:"privKeyRaw"` 27 | CipherKey string `json:"cipherKey"` 28 | } 29 | 30 | SignatureMetadata struct { 31 | PrivKey *rsa.PrivateKey `json:"privKey"` 32 | PrivKeyRaw string `json:"privKeyRaw"` 33 | SigKey string `json:"sigKey"` 34 | CipherKey string `json:"cipherKey"` 35 | JweKey opt.JweEncryptMetadata `json:"jweKey"` 36 | } 37 | 38 | SignMetadata struct { 39 | Token string `json:"token"` 40 | Expired int `json:"expired"` 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /shared/dto/su.users.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | /** 4 | * =================================================== 5 | * USERS REQUEST TERITORY 6 | * =================================================== 7 | */ 8 | 9 | type ( 10 | LoginDTO struct { 11 | Email string `json:"email" db:"email" validator:"required,email"` 12 | Password string `json:"password" validator:"required"` 13 | } 14 | ) 15 | 16 | /** 17 | * =================================================== 18 | * USERS RESPONSE TERITORY 19 | * =================================================== 20 | */ 21 | 22 | type ( 23 | Login struct { 24 | Role string `json:"role"` 25 | Token string `json:"token"` 26 | Expired int `json:"expired"` 27 | } 28 | 29 | Users struct { 30 | ID string `json:"id,omitempty"` 31 | Name string `json:"name,omitempty"` 32 | Email string `json:"email,omitempty"` 33 | Status string `json:"status,omitempty"` 34 | Password string `json:"password,omitempty"` 35 | CreatedAt string `json:"created_at,omitempty"` 36 | UpdatedAt string `json:"updated_at,omitempty"` 37 | DeletedAt string `json:"deleted_at,omitempty"` 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /shared/helpers/api.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "reflect" 7 | "time" 8 | 9 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 10 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 11 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 12 | ) 13 | 14 | type responseTimer struct { 15 | startTime time.Time 16 | http.ResponseWriter 17 | } 18 | 19 | var errorCodeMapping = map[int]string{ 20 | http.StatusBadGateway: "SERVICE_ERROR", 21 | http.StatusServiceUnavailable: "SERVICE_UNAVAILABLE", 22 | http.StatusGatewayTimeout: "SERVICE_TIMEOUT", 23 | http.StatusConflict: "DUPLICATE_RESOURCE", 24 | http.StatusBadRequest: "INVALID_REQUEST", 25 | http.StatusUnprocessableEntity: "INVALID_REQUEST", 26 | http.StatusPreconditionFailed: "REQUEST_COULD_NOT_BE_PROCESSED", 27 | http.StatusForbidden: "ACCESS_DENIED", 28 | http.StatusUnauthorized: "UNAUTHORIZED_TOKEN", 29 | http.StatusNotFound: "UNKNOWN_RESOURCE", 30 | http.StatusInternalServerError: "GENERAL_ERROR", 31 | } 32 | 33 | func Version(path string) string { 34 | return fmt.Sprintf("%s/%s", cons.API, path) 35 | } 36 | 37 | func Api(rw http.ResponseWriter, r *http.Request, options opt.Response) { 38 | rt := &responseTimer{startTime: time.Now(), ResponseWriter: rw} 39 | 40 | response := buildResponse(options, r, rt) 41 | writeResponse(rt, NewParser(), response) 42 | } 43 | 44 | func (rt *responseTimer) WriteHeader(code int) { 45 | rt.ResponseWriter.WriteHeader(code) 46 | } 47 | 48 | func getErrorCode(statusCode int) string { 49 | if code, exists := errorCodeMapping[statusCode]; exists { 50 | return code 51 | } 52 | 53 | return errorCodeMapping[http.StatusInternalServerError] 54 | } 55 | 56 | func isEmptyResponse(resp opt.Response) bool { 57 | return reflect.DeepEqual(resp, opt.Response{}) 58 | } 59 | 60 | func getProtocol(r *http.Request) string { 61 | if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { 62 | return "https" 63 | } 64 | return "http" 65 | } 66 | 67 | func getIPAddress(r *http.Request) string { 68 | ip := r.Header.Get("X-Forwarded-For") 69 | 70 | if ip == "" { 71 | ip = r.Header.Get("X-Real-IP") 72 | } 73 | 74 | if ip == "" { 75 | ip = r.RemoteAddr 76 | } 77 | 78 | return ip 79 | } 80 | 81 | func buildResponse(options opt.Response, r *http.Request, rt *responseTimer) opt.Response { 82 | response := opt.Response{ 83 | StatCode: http.StatusInternalServerError, 84 | ErrMsg: cons.DEFAULT_ERR_MSG, 85 | } 86 | 87 | if isEmptyResponse(options) { 88 | defaultErrCode := getErrorCode(http.StatusInternalServerError) 89 | response.ErrCode = &defaultErrCode 90 | 91 | return response 92 | } 93 | 94 | response = copyResponseFields(options, response) 95 | setResponseDefaults(&response) 96 | 97 | response.Info = opt.Info{ 98 | Host: r.Host, 99 | Protocol: getProtocol(r), 100 | Path: r.URL.Path, 101 | Method: r.Method, 102 | Timestamp: time.Now().Format(time.RFC3339), 103 | ResponseTime: fmt.Sprintf("%d ms", time.Since(rt.startTime).Milliseconds()), 104 | UserAgent: r.UserAgent(), 105 | IPAddress: getIPAddress(r), 106 | } 107 | 108 | return response 109 | } 110 | 111 | func copyResponseFields(source, target opt.Response) opt.Response { 112 | if source.StatCode != 0 { 113 | target.StatCode = source.StatCode 114 | } 115 | 116 | if source.Message != nil { 117 | target.Message = source.Message 118 | } 119 | 120 | if source.ErrCode != nil { 121 | target.ErrCode = source.ErrCode 122 | } 123 | 124 | if source.ErrMsg != "" { 125 | target.ErrMsg = source.ErrMsg 126 | } 127 | 128 | if source.Data != nil { 129 | target.Data = source.Data 130 | } 131 | 132 | if source.Errors != nil { 133 | target.Errors = source.Errors 134 | } 135 | 136 | if source.Pagination != nil { 137 | target.Pagination = source.Pagination 138 | } 139 | 140 | target = opt.Response{ 141 | StatCode: target.StatCode, 142 | Message: target.Message, 143 | ErrCode: target.ErrCode, 144 | ErrMsg: target.ErrMsg, 145 | Data: target.Data, 146 | Errors: target.Errors, 147 | Pagination: target.Pagination, 148 | } 149 | 150 | return target 151 | } 152 | 153 | func setResponseDefaults(response *opt.Response) { 154 | if response.StatCode == 0 { 155 | response.StatCode = http.StatusInternalServerError 156 | } 157 | 158 | if response.StatCode >= http.StatusBadRequest && response.ErrCode == nil { 159 | defaultErrCode := getErrorCode(int(response.StatCode)) 160 | response.ErrCode = &defaultErrCode 161 | } 162 | 163 | if response.StatCode >= http.StatusInternalServerError && response.ErrMsg == cons.DEFAULT_ERR_MSG { 164 | response.ErrMsg = cons.DEFAULT_ERR_MSG 165 | } 166 | } 167 | 168 | func writeResponse(rw http.ResponseWriter, parser inf.IParser, response opt.Response) { 169 | rw.Header().Set("Content-Type", "application/json") 170 | 171 | statusCode := response.StatCode 172 | if statusCode == 0 { 173 | statusCode = http.StatusInternalServerError 174 | } 175 | rw.WriteHeader(int(statusCode)) 176 | 177 | if err := parser.Encode(rw, response); err != nil { 178 | rw.WriteHeader(http.StatusInternalServerError) 179 | 180 | errorResponse := fmt.Sprintf(`{"stat_code":%d, "err_code":"%s", "err_msg":"%s"}`, http.StatusInternalServerError, errorCodeMapping[http.StatusInternalServerError], cons.DEFAULT_ERR_MSG) 181 | fmt.Fprint(rw, errorResponse) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /shared/helpers/cert.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "errors" 9 | 10 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 11 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 12 | ) 13 | 14 | type cert struct{} 15 | 16 | func NewCert() inf.ICert { 17 | return cert{} 18 | } 19 | 20 | func (h cert) GeneratePrivateKey(password []byte) (string, error) { 21 | var pemBlock *pem.Block = new(pem.Block) 22 | 23 | rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | privateKeyTransform := h.PrivateKeyToRaw(rsaPrivateKey) 29 | 30 | if password != nil { 31 | encryptPemBlock, err := x509.EncryptPEMBlock(rand.Reader, "RSA PRIVATE KEY", []byte(privateKeyTransform), []byte(password), x509.PEMCipherAES256) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | pemBlock = encryptPemBlock 37 | } else { 38 | decodePemBlock, _ := pem.Decode([]byte(privateKeyTransform)) 39 | if pemBlock == nil { 40 | return "", errors.New("Invalid PrivateKey") 41 | } 42 | 43 | pemBlock = decodePemBlock 44 | } 45 | 46 | return string(pem.EncodeToMemory(pemBlock)), nil 47 | } 48 | 49 | func (h cert) PrivateKeyRawToKey(privateKey []byte, password []byte) (*rsa.PrivateKey, error) { 50 | decodePrivateKey, _ := pem.Decode(privateKey) 51 | if decodePrivateKey == nil { 52 | return nil, errors.New("Invalid PrivateKey") 53 | } 54 | 55 | if x509.IsEncryptedPEMBlock(decodePrivateKey) { 56 | decryptPrivateKey, err := x509.DecryptPEMBlock(decodePrivateKey, password) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | decodePrivateKey, _ = pem.Decode(decryptPrivateKey) 62 | if decodePrivateKey == nil { 63 | return nil, errors.New("Invalid PrivateKey") 64 | } 65 | } 66 | 67 | rsaPrivKey, err := x509.ParsePKCS1PrivateKey(decodePrivateKey.Bytes) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return rsaPrivKey, nil 73 | } 74 | 75 | func (h cert) PrivateKeyToRaw(publicKey *rsa.PrivateKey) string { 76 | privateKeyTransform := pem.EncodeToMemory(&pem.Block{ 77 | Type: cons.PRIVPKCS1, 78 | Bytes: x509.MarshalPKCS1PrivateKey(publicKey), 79 | }) 80 | 81 | return string(privateKeyTransform) 82 | } 83 | -------------------------------------------------------------------------------- /shared/helpers/cipher.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "crypto/aes" 5 | cpr "crypto/cipher" 6 | "crypto/hmac" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "crypto/sha512" 10 | "encoding/hex" 11 | "errors" 12 | 13 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 14 | "golang.org/x/crypto/scrypt" 15 | ) 16 | 17 | type cipher struct{} 18 | 19 | func NewCipher() inf.ICipher { 20 | return cipher{} 21 | } 22 | 23 | func (h cipher) AES256Encrypt(secretKey, plainText string) (string, error) { 24 | secretKeyByte := make([]byte, len(secretKey)) 25 | secretKeyByte = []byte(secretKey) 26 | 27 | plainTextByte := make([]byte, len(plainText)) 28 | plainTextByte = []byte(plainText) 29 | 30 | tagSize := 16 31 | 32 | if len(secretKeyByte) < 32 { 33 | return "", errors.New("Secretkey length mismatch") 34 | } 35 | 36 | key, err := scrypt.Key([]byte(secretKey), []byte("salt"), 1024, 8, 1, 32) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | block, err := aes.NewCipher(key) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | gcm, err := cpr.NewGCMWithTagSize(block, tagSize) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | nonceSize := make([]byte, gcm.NonceSize()) 52 | if _, err = rand.Read(nonceSize); err != nil { 53 | return "", err 54 | } 55 | 56 | cipherText := gcm.Seal(nonceSize, nonceSize, []byte(plainTextByte), nil) 57 | 58 | return hex.EncodeToString(cipherText), nil 59 | } 60 | 61 | func (h cipher) AES256Decrypt(secretKey string, cipherText string) (string, error) { 62 | secretKeyByte := make([]byte, len(secretKey)) 63 | secretKeyByte = []byte(secretKey) 64 | tagSize := 16 65 | 66 | if len(secretKeyByte) < 32 { 67 | return "", errors.New("Secretkey length mismatch") 68 | } 69 | 70 | key, err := scrypt.Key(secretKeyByte, []byte("salt"), 1024, 8, 1, 32) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | cipherTextByte, err := hex.DecodeString(cipherText) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | block, err := aes.NewCipher(key) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | gcm, err := cpr.NewGCMWithTagSize(block, tagSize) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | nonceSize := make([]byte, gcm.NonceSize()) 91 | if _, err = rand.Read(nonceSize); err != nil { 92 | return "", err 93 | } else if len(cipherTextByte) < len(nonceSize) { 94 | return "", errors.New("Cipher text to short") 95 | } 96 | 97 | nonce, ciphertext := cipherTextByte[:len(nonceSize)], cipherTextByte[len(nonceSize):] 98 | plaintext, err := gcm.Open(nil, []byte(nonce), []byte(ciphertext), nil) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | return string(plaintext), nil 104 | } 105 | 106 | func (h cipher) HMACSHA512Sign(secretKey, data string) (string, error) { 107 | hashHMAC512 := hmac.New(sha512.New, []byte(secretKey)) 108 | 109 | if _, err := hashHMAC512.Write([]byte(data)); err != nil { 110 | return "", err 111 | } 112 | 113 | return hex.EncodeToString(hashHMAC512.Sum(nil)), nil 114 | } 115 | 116 | func (h cipher) HMACSHA512Verify(secretKey, data, hash string) bool { 117 | hashHMAC512 := hmac.New(sha512.New, []byte(secretKey)) 118 | 119 | if _, err := hashHMAC512.Write([]byte(data)); err != nil { 120 | return false 121 | } 122 | 123 | return hmac.Equal([]byte(hash), hashHMAC512.Sum(nil)) 124 | } 125 | 126 | func (h cipher) SHA256Sign(plainText string) (string, error) { 127 | hashSHA256 := sha256.New() 128 | 129 | if _, err := hashSHA256.Write([]byte(plainText)); err != nil { 130 | return "", err 131 | } 132 | return hex.EncodeToString(hashSHA256.Sum(nil)), nil 133 | } 134 | 135 | func (h cipher) SHA512Sign(plainText string) (string, error) { 136 | hashSHA512 := sha512.New() 137 | 138 | if _, err := hashSHA512.Write([]byte(plainText)); err != nil { 139 | return "", err 140 | } 141 | return hex.EncodeToString(hashSHA512.Sum(nil)), nil 142 | } 143 | -------------------------------------------------------------------------------- /shared/helpers/crypto.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "crypto/aes" 5 | cpr "crypto/cipher" 6 | "crypto/hmac" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "crypto/sha512" 10 | "encoding/hex" 11 | "errors" 12 | 13 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 14 | "golang.org/x/crypto/scrypt" 15 | ) 16 | 17 | type crypto struct{} 18 | 19 | func NewCrypto() inf.ICrypto { 20 | return crypto{} 21 | } 22 | 23 | func (h crypto) AES256Encrypt(secretKey, plainText string) (string, error) { 24 | secretKeyByte := make([]byte, len(secretKey)) 25 | secretKeyByte = []byte(secretKey) 26 | 27 | plainTextByte := make([]byte, len(plainText)) 28 | plainTextByte = []byte(plainText) 29 | 30 | tagSize := 16 31 | 32 | if len(secretKeyByte) < 32 { 33 | return "", errors.New("Secretkey length mismatch") 34 | } 35 | 36 | key, err := scrypt.Key([]byte(secretKey), []byte("salt"), 1024, 8, 1, 32) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | block, err := aes.NewCipher(key) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | gcm, err := cpr.NewGCMWithTagSize(block, tagSize) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | nonceSize := make([]byte, gcm.NonceSize()) 52 | if _, err = rand.Read(nonceSize); err != nil { 53 | return "", err 54 | } 55 | 56 | cipherText := gcm.Seal(nonceSize, nonceSize, []byte(plainTextByte), nil) 57 | 58 | return hex.EncodeToString(cipherText), nil 59 | } 60 | 61 | func (h crypto) AES256Decrypt(secretKey string, cipherText string) (string, error) { 62 | secretKeyByte := make([]byte, len(secretKey)) 63 | secretKeyByte = []byte(secretKey) 64 | tagSize := 16 65 | 66 | if len(secretKeyByte) < 32 { 67 | return "", errors.New("Secretkey length mismatch") 68 | } 69 | 70 | key, err := scrypt.Key(secretKeyByte, []byte("salt"), 1024, 8, 1, 32) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | cipherTextByte, err := hex.DecodeString(cipherText) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | block, err := aes.NewCipher(key) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | gcm, err := cpr.NewGCMWithTagSize(block, tagSize) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | nonceSize := make([]byte, gcm.NonceSize()) 91 | if _, err = rand.Read(nonceSize); err != nil { 92 | return "", err 93 | } else if len(cipherTextByte) < len(nonceSize) { 94 | return "", errors.New("Cipher text to short") 95 | } 96 | 97 | nonce, ciphertext := cipherTextByte[:len(nonceSize)], cipherTextByte[len(nonceSize):] 98 | plaintext, err := gcm.Open(nil, []byte(nonce), []byte(ciphertext), nil) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | return string(plaintext), nil 104 | } 105 | 106 | func (h crypto) HMACSHA512Sign(secretKey, data string) (string, error) { 107 | hashHMAC512 := hmac.New(sha512.New, []byte(secretKey)) 108 | 109 | if _, err := hashHMAC512.Write([]byte(data)); err != nil { 110 | return "", err 111 | } 112 | 113 | return hex.EncodeToString(hashHMAC512.Sum(nil)), nil 114 | } 115 | 116 | func (h crypto) HMACSHA512Verify(secretKey, data, hash string) bool { 117 | hashHMAC512 := hmac.New(sha512.New, []byte(secretKey)) 118 | 119 | if _, err := hashHMAC512.Write([]byte(data)); err != nil { 120 | return false 121 | } 122 | 123 | return hmac.Equal([]byte(hash), hashHMAC512.Sum(nil)) 124 | } 125 | 126 | func (h crypto) SHA256Sign(plainText string) (string, error) { 127 | hashSHA256 := sha256.New() 128 | 129 | if _, err := hashSHA256.Write([]byte(plainText)); err != nil { 130 | return "", err 131 | } 132 | return hex.EncodeToString(hashSHA256.Sum(nil)), nil 133 | } 134 | 135 | func (h crypto) SHA512Sign(plainText string) (string, error) { 136 | hashSHA512 := sha512.New() 137 | 138 | if _, err := hashSHA512.Write([]byte(plainText)); err != nil { 139 | return "", err 140 | } 141 | return hex.EncodeToString(hashSHA512.Sum(nil)), nil 142 | } 143 | -------------------------------------------------------------------------------- /shared/helpers/pagination.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "math" 5 | 6 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 7 | ) 8 | 9 | func Pagination(limit, offset, total int) *opt.Pagination { 10 | res := new(opt.Pagination) 11 | 12 | res.Limit = limit 13 | res.Page = offset 14 | res.TotalPage = math.Ceil(float64(total) / float64(limit)) 15 | res.TotalData = total 16 | 17 | return res 18 | } 19 | -------------------------------------------------------------------------------- /shared/helpers/parser.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/goccy/go-json" 12 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 13 | 14 | "github.com/google/uuid" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | type parser struct{} 19 | 20 | func NewParser() inf.IParser { 21 | return parser{} 22 | } 23 | 24 | func (h parser) ToString(v any) string { 25 | return strings.TrimSpace(fmt.Sprintf("%v", v)) 26 | } 27 | 28 | func (h parser) ToInt(v any) (int, error) { 29 | parse, err := strconv.Atoi(h.ToString(v)) 30 | if err != nil { 31 | return 0, nil 32 | } 33 | 34 | return parse, nil 35 | } 36 | 37 | func (h parser) ToFloat(v any) (float64, error) { 38 | parse, err := strconv.ParseFloat(h.ToString(v), 64) 39 | if err != nil { 40 | return 0, err 41 | } 42 | 43 | return parse, nil 44 | } 45 | 46 | func (h parser) ToByte(v any) ([]byte, error) { 47 | reader := strings.NewReader(h.ToString(v)) 48 | data := &bytes.Buffer{} 49 | 50 | if _, err := reader.WriteTo(data); err != nil { 51 | return nil, err 52 | } 53 | 54 | return data.Bytes(), nil 55 | } 56 | 57 | func (h parser) Marshal(src any) ([]byte, error) { 58 | return json.Marshal(src) 59 | } 60 | 61 | func (h parser) Unmarshal(src []byte, dest any) error { 62 | decoder := json.NewDecoder(bytes.NewReader(src)) 63 | 64 | for decoder.More() { 65 | if err := decoder.Decode(dest); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (h parser) Decode(src io.Reader, dest any) error { 74 | decoder := json.NewDecoder(src) 75 | 76 | for decoder.More() { 77 | if err := decoder.Decode(dest); err != nil { 78 | return err 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (h parser) Encode(src io.Writer, dest any) error { 86 | return json.NewEncoder(src).Encode(dest) 87 | } 88 | 89 | func (h parser) FromUUID(s string) uuid.UUID { 90 | fromId, err := uuid.FromBytes([]byte(s)) 91 | if err != nil { 92 | logrus.Errorf("FromUUID: %v", err) 93 | } 94 | 95 | return fromId 96 | } 97 | 98 | func (h parser) FromNullUUID(s string) uuid.NullUUID { 99 | fromId, err := uuid.FromBytes([]byte(s)) 100 | if err != nil { 101 | logrus.Errorf("FromNullUUID: %v", err) 102 | } 103 | return uuid.NullUUID{UUID: fromId, Valid: true} 104 | } 105 | 106 | func (h parser) DecimalToFloat(n int64) float64 { 107 | return float64(n) / 100 108 | } 109 | -------------------------------------------------------------------------------- /shared/helpers/transform.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 5 | ) 6 | 7 | type transform struct{} 8 | 9 | func NewTransform() inf.ITransform { 10 | return transform{} 11 | } 12 | 13 | func (h transform) ReqToRes(src, dest any) error { 14 | helper := NewParser() 15 | 16 | srcByte, err := helper.Marshal(src) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | if err = helper.Unmarshal(srcByte, dest); err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (h transform) ResToReq(src, dest any) error { 29 | helper := NewParser() 30 | 31 | srcByte, err := helper.Marshal(src) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if err = helper.Unmarshal(srcByte, dest); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /shared/interfaces/common.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | type ( 4 | IApi interface { 5 | Middleware() 6 | Router() 7 | Listener() 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /shared/interfaces/helper.cert.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | import ( 4 | "crypto/rsa" 5 | ) 6 | 7 | type ICert interface { 8 | GeneratePrivateKey(password []byte) (string, error) 9 | PrivateKeyRawToKey(privateKey []byte, password []byte) (*rsa.PrivateKey, error) 10 | PrivateKeyToRaw(publicKey *rsa.PrivateKey) string 11 | } 12 | -------------------------------------------------------------------------------- /shared/interfaces/helper.cipher.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | type ICipher interface { 4 | AES256Encrypt(secretKey, plainText string) (string, error) 5 | AES256Decrypt(secretKey string, cipherText string) (string, error) 6 | HMACSHA512Sign(secretKey, data string) (string, error) 7 | HMACSHA512Verify(secretKey, data, hash string) bool 8 | SHA256Sign(plainText string) (string, error) 9 | SHA512Sign(plainText string) (string, error) 10 | } 11 | -------------------------------------------------------------------------------- /shared/interfaces/helper.crypto.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | type ICrypto interface { 4 | AES256Encrypt(secretKey, plainText string) (string, error) 5 | AES256Decrypt(secretKey string, cipherText string) (string, error) 6 | HMACSHA512Sign(secretKey, data string) (string, error) 7 | HMACSHA512Verify(secretKey, data, hash string) bool 8 | SHA256Sign(plainText string) (string, error) 9 | SHA512Sign(plainText string) (string, error) 10 | } 11 | -------------------------------------------------------------------------------- /shared/interfaces/helper.parser.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type IParser interface { 10 | ToString(v any) string 11 | ToInt(v any) (int, error) 12 | ToFloat(v any) (float64, error) 13 | ToByte(v any) ([]byte, error) 14 | Marshal(source any) ([]byte, error) 15 | Unmarshal(src []byte, dest any) error 16 | Decode(src io.Reader, dest any) error 17 | Encode(src io.Writer, dest any) error 18 | FromUUID(s string) uuid.UUID 19 | FromNullUUID(s string) uuid.NullUUID 20 | DecimalToFloat(n int64) float64 21 | } 22 | -------------------------------------------------------------------------------- /shared/interfaces/helper.transform.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | type ITransform interface { 4 | ReqToRes(src, dest any) error 5 | ResToReq(src, dest any) error 6 | } 7 | -------------------------------------------------------------------------------- /shared/interfaces/pkg.jose.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | import ( 4 | "crypto/rsa" 5 | 6 | "github.com/lestrrat-go/jwx/v3/jwk" 7 | "github.com/lestrrat-go/jwx/v3/jwt" 8 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 9 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 10 | ) 11 | 12 | type IJose interface { 13 | JweEncrypt(publicKey *rsa.PublicKey, plainText string) ([]byte, *opt.JweEncryptMetadata, error) 14 | JweDecrypt(privateKey *rsa.PrivateKey, cipherText []byte) (string, error) 15 | ImportJsonWebKey(jwkKey jwk.Key) (*opt.JwkMetadata, error) 16 | ExportJsonWebKey(privateKey *rsa.PrivateKey) (*opt.JwkMetadata, error) 17 | JwtSign(options *dto.JwtSignOption) ([]byte, error) 18 | JwtVerify(prefix string, token string, redis IRedis) (*jwt.Token, error) 19 | } 20 | -------------------------------------------------------------------------------- /shared/interfaces/pkg.jwt.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | import ( 4 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 5 | ) 6 | 7 | type IJsonWebToken interface { 8 | Sign(prefix string, body any) (*opt.SignMetadata, error) 9 | } 10 | -------------------------------------------------------------------------------- /shared/interfaces/pkg.redis.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type IRedis interface { 8 | SetEx(key string, expiration time.Duration, value any) error 9 | Get(key string) ([]byte, error) 10 | Exists(key string) (int64, error) 11 | HSetEx(key string, expiration time.Duration, values ...any) error 12 | HGet(key string, field string) ([]byte, error) 13 | } 14 | -------------------------------------------------------------------------------- /shared/interfaces/ruem.users.interface.go: -------------------------------------------------------------------------------- 1 | package inf 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | entitie "github.com/restuwahyu13/go-clean-architecture/domain/entities" 8 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 9 | ) 10 | 11 | type ( 12 | IUsersRepositorie interface { 13 | Find(query string, args ...any) ([]entitie.UsersEntitie, error) 14 | FindOne(query string, args ...any) (*entitie.UsersEntitie, error) 15 | Create(dest any, query string, args ...any) error 16 | Update(dest any, query string, args ...any) error 17 | Delete(dest any, query string, args ...any) error 18 | } 19 | 20 | IUsersService interface { 21 | Ping(ctx context.Context) opt.Response 22 | } 23 | 24 | IUsersException interface { 25 | Login(key string) string 26 | } 27 | 28 | IUsersUsecase interface { 29 | Ping(ctx context.Context) opt.Response 30 | } 31 | 32 | IUsersController interface { 33 | Ping(rw http.ResponseWriter, r *http.Request) 34 | } 35 | 36 | IUsersModule[IUserService any] interface { 37 | Service() IUserService 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /shared/output/config.env.output.go: -------------------------------------------------------------------------------- 1 | package opt 2 | 3 | type ( 4 | Application struct { 5 | ENV string 6 | PORT string 7 | INBOUND_SIZE int 8 | } 9 | 10 | Redis struct { 11 | URL string 12 | } 13 | 14 | Postgres struct { 15 | URL string 16 | } 17 | 18 | Jwt struct { 19 | SECRET string 20 | EXPIRED int 21 | } 22 | 23 | Environtment struct { 24 | APP *Application 25 | REDIS *Redis 26 | POSTGRES *Postgres 27 | JWT *Jwt 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /shared/output/helper.api.output.go: -------------------------------------------------------------------------------- 1 | package opt 2 | 3 | type ( 4 | Error struct { 5 | Name string `json:"name,omitempty"` 6 | Message string `json:"message"` 7 | Code int `json:"code,omitempty"` 8 | Stack any `json:"stack,omitempty"` 9 | } 10 | 11 | Response struct { 12 | StatCode float64 `json:"stat_code"` 13 | Message any `json:"message,omitempty"` 14 | ErrCode any `json:"err_code,omitempty"` 15 | ErrMsg any `json:"err_msg,omitempty"` 16 | Pagination any `json:"pagination,omitempty"` 17 | Data any `json:"data,omitempty"` 18 | Errors any `json:"errors,omitempty"` 19 | Info Info `json:"info"` 20 | } 21 | 22 | Info struct { 23 | Host any `json:"host"` 24 | Path any `json:"path"` 25 | Method any `json:"method"` 26 | Protocol any `json:"protocol"` 27 | IPAddress any `json:"ip_address"` 28 | UserAgent any `json:"user_agent"` 29 | Timestamp any `json:"timestamp"` 30 | ResponseTime any `json:"response_time"` 31 | } 32 | ) 33 | -------------------------------------------------------------------------------- /shared/output/helper.pagination.output.go: -------------------------------------------------------------------------------- 1 | package opt 2 | 3 | type Pagination struct { 4 | Page int `json:"page"` 5 | Limit int `json:"per_page"` 6 | TotalPage float64 `json:"total_page"` 7 | TotalData int `json:"total_data"` 8 | } 9 | -------------------------------------------------------------------------------- /shared/output/pkg.jose.output.go: -------------------------------------------------------------------------------- 1 | package opt 2 | 3 | import "github.com/lestrrat-go/jwx/v3/jwk" 4 | 5 | type ( 6 | JweEncryptMetadata struct { 7 | CipherText string `json:"ciphertext"` 8 | EncryptedKey string `json:"encrypted_key"` 9 | Header map[string]any `json:"header"` 10 | IV string `json:"iv"` 11 | Protected string `json:"protected"` 12 | Tag string `json:"tag"` 13 | } 14 | 15 | JwkRawMetadata struct { 16 | D string `json:"d"` 17 | Dp string `json:"dp"` 18 | Dq string `json:"dq"` 19 | E string `json:"e"` 20 | Kty string `json:"kty"` 21 | N string `json:"n"` 22 | P string `json:"p"` 23 | Q string `json:"q"` 24 | Qi string `json:"qi"` 25 | } 26 | 27 | JwkMetadata struct { 28 | KeyRaw JwkRawMetadata 29 | Key jwk.Key 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /shared/output/pkg.jwt.output.go: -------------------------------------------------------------------------------- 1 | package opt 2 | 3 | import ( 4 | "crypto/rsa" 5 | ) 6 | 7 | type ( 8 | SecretMetadata struct { 9 | PrivKeyRaw string `json:"privKeyRaw"` 10 | CipherKey string `json:"cipherKey"` 11 | } 12 | 13 | SignatureMetadata struct { 14 | PrivKey *rsa.PrivateKey `json:"privKey"` 15 | PrivKeyRaw string `json:"privKeyRaw"` 16 | SigKey string `json:"sigKey"` 17 | CipherKey string `json:"cipherKey"` 18 | JweKey JweEncryptMetadata `json:"jweKey"` 19 | } 20 | 21 | SignMetadata struct { 22 | Token string `json:"token"` 23 | Expired int `json:"expired"` 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /shared/pkg/graceful.pkg.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/tls" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/ory/graceful" 10 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 11 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 12 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 13 | ) 14 | 15 | func Graceful(Handler func() *dto.GracefulConfig) error { 16 | parser := helper.NewParser() 17 | inboundSize, _ := parser.ToInt(os.Getenv("INBOUND_SIZE")) 18 | 19 | h := Handler() 20 | secure := true 21 | 22 | if _, ok := os.LookupEnv("GO_ENV"); ok && os.Getenv("GO_ENV") != "development" { 23 | secure = false 24 | } 25 | 26 | server := http.Server{ 27 | Handler: h.HANDLER, 28 | Addr: ":" + h.ENV.APP.PORT, 29 | MaxHeaderBytes: inboundSize, 30 | TLSConfig: &tls.Config{ 31 | Rand: rand.Reader, 32 | InsecureSkipVerify: secure, 33 | }, 34 | } 35 | 36 | Logrus(cons.INFO, "Server listening on port %s", h.ENV.APP.PORT) 37 | return graceful.Graceful(server.ListenAndServe, server.Shutdown) 38 | } 39 | -------------------------------------------------------------------------------- /shared/pkg/jose.pkg.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "errors" 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/lestrrat-go/jwx/v3/jwa" 11 | "github.com/lestrrat-go/jwx/v3/jwe" 12 | "github.com/lestrrat-go/jwx/v3/jwk" 13 | "github.com/lestrrat-go/jwx/v3/jws" 14 | "github.com/lestrrat-go/jwx/v3/jwt" 15 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 16 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 17 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 18 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 19 | ) 20 | 21 | type jose struct { 22 | ctx context.Context 23 | cert inf.ICert 24 | parser inf.IParser 25 | transform inf.ITransform 26 | } 27 | 28 | func NewJose(ctx context.Context) inf.IJose { 29 | jwk.Configure(jwk.WithStrictKeyUsage(true)) 30 | 31 | cert := helper.NewCert() 32 | parser := helper.NewParser() 33 | transform := helper.NewTransform() 34 | 35 | return &jose{ctx: ctx, cert: cert, parser: parser, transform: transform} 36 | } 37 | 38 | func (p jose) JweEncrypt(publicKey *rsa.PublicKey, plainText string) ([]byte, *opt.JweEncryptMetadata, error) { 39 | jweEncryptMetadataReq := new(dto.JweEncryptMetadata) 40 | jweEncryptMetadataRes := new(opt.JweEncryptMetadata) 41 | 42 | headers := jwe.NewHeaders() 43 | headers.Set("sig", plainText) 44 | headers.Set("alg", jwa.RSA_OAEP_512().String()) 45 | headers.Set("enc", jwa.A256GCM().String()) 46 | 47 | cipherText, err := jwe.Encrypt([]byte(plainText), jwe.WithKey(jwa.RSA_OAEP_512(), publicKey), jwe.WithContentEncryption(jwa.A256GCM()), jwe.WithCompact(), jwe.WithJSON(), jwe.WithProtectedHeaders(headers)) 48 | if err != nil { 49 | return nil, nil, err 50 | } 51 | 52 | if err := p.parser.Unmarshal(cipherText, jweEncryptMetadataReq); err != nil { 53 | return nil, nil, err 54 | } 55 | 56 | if err := p.transform.ReqToRes(jweEncryptMetadataReq, jweEncryptMetadataRes); err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | return cipherText, jweEncryptMetadataRes, nil 61 | } 62 | 63 | func (p jose) JweDecrypt(privateKey *rsa.PrivateKey, cipherText []byte) (string, error) { 64 | jwtKey, err := jwk.Import(privateKey) 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | jwkSet := jwk.NewSet() 70 | if err := jwkSet.AddKey(jwtKey); err != nil { 71 | return "", err 72 | } 73 | 74 | plainText, err := jwe.Decrypt(cipherText, jwe.WithKey(jwa.RSA_OAEP_512(), jwtKey), jwe.WithKeySet(jwkSet, jwe.WithRequireKid(false))) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | return string(plainText), nil 80 | } 81 | 82 | func (p jose) ImportJsonWebKey(jwkKey jwk.Key) (*opt.JwkMetadata, error) { 83 | jwkRawMetadataReq := dto.JwkMetadata{} 84 | jwkRawMetadataRes := opt.JwkMetadata{} 85 | 86 | if _, err := jwk.IsPrivateKey(jwkKey); err != nil { 87 | return nil, err 88 | } 89 | 90 | if err := jwk.AssignKeyID(jwkKey); err != nil { 91 | return nil, err 92 | } 93 | 94 | jwkKeyByte, err := p.parser.Marshal(&jwkKey) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | jwkRaw, err := jwk.ParseKey(jwkKeyByte) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | if err := p.parser.Unmarshal(jwkKeyByte, &jwkRawMetadataReq.KeyRaw); err != nil { 105 | return nil, err 106 | } 107 | 108 | if err := p.transform.ReqToRes(&jwkRawMetadataReq, &jwkRawMetadataRes); err != nil { 109 | return nil, err 110 | } 111 | 112 | jwkRawMetadataRes.Key = jwkRaw 113 | 114 | return &jwkRawMetadataRes, nil 115 | } 116 | 117 | func (p jose) ExportJsonWebKey(privateKey *rsa.PrivateKey) (*opt.JwkMetadata, error) { 118 | jwkRawMetadataReq := dto.JwkMetadata{} 119 | jwkRawMetadataRes := opt.JwkMetadata{} 120 | 121 | jwkRaw, err := jwk.ParseKey([]byte(p.cert.PrivateKeyToRaw(privateKey)), jwk.WithPEM(true)) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | jwkRawByte, err := p.parser.Marshal(&jwkRaw) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | if err := p.parser.Unmarshal(jwkRawByte, &jwkRawMetadataReq.KeyRaw); err != nil { 132 | return nil, err 133 | } 134 | 135 | if err := p.transform.ReqToRes(&jwkRawMetadataReq, &jwkRawMetadataRes); err != nil { 136 | return nil, err 137 | } 138 | 139 | jwkRawMetadataRes.Key = jwkRaw.(jwk.Key) 140 | 141 | return &jwkRawMetadataRes, nil 142 | } 143 | 144 | func (p jose) JwtSign(options *dto.JwtSignOption) ([]byte, error) { 145 | jwsHeader := jws.NewHeaders() 146 | jwsHeader.Set("alg", jwa.RS512) 147 | jwsHeader.Set("typ", "JWT") 148 | jwsHeader.Set("cty", "JWT") 149 | jwsHeader.Set("kid", options.Kid) 150 | jwsHeader.Set("b64", true) 151 | 152 | jwtBuilder := jwt.NewBuilder() 153 | jwtBuilder.Audience(options.Aud) 154 | jwtBuilder.Issuer(options.Iss) 155 | jwtBuilder.Subject(options.Sub) 156 | jwtBuilder.IssuedAt(options.Iat) 157 | jwtBuilder.Expiration(options.Exp) 158 | jwtBuilder.JwtID(options.Jti) 159 | jwtBuilder.Claim("timestamp", options.Claim) 160 | 161 | jwtToken, err := jwtBuilder.Build() 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | token, err := jwt.Sign(jwtToken, jwt.WithKey(jwa.RS512(), options.PrivateKey, jws.WithProtectedHeaders(jwsHeader))) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | return token, nil 172 | } 173 | 174 | func (p jose) JwtVerify(prefix string, token string, redis inf.IRedis) (*jwt.Token, error) { 175 | signatureKey := fmt.Sprintf("CREDENTIAL:%s", prefix) 176 | signatureMetadataField := "signature_metadata" 177 | 178 | signatureMetadata := new(dto.SignatureMetadata) 179 | signatureMetadataBytes, err := redis.HGet(signatureKey, signatureMetadataField) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | if err := p.parser.Unmarshal(signatureMetadataBytes, signatureMetadata); err != nil { 185 | return nil, err 186 | } 187 | 188 | if reflect.DeepEqual(signatureMetadata, dto.SignatureMetadata{}) { 189 | return nil, errors.New("Invalid secretkey or signature") 190 | } 191 | 192 | privateKey, err := p.cert.PrivateKeyRawToKey([]byte(signatureMetadata.PrivKeyRaw), []byte(signatureMetadata.CipherKey)) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | exportJws, err := jws.ParseString(token) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | signatures := exportJws.Signatures() 203 | if len(signatures) < 1 { 204 | return nil, errors.New("Invalid signature") 205 | } 206 | 207 | jwsSignature := new(jws.Signature) 208 | for _, signature := range signatures { 209 | jwsSignature = signature 210 | break 211 | } 212 | 213 | jwsHeaders := jwsSignature.ProtectedHeaders() 214 | 215 | algorithm, ok := jwsHeaders.Algorithm() 216 | if !ok { 217 | return nil, errors.New("Invalid algorithm") 218 | } else if algorithm != jwa.RS512() { 219 | return nil, errors.New("Invalid algorithm") 220 | } 221 | 222 | kid, ok := jwsHeaders.KeyID() 223 | if !ok { 224 | return nil, errors.New("Invalid keyid") 225 | } else if kid != signatureMetadata.JweKey.CipherText { 226 | return nil, errors.New("Invalid keyid") 227 | } 228 | 229 | aud := signatureMetadata.SigKey[10:20] 230 | iss := signatureMetadata.SigKey[30:40] 231 | sub := signatureMetadata.SigKey[50:60] 232 | claim := "timestamp" 233 | 234 | jwkKey, err := jwk.Import(privateKey) 235 | if err != nil { 236 | return nil, err 237 | } 238 | 239 | _, err = jws.Verify([]byte(token), jws.WithValidateKey(true), jws.WithKey(algorithm, jwkKey), jws.WithMessage(exportJws)) 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | jwtParse, err := jwt.Parse([]byte(token), 245 | jwt.WithKey(algorithm, privateKey), 246 | jwt.WithAudience(aud), 247 | jwt.WithIssuer(iss), 248 | jwt.WithSubject(sub), 249 | jwt.WithRequiredClaim(claim), 250 | ) 251 | 252 | if err != nil { 253 | return nil, err 254 | } 255 | 256 | return &jwtParse, nil 257 | } 258 | -------------------------------------------------------------------------------- /shared/pkg/jwt.pkg.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/sha512" 9 | "encoding/hex" 10 | "fmt" 11 | "math" 12 | 13 | "time" 14 | 15 | goredis "github.com/redis/go-redis/v9" 16 | cons "github.com/restuwahyu13/go-clean-architecture/shared/constants" 17 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 18 | helper "github.com/restuwahyu13/go-clean-architecture/shared/helpers" 19 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 20 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 21 | ) 22 | 23 | type jsonWebToken struct { 24 | env *dto.Environtment 25 | rds inf.IRedis 26 | jose inf.IJose 27 | cipher inf.ICrypto 28 | cert inf.ICert 29 | parser inf.IParser 30 | transform inf.ITransform 31 | } 32 | 33 | func NewJsonWebToken(ctx context.Context, env *dto.Environtment, con *goredis.Client) inf.IJsonWebToken { 34 | jose := NewJose(ctx) 35 | 36 | rds, err := NewRedis(ctx, con) 37 | if err != nil { 38 | Logrus(cons.FATAL, err) 39 | } 40 | 41 | cipher := helper.NewCrypto() 42 | cert := helper.NewCert() 43 | parser := helper.NewParser() 44 | transform := helper.NewTransform() 45 | 46 | return &jsonWebToken{ 47 | env: env, 48 | rds: rds, 49 | jose: jose, 50 | cipher: cipher, 51 | cert: cert, 52 | parser: parser, 53 | transform: transform, 54 | } 55 | } 56 | 57 | func (p jsonWebToken) createSecret(prefix string, body []byte) (*opt.SecretMetadata, error) { 58 | secretMetadataReq := new(dto.SecretMetadata) 59 | secretMetadataRes := new(opt.SecretMetadata) 60 | 61 | timeNow := time.Now().Format(time.UnixDate) 62 | cipherTextRandom := fmt.Sprintf("%s:%s:%s:%d", prefix, string(body), timeNow, p.env.JWT.EXPIRED) 63 | cipherTextData := hex.EncodeToString([]byte(cipherTextRandom)) 64 | 65 | cipherSecretKey, err := p.cipher.SHA512Sign(cipherTextData) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | cipherText, err := p.cipher.SHA512Sign(timeNow) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | cipherKey, err := p.cipher.AES256Encrypt(cipherSecretKey, cipherText) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | rsaPrivateKeyPassword := []byte(cipherKey) 81 | 82 | privateKey, err := p.cert.GeneratePrivateKey(rsaPrivateKeyPassword) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | secretMetadataReq.PrivKeyRaw = privateKey 88 | secretMetadataReq.CipherKey = cipherKey 89 | 90 | if err := p.transform.ReqToRes(secretMetadataReq, secretMetadataRes); err != nil { 91 | return nil, err 92 | } 93 | 94 | return secretMetadataRes, nil 95 | } 96 | 97 | func (p jsonWebToken) createSignature(prefix string, body any) (*opt.SignatureMetadata, error) { 98 | var ( 99 | signatureMetadataReq *dto.SignatureMetadata = new(dto.SignatureMetadata) 100 | signatureMetadataRes *opt.SignatureMetadata = new(opt.SignatureMetadata) 101 | signatureKey string = fmt.Sprintf("CREDENTIAL:%s", prefix) 102 | signatureField string = "signature_metadata" 103 | ) 104 | 105 | bodyByte, err := p.parser.Marshal(body) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | secretKey, err := p.createSecret(prefix, bodyByte) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | rsaPrivateKey, err := p.cert.PrivateKeyRawToKey([]byte(secretKey.PrivKeyRaw), []byte(secretKey.CipherKey)) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | cipherHash512 := sha512.New() 121 | cipherHash512.Write(bodyByte) 122 | cipherHash512Body := cipherHash512.Sum(nil) 123 | 124 | signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA512, cipherHash512Body) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | if err := rsa.VerifyPKCS1v15(&rsaPrivateKey.PublicKey, crypto.SHA512, cipherHash512Body, signature); err != nil { 130 | return nil, err 131 | } 132 | 133 | signatureOutput := hex.EncodeToString(signature) 134 | 135 | _, jweKey, err := p.jose.JweEncrypt(&rsaPrivateKey.PublicKey, signatureOutput) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | signatureMetadataReq.PrivKeyRaw = secretKey.PrivKeyRaw 141 | signatureMetadataReq.SigKey = signatureOutput 142 | signatureMetadataReq.CipherKey = secretKey.CipherKey 143 | signatureMetadataReq.JweKey = *jweKey 144 | signatureMetadataReq.PrivKey = rsaPrivateKey 145 | 146 | signatureMetadataByte, err := p.parser.Marshal(signatureMetadataReq) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | jwtClaim := string(signatureMetadataByte) 152 | jwtExpired := time.Duration(time.Minute * time.Duration(p.env.JWT.EXPIRED)) 153 | 154 | if err := p.rds.HSetEx(signatureKey, jwtExpired, signatureField, jwtClaim); err != nil { 155 | return nil, err 156 | } 157 | 158 | if err := p.transform.ReqToRes(signatureMetadataReq, signatureMetadataRes); err != nil { 159 | return nil, err 160 | } 161 | 162 | return signatureMetadataRes, nil 163 | } 164 | 165 | func (p jsonWebToken) Sign(prefix string, body any) (*opt.SignMetadata, error) { 166 | tokenKey := fmt.Sprintf("TOKEN:%s", prefix) 167 | signMetadataRes := new(opt.SignMetadata) 168 | 169 | tokenExist, err := p.rds.Exists(tokenKey) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | if tokenExist < 1 { 175 | signature, err := p.createSignature(prefix, body) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | timestamp := time.Now().Format(cons.DATE_TIME_FORMAT) 181 | aud := signature.SigKey[10:20] 182 | iss := signature.SigKey[30:40] 183 | sub := signature.SigKey[50:60] 184 | suffix := int(math.Pow(float64(p.env.JWT.EXPIRED), float64(len(aud)+len(iss)+len(sub)))) 185 | 186 | secretKey := fmt.Sprintf("%s:%s:%s:%s:%d", aud, iss, sub, timestamp, suffix) 187 | secretData := hex.EncodeToString([]byte(secretKey)) 188 | 189 | jti, err := p.cipher.AES256Encrypt(secretData, prefix) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | duration := time.Duration(time.Minute * time.Duration(p.env.JWT.EXPIRED)) 195 | jwtIat := time.Now().UTC().Add(-duration) 196 | jwtExp := time.Now().Add(duration) 197 | 198 | tokenPayload := new(dto.JwtSignOption) 199 | tokenPayload.SecretKey = signature.CipherKey 200 | tokenPayload.Kid = signature.JweKey.CipherText 201 | tokenPayload.PrivateKey = signature.PrivKey 202 | tokenPayload.Aud = []string{aud} 203 | tokenPayload.Iss = iss 204 | tokenPayload.Sub = sub 205 | tokenPayload.Jti = jti 206 | tokenPayload.Iat = jwtIat 207 | tokenPayload.Exp = jwtExp 208 | tokenPayload.Claim = timestamp 209 | 210 | tokenData, err := p.jose.JwtSign(tokenPayload) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | if err := p.rds.SetEx(tokenKey, duration, string(tokenData)); err != nil { 216 | return nil, err 217 | } 218 | 219 | signMetadataRes.Token = string(tokenData) 220 | signMetadataRes.Expired = p.env.JWT.EXPIRED 221 | 222 | return signMetadataRes, nil 223 | } else { 224 | tokenData, err := p.rds.Get(tokenKey) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | signMetadataRes.Token = string(tokenData) 230 | signMetadataRes.Expired = p.env.JWT.EXPIRED 231 | 232 | return signMetadataRes, nil 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /shared/pkg/logrus.pkg.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func Logrus(Type string, Msg any, Args ...any) { 10 | format := false 11 | logrus.SetFormatter(&logrus.TextFormatter{ 12 | TimestampFormat: time.RFC3339, 13 | }) 14 | 15 | if Args != nil { 16 | format = true 17 | } 18 | 19 | switch Type { 20 | 21 | case "info": 22 | if format { 23 | logrus.Infof(Msg.(string), Args...) 24 | } else { 25 | logrus.Info(Msg) 26 | } 27 | 28 | break 29 | 30 | case "error": 31 | if format { 32 | logrus.Errorf(Msg.(string), Args...) 33 | } else { 34 | logrus.Error(Msg) 35 | } 36 | 37 | break 38 | 39 | case "print": 40 | if format { 41 | logrus.Printf(Msg.(string), Args...) 42 | } else { 43 | logrus.Print(Msg) 44 | } 45 | 46 | break 47 | 48 | case "fatal": 49 | if format { 50 | logrus.Fatalf(Msg.(string), Args...) 51 | } else { 52 | logrus.Fatal(Msg) 53 | } 54 | 55 | break 56 | 57 | case "debug": 58 | if format { 59 | logrus.Debugf(Msg.(string), Args...) 60 | } else { 61 | logrus.Debug(Msg) 62 | } 63 | 64 | break 65 | 66 | case "panic": 67 | if format { 68 | logrus.Panicf(Msg.(string), Args...) 69 | } else { 70 | logrus.Panic(Msg) 71 | } 72 | 73 | break 74 | 75 | default: 76 | logrus.Println(Msg) 77 | 78 | break 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /shared/pkg/redis.pkg.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | 6 | "time" 7 | 8 | goredis "github.com/redis/go-redis/v9" 9 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 10 | ) 11 | 12 | type redis struct { 13 | redis *goredis.Client 14 | ctx context.Context 15 | } 16 | 17 | func NewRedis(ctx context.Context, con *goredis.Client) (inf.IRedis, error) { 18 | return &redis{redis: con, ctx: ctx}, nil 19 | } 20 | 21 | func (p redis) SetEx(key string, expiration time.Duration, value any) error { 22 | cmd := p.redis.SetEx(p.ctx, key, value, expiration) 23 | 24 | if err := cmd.Err(); err != nil { 25 | return err 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func (p redis) Get(key string) ([]byte, error) { 32 | cmd := p.redis.Get(p.ctx, key) 33 | 34 | if err := cmd.Err(); err != nil { 35 | return nil, err 36 | } 37 | 38 | res := cmd.Val() 39 | return []byte(res), nil 40 | } 41 | 42 | func (p redis) Del(key string) (int64, error) { 43 | cmd := p.redis.Del(p.ctx, key) 44 | 45 | if err := cmd.Err(); err != nil { 46 | return 0, err 47 | } 48 | 49 | return cmd.Val(), nil 50 | } 51 | 52 | func (p redis) Exists(key string) (int64, error) { 53 | cmd := p.redis.Exists(p.ctx, key) 54 | 55 | if err := cmd.Err(); err != nil { 56 | return 0, err 57 | } 58 | 59 | return cmd.Val(), nil 60 | } 61 | 62 | func (p redis) HSetEx(key string, expiration time.Duration, values ...any) error { 63 | cmd := p.redis.HSet(p.ctx, key, values) 64 | p.redis.Expire(p.ctx, key, expiration) 65 | 66 | if err := cmd.Err(); err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (p redis) HGet(key, field string) ([]byte, error) { 74 | cmd := p.redis.HGet(p.ctx, key, field) 75 | 76 | if err := cmd.Err(); err != nil { 77 | return nil, err 78 | } 79 | 80 | res := cmd.Val() 81 | return []byte(res), nil 82 | } 83 | -------------------------------------------------------------------------------- /usecases/users.usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/restuwahyu13/go-clean-architecture/shared/dto" 7 | inf "github.com/restuwahyu13/go-clean-architecture/shared/interfaces" 8 | opt "github.com/restuwahyu13/go-clean-architecture/shared/output" 9 | ) 10 | 11 | type usersUsecase struct { 12 | service inf.IUsersService 13 | } 14 | 15 | func NewUsersUsecase(options dto.UsecaseOptions[inf.IUsersService]) inf.IUsersUsecase { 16 | return &usersUsecase{service: options.SERVICE} 17 | } 18 | 19 | func (u usersUsecase) Ping(ctx context.Context) opt.Response { 20 | return u.service.Ping(ctx) 21 | } 22 | --------------------------------------------------------------------------------