├── .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 |
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 |
--------------------------------------------------------------------------------