├── .editorconfig ├── .env.example ├── .github └── workflows │ ├── aws_ebs_deploy.yml │ └── go-sec.yml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── UPDATING_DEPENDENCIES.md ├── atlas.hcl ├── bootstrap ├── app.go └── modules.go ├── console ├── console.go └── serve.go ├── contributing.md ├── dbconfig.yml ├── devguide ├── AddingEndpoints.md ├── HandlingErrors.md └── UnhandledExceptions.md ├── docker-compose.yml ├── docker ├── custom.cnf ├── db.Dockerfile ├── run.sh └── web.Dockerfile ├── domain ├── constants │ └── user.go ├── models │ └── user.go ├── module.go └── user │ ├── api_error.go │ ├── controller.go │ ├── module.go │ ├── repository.go │ ├── route.go │ └── service.go ├── go.mod ├── go.sum ├── hooks └── pre-commit ├── main.go ├── migrations ├── 20240606114654.sql └── atlas.sum ├── pkg ├── errorz │ ├── base.go │ ├── common_errors.go │ ├── errors.go │ └── type.go ├── framework │ ├── command.go │ ├── context_constants.go │ ├── env.go │ └── logger.go ├── infrastructure │ ├── aws.go │ ├── db.go │ ├── module.go │ └── router.go ├── middlewares │ ├── auth_middleware.go │ ├── cognito_middleware.go │ ├── middlewares.go │ ├── rate_limit_middleware.go │ └── upload_middleware.go ├── module.go ├── responses │ ├── handle_errors.go │ ├── handle_errors_test.go │ ├── mock_sentry_service_test.go │ └── response.go ├── services │ ├── cognito.go │ ├── module.go │ ├── s3.go │ └── ses_service.go ├── types │ ├── base.go │ ├── binary_uuid.go │ └── file_metadata.go └── utils │ ├── aws_error_mapper.go │ ├── custom_bind.go │ ├── datatype_converter.go │ ├── is_cli.go │ ├── pagination.go │ ├── send_sentry_msg.go │ ├── sentry_service.go │ └── status_in_list.go └── seeds ├── admin_seed.go └── seeds.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 11 | indent_style = tab 12 | indent_size = 8 13 | 14 | [*.md] 15 | indent_size = 4 16 | trim_trailing_whitespace = false 17 | 18 | eclint_indent_style = unset 19 | 20 | [Dockerfile] 21 | indent_size = 4 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SERVER_PORT=5000 2 | ENVIRONMENT=local 3 | LOG_LEVEL=info 4 | 5 | DB_HOST=database 6 | DB_PORT=3306 7 | DB_NAME=clean_gin 8 | DB_USER=root 9 | DB_PASS=secret 10 | DB_FORWARD_PORT = 11 | 12 | # cloudsql, mysql 13 | DB_TYPE=mysql 14 | 15 | SENTRY_DSN= 16 | 17 | MAX_MULTIPART_MEMORY=10485760 18 | 19 | ADMINER_PORT=5001 20 | 21 | ADMIN_EMAIL= 22 | ADMIN_PASSWORD= 23 | 24 | STORAGE_BUCKET_NAME= 25 | 26 | DEBUG_PORT=5002 27 | 28 | TIMEZONE= 29 | 30 | STORAGE_BUCKET_NAME= 31 | 32 | AWS_REGION= 33 | AWS_ACCESS_KEY_ID= 34 | COGNITO_CLIENT_ID= 35 | COGNITO_USER_POOL_ID= 36 | AWS_SECRET_ACCESS_KEY= -------------------------------------------------------------------------------- /.github/workflows/aws_ebs_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy API. 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | types: [review_requested, edited, synchronize] 11 | #For manual trigger of workflow. 12 | workflow_dispatch: 13 | 14 | jobs: 15 | setup_environment: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | env_name: ${{ steps.get_env.outputs.env }} 19 | env: 20 | GITHUB_REF: ${{ github.ref }} 21 | steps: 22 | - id: get_env 23 | run: | 24 | if grep -q "refs/tags/v" <<< ${{github.ref}} || grep -q "refs/heads/main" <<< ${{github.ref}}; then 25 | echo "env=PROD" >> $GITHUB_OUTPUT 26 | else 27 | echo "env=DEV" >> $GITHUB_OUTPUT 28 | fi 29 | - id: print_env 30 | name: Print environment 31 | run: echo "Environment :- ${{ steps.get_env.outputs.env }}" 32 | 33 | deploy: 34 | name: Build and Deploy Go 35 | runs-on: ubuntu-latest 36 | needs: setup_environment 37 | environment: ${{needs.setup_environment.outputs.env_name}} 38 | steps: 39 | - name: Checkout source code 40 | uses: actions/checkout@v3 41 | 42 | - name: Use golang ${{matrix.go-version}} 43 | uses: actions/setup-go@v4 44 | with: 45 | go-version: "^1.22" 46 | 47 | - run: go version 48 | 49 | - name: Set up MySQL 50 | run: | 51 | sudo /etc/init.d/mysql start 52 | mysql -e 'CREATE DATABASE root;' -uroot -proot 53 | mysql -e "show databases;" -uroot -proot 54 | 55 | - name: Initialize the environment variables 56 | run: | 57 | echo 'ENVIRONMENT=workflow 58 | SERVER_PORT=8080 59 | DB_HOST=localhost 60 | DB_NAME=root 61 | DB_PASS=root 62 | DB_PORT=3306 63 | DB_TYPE=local 64 | DB_USER=root 65 | DB_FORWARD_PORT=3306 66 | LOG_LEVEL=info' > .env 67 | 68 | - name: Build the repository 69 | run: | 70 | go build main.go 71 | curl -sSf https://atlasgo.sh | sh 72 | 73 | - name: Run migration 74 | run: | 75 | make migrate-apply 76 | 77 | - name: Start the service 78 | run: 79 | ./main app:serve & 80 | - name: Validate if the service is working or not via health check api 81 | run: | 82 | sleep 5 83 | curl http://localhost:8080/health-check 84 | 85 | - name: Slack Notification on SUCCESS 86 | if: success() 87 | uses: tokorom/action-slack-incoming-webhook@main 88 | env: 89 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 90 | with: 91 | text: A development deployment job for api has succeeded :tada:. 92 | attachments: | 93 | [ 94 | { 95 | "color": "good", 96 | "author_name": "${{ github.actor }}", 97 | "author_icon": "${{ github.event.sender.avatar_url }}", 98 | "fields": [ 99 | { 100 | "title": "Commit Message", 101 | "value": "${{ github.event.head_commit.message }}" 102 | }, 103 | { 104 | "title": "GitHub Actions URL", 105 | "value": "${{ github.event.repository.url }}/actions/runs/${{ github.run_id }}" 106 | }, 107 | { 108 | "title": "Compare URL", 109 | "value": "${{ github.event.compare }}" 110 | }, 111 | { 112 | "title": "ENV", 113 | "value": "${{needs.setup_environment.outputs.env_name}}" 114 | } 115 | ] 116 | } 117 | ] 118 | 119 | - name: Slack Notification on FAILURE 120 | if: failure() 121 | uses: tokorom/action-slack-incoming-webhook@main 122 | env: 123 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 124 | with: 125 | text: A development deployment job for api has failed :crying_cat_face:. 126 | attachments: | 127 | [ 128 | { 129 | "color": "danger", 130 | "author_name": "${{ github.actor }}", 131 | "author_icon": "${{ github.event.sender.avatar_url }}", 132 | "fields": [ 133 | { 134 | "title": "Commit Message", 135 | "value": "${{ github.event.head_commit.message }}" 136 | }, 137 | { 138 | "title": "GitHub Actions URL", 139 | "value": "${{ github.event.repository.url }}/actions/runs/${{ github.run_id }}" 140 | }, 141 | { 142 | "title": "Compare URL", 143 | "value": "${{ github.event.compare }}" 144 | }, 145 | { 146 | "title": "ENV", 147 | "value": "${{needs.setup_environment.outputs.env_name}}" 148 | } 149 | ] 150 | } 151 | ] 152 | -------------------------------------------------------------------------------- /.github/workflows/go-sec.yml: -------------------------------------------------------------------------------- 1 | name: "API Security Scan" 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | workflow_dispatch: 8 | 9 | jobs: 10 | go-sec-tests: 11 | environment: "DEV" 12 | runs-on: ubuntu-latest 13 | env: 14 | GO111MODULE: on 15 | steps: 16 | - name: Checkout Source 17 | uses: actions/checkout@v3 18 | - name: Run Gosec Security Scanner 19 | uses: securego/gosec@master 20 | with: 21 | args: "-no-fail -fmt html -out index.html ./..." 22 | 23 | - name: Generate Report 24 | run: | 25 | echo "Generating Report" 26 | mkdir ./public 27 | mv index.html ./public/ 28 | 29 | - name: Deploy to Github Pages. 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{secrets.GITHUB_TOKEN}} 33 | publish_dir: ./public 34 | user_name: "go-sec-tester" 35 | user_email: "github-report@users.noreply.github.com" 36 | 37 | - name: Slack Notification 38 | if: success() 39 | uses: tokorom/action-slack-incoming-webhook@main 40 | env: 41 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 42 | REPOSITORY_NAME: ${{ github.event.repository.name }} 43 | with: 44 | text: Go Sec report has been Generated :tada:. 45 | attachments: | 46 | [ 47 | { 48 | "color": "good", 49 | "author_name": "Go Sec Report", 50 | "author_icon": "https://cdn-icons-png.flaticon.com/512/3082/3082421.png", 51 | "fields": [ 52 | { 53 | "title": "Report URL", 54 | "value": "http://wesionaryteam.github.io/${{env.REPOSITORY_NAME}}" 55 | } 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | log/access.log 3 | .git 4 | .worktrees 5 | log 6 | .idea/ 7 | .DS_Store 8 | 9 | __debug_bin 10 | 11 | serviceAccountKey.json 12 | .test.env 13 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | errcheck: 3 | check-type-assertions: true 4 | goconst: 5 | min-len: 2 6 | min-occurrences: 3 7 | 8 | govet: 9 | check-shadowing: true 10 | nolintlint: 11 | require-explanation: true 12 | require-specific: true 13 | revive: 14 | rules: 15 | - name: unused-parameter 16 | severity: warning 17 | disabled: true 18 | arguments: 19 | - allowRegex: "^_|^tx$" 20 | 21 | linters: 22 | disable-all: true 23 | enable: 24 | - bodyclose 25 | - unused 26 | - errcheck 27 | - exportloopref 28 | - goconst 29 | - gofmt 30 | - goimports 31 | - gocyclo 32 | - gosimple 33 | - ineffassign 34 | - misspell 35 | - nakedret 36 | - prealloc 37 | - predeclared 38 | - revive 39 | - staticcheck 40 | - unused 41 | - typecheck 42 | - unconvert 43 | - unparam 44 | - unused 45 | 46 | run: 47 | issues-exit-code: 1 48 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/golangci/golangci-lint.git 3 | rev: v1.52.2 4 | hooks: 5 | - id: golangci-lint -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Attach to Remote", 5 | "type": "go", 6 | "request": "attach", 7 | "mode": "remote", 8 | "port": 5002, 9 | "host": "127.0.0.1", 10 | "cwd": "${workspaceFolder}", 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.inferGopath": false, 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | export 3 | 4 | MIGRATE=atlas migrate 5 | 6 | migrate-status: 7 | $(MIGRATE) status --url "mysql://$(DB_USER):$(DB_PASS)@:$(DB_FORWARD_PORT)/$(DB_NAME)" 8 | 9 | migrate-diff: 10 | $(MIGRATE) diff --env gorm 11 | 12 | migrate-apply: 13 | $(MIGRATE) apply --url "mysql://$(DB_USER):$(DB_PASS)@:$(DB_FORWARD_PORT)/$(DB_NAME)" 14 | 15 | migrate-down: 16 | $(MIGRATE) down --url "mysql://$(DB_USER):$(DB_PASS)@:$(DB_FORWARD_PORT)/$(DB_NAME)" --env gorm 17 | 18 | migrate-hash: 19 | $(MIGRATE) hash 20 | 21 | lint-setup: 22 | python3 -m ensurepip --upgrade 23 | sudo pip3 install pre-commit 24 | pre-commit install 25 | pre-commit autoupdate 26 | 27 | .PHONY: migrate-status migrate-diff migrate-apply migrate-down migrate-hash lint-setup 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Clean Architecture 2 | 3 | Clean Architecture with [Gin Web Framework](https://github.com/gin-gonic/gin) 4 | 5 | ## Features :star: 6 | 7 | - Clean Architecture written in Go 8 | - Application backbone with [Gin Web Framework](https://github.com/gin-gonic/gin) 9 | - Dependency injection using [uber-go/fx](https://pkg.go.dev/go.uber.org/fx) 10 | - Uses fully featured [GORM](https://gorm.io/index.html) 11 | 12 | ## Linter setup 13 | 14 | Need [Python3](https://www.python.org/) to setup linter in git pre-commit hook. 15 | 16 | ```zsh 17 | make lint-setup 18 | ``` 19 | 20 | --- 21 | 22 | ## Run application 23 | 24 | - Setup environment variables 25 | 26 | ```zsh 27 | cp .env.example .env 28 | ``` 29 | 30 | - Update your database credentials environment variables in `.env` file 31 | - Update `STORAGE_BUCKET_NAME` in `.env` with your AWS S3 bucket name. 32 | 33 | ### Locally 34 | 35 | - Run `go run main.go app:serve` to start the server. 36 | - There are other commands available as well. You can run `go run main.go -help` to know about other commands available. 37 | 38 | ### Using `Docker` 39 | 40 | > Ensure Docker is already installed in the machine. 41 | 42 | - Start server using command `docker-compose up -d` or `sudo docker-compose up -d` if there are permission issues. 43 | 44 | --- 45 | 46 | ## Folder Structure :file_folder: 47 | 48 | | Folder Path | Description | 49 | | -------------------------------- | ------------------------------------------------------------------------------------------------------ | 50 | | `/bootstrap` | Contains modules required to start the application. | 51 | | `/console` | Server commands; run `go run main.go -help` for all available commands. | 52 | | `/docker` | Docker files required for `docker-compose`. | 53 | | `/docs` | Contains project documentation. | 54 | | `/domain` | Contains models, constants, and a folder for each domain with controller, repository, routes, and services. | 55 | | `/domain/constants` | Global application constants. | 56 | | `/domain/models` | ORM models. | 57 | | `/domain/` | Controller, repository, routes, and service for a domain (e.g., `user` is a domain in this template). | 58 | | `/hooks` | Git hooks. | 59 | | `/migrations` | Database migration files managed by Atlas. | 60 | | `/pkg` | Contains shared packages for errors, framework utilities, infrastructure, middlewares, responses, services, types, and utils. | 61 | | `/pkg/errorz` | Defines custom error types and handlers for the application. | 62 | | `/pkg/framework` | Core framework components like environment variable parsing, logger setup, etc. | 63 | | `/pkg/infrastructure` | Setup for third-party service connections (e.g., AWS, database, router). | 64 | | `/pkg/middlewares` | HTTP request middlewares used in the application. | 65 | | `/pkg/responses` | Defines standardized HTTP response structures and error handling. | 66 | | `/pkg/services` | Shared application services or clients for external services (e.g., Cognito, S3, SES). | 67 | | `/pkg/types` | Custom data types used throughout the application. | 68 | | `/pkg/utils` | Global utility and helper functions. | 69 | | `/seeds` | Seed data for database tables. | 70 | | `/tests` | Application tests (unit, integration, etc.). | 71 | | `.env.example` | sample environment variables | 72 | | `docker-compose.yml` | `docker compose` file for service application via `Docker` | 73 | | `main.go` | entry-point of the server | 74 | | `Makefile` | stores frequently used commands; can be invoked using `make` command | 75 | 76 | --- 77 | 78 | ## 🚀 Running Migrations 79 | 80 | This project uses [Atlas](https://atlasgo.io/) for database schema migrations. Atlas enables declarative, versioned, and diff-based schema changes. 81 | 82 | --- 83 | 84 | ### 🧰 Prerequisites 85 | 86 | Make sure you have the following set up: 87 | 88 | - **Atlas CLI**: Install Atlas by running: 89 | 90 | ```sh 91 | curl -sSf https://atlasgo.sh | sh 92 | ``` 93 | 94 | > For other installation methods or details, visit the [official installation guide](https://atlasgo.io/getting-started/installation). 95 | 96 | - **`.env` file** at the project root with the following environment variables: 97 | 98 | ```env 99 | DB_USER=root 100 | DB_PASS=secret 101 | DB_NAME=exampledb 102 | DB_FORWARD_PORT=3306 103 | ``` 104 | 105 | --- 106 | 107 | ### 📦 Available Migration Commands 108 | 109 | Below are the supported `make` commands for managing database migrations: 110 | 111 | | Make Command | Description | 112 | | --------------------- | --------------------------------------------------------------------------- | 113 | | `make migrate-status` | Show the current migration status | 114 | | `make migrate-diff` | Generate a new migration by comparing models to the current DB (`gorm` env) | 115 | | `make migrate-apply` | Apply all pending migrations | 116 | | `make migrate-down` | Roll back the most recent migration (`gorm` env) | 117 | | `make migrate-hash` | Hash migration files for integrity checking | 118 | 119 | --- 120 | 121 | 📚 For more on schema management and best practices, refer to the [Atlas documentation](https://atlasgo.io). 122 | 123 | ## Testing 124 | 125 | The framework comes with unit and integration testing support out of the box. You can check examples written in tests directory. 126 | 127 | To run the test just run: 128 | 129 | ```zsh 130 | go test ./... -v 131 | ``` 132 | 133 | ### For test coverage 134 | 135 | ```zsh 136 | go test ./... -v -coverprofile cover.txt -coverpkg=./... 137 | go tool cover -html=cover.txt -o index.html 138 | ``` 139 | 140 | ### Update Dependencies 141 | See [UPDATING_DEPENDENCIES.md](./UPDATING_DEPENDENCIES.md) file for more information on how to update project dependencies. 142 | 143 | 144 | 145 | 146 | ### Contribute 👩‍💻🧑‍💻 147 | 148 | We are happy that you are looking to improve go clean architecture. Please check out the [contributing guide](contributing.md) 149 | 150 | Even if you are not able to make contributions via code, please don't hesitate to file bugs or feature requests that needs to be implemented to solve your use case. 151 | 152 | ### Authors 153 | 154 |
155 | 156 | 157 | 158 |
159 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover a security vulnerability, please open an issue. 6 | 7 | ### Preferred Languages 8 | 9 | We prefer all communications in English. 10 | 11 | 12 | Thank you 13 | -------------------------------------------------------------------------------- /UPDATING_DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # Update Dependencies 2 | 3 | ## Steps to Update Dependencies 4 | 5 | 1. `go get -u` 6 | 2. Remove all the dependencies packages that has `// indirect` from the modules 7 | 3. `go mod tidy` 8 | 9 | ## Discovering available updates 10 | 11 | List all of the modules that are dependencies of your current module, along with the latest version available for each: 12 | ```zsh 13 | go list -m -u all 14 | ``` 15 | 16 | Display the latest version available for a specific module: 17 | 18 | ```zsh 19 | go list -m -u example.com/theirmodule 20 | ``` 21 | 22 | **Example:** 23 | 24 | ```zsh 25 | go list -m -u cloud.google.com/go/firestore 26 | cloud.google.com/go/firestore v1.2.0 [v1.6.1] 27 | ``` 28 | 29 | ## Getting a specific dependency version 30 | 31 | To get a specific numbered version, append the module path with an `@` sign followed by the `version` you want: 32 | 33 | ```zsh 34 | go get example.com/theirmodule@v1.3.4 35 | ``` 36 | 37 | To get the latest version, append the module path with @latest: 38 | 39 | ```zsh 40 | go get example.com/theirmodule@latest 41 | ``` 42 | 43 | ## Synchronizing your code’s dependencies 44 | 45 | ```zsh 46 | go mod tidy 47 | ``` 48 | -------------------------------------------------------------------------------- /atlas.hcl: -------------------------------------------------------------------------------- 1 | data "external_schema" "gorm" { 2 | program = [ 3 | "go", 4 | "run", 5 | "-mod=mod", 6 | "ariga.io/atlas-provider-gorm", 7 | "load", 8 | "--path", "./domain/models", 9 | "--dialect", "mysql", // | postgres | sqlite | sqlserver 10 | ] 11 | } 12 | 13 | env "gorm" { 14 | src = data.external_schema.gorm.url 15 | dev = "docker://mysql/8/dev" 16 | migration { 17 | dir = "file://migrations" 18 | } 19 | format { 20 | migrate { 21 | diff = "{{ sql . \" \" }}" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bootstrap/app.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "clean-architecture/console" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var rootCmd = &cobra.Command{ 10 | Use: "clean-architecture", 11 | Short: "Commander for clean architecture", 12 | Long: ` 13 | This is a command runner or cli for api architecture in golang. 14 | Using this we can use underlying dependency injection container for running scripts. 15 | Main advantage is that, we can use same services, repositories, infrastructure present in the application itself`, 16 | TraverseChildren: true, 17 | } 18 | 19 | // App root of the application 20 | type App struct { 21 | *cobra.Command 22 | } 23 | 24 | // NewApp creates new root command 25 | func NewApp() App { 26 | cmd := App{ 27 | Command: rootCmd, 28 | } 29 | cmd.AddCommand(console.GetSubCommands(CommonModules)...) 30 | return cmd 31 | } 32 | 33 | var RootApp = NewApp() 34 | -------------------------------------------------------------------------------- /bootstrap/modules.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "clean-architecture/domain" 5 | "clean-architecture/pkg" 6 | "clean-architecture/seeds" 7 | 8 | "go.uber.org/fx" 9 | ) 10 | 11 | var CommonModules = fx.Options( 12 | pkg.Module, 13 | domain.Module, 14 | seeds.Module, 15 | ) 16 | -------------------------------------------------------------------------------- /console/console.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "context" 6 | 7 | "github.com/spf13/cobra" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | var cmds = map[string]framework.Command{ 12 | "app:serve": NewServeCommand(), 13 | } 14 | 15 | // GetSubCommands gives a list of sub commands 16 | func GetSubCommands(opt fx.Option) []*cobra.Command { 17 | subCommands := make([]*cobra.Command, 0) 18 | for name, cmd := range cmds { 19 | subCommands = append(subCommands, WrapSubCommand(name, cmd, opt)) 20 | } 21 | return subCommands 22 | } 23 | 24 | func WrapSubCommand(name string, cmd framework.Command, opt fx.Option) *cobra.Command { 25 | wrappedCmd := &cobra.Command{ 26 | Use: name, 27 | Short: cmd.Short(), 28 | Run: func(c *cobra.Command, args []string) { 29 | logger := framework.GetLogger() 30 | 31 | opts := fx.Options( 32 | fx.WithLogger(logger.GetFxLogger), 33 | fx.Invoke(cmd.Run()), 34 | ) 35 | ctx := context.Background() 36 | app := fx.New(opt, opts) 37 | err := app.Start(ctx) 38 | defer func() { 39 | err = app.Stop(ctx) 40 | if err != nil { 41 | logger.Fatal(err) 42 | } 43 | }() 44 | if err != nil { 45 | logger.Fatal(err) 46 | } 47 | }, 48 | } 49 | cmd.Setup(wrappedCmd) 50 | return wrappedCmd 51 | } 52 | -------------------------------------------------------------------------------- /console/serve.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "clean-architecture/pkg/infrastructure" 6 | "clean-architecture/pkg/middlewares" 7 | "time" 8 | 9 | "github.com/getsentry/sentry-go" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // ServeCommand test command 14 | type ServeCommand struct{} 15 | 16 | func (s *ServeCommand) Short() string { 17 | return "serve application" 18 | } 19 | 20 | func (s *ServeCommand) Setup(cmd *cobra.Command) {} 21 | 22 | func (s *ServeCommand) Run() framework.CommandRunner { 23 | return func( 24 | middleware middlewares.Middlewares, 25 | env *framework.Env, 26 | router infrastructure.Router, 27 | logger framework.Logger, 28 | database infrastructure.Database, 29 | //seeds seeds.Seeds, 30 | 31 | ) { 32 | logger.Info(`+-----------------------+`) 33 | logger.Info(`| GO CLEAN ARCHITECTURE |`) 34 | logger.Info(`+-----------------------+`) 35 | 36 | // Using time zone as specified in env file 37 | loc, _ := time.LoadLocation(env.TimeZone) 38 | time.Local = loc 39 | 40 | middleware.Setup() 41 | //seeds.Setup() 42 | 43 | if env.Environment != "local" && env.SentryDSN != "" { 44 | err := sentry.Init(sentry.ClientOptions{ 45 | Dsn: env.SentryDSN, 46 | Environment: env.Environment, 47 | AttachStacktrace: true, 48 | }) 49 | if err != nil { 50 | logger.Error("sentry initialization failed") 51 | logger.Error(err.Error()) 52 | } 53 | } 54 | logger.Info("Running server") 55 | if env.ServerPort == "" { 56 | if err := router.Run(); err != nil { 57 | logger.Fatal(err) 58 | return 59 | } 60 | } else { 61 | if err := router.Run(":" + env.ServerPort); err != nil { 62 | logger.Fatal(err) 63 | return 64 | } 65 | } 66 | } 67 | } 68 | 69 | func NewServeCommand() *ServeCommand { 70 | return &ServeCommand{} 71 | } 72 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Welcome to go_clean_architecture contributing guide 2 | 3 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 4 | 5 | ## Issues 6 | 7 | ### Create a new issue 8 | 9 | - [Check to make sure](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments) someone hasn't already opened a similar [issue](https://github.com/wesionaryTEAM/go_clean_architecture/issues). 10 | - If a similar issue doesn't exist, open a new issue using a relevant [issue form](https://github.com/wesionaryTEAM/go_clean_architecture/issues/new/choose). 11 | 12 | ### Pick up an issue to solve 13 | 14 | - Scan through our [existing issues](https://github.com/wesionaryTEAM/go_clean_architecture/issues) to find one that interests you. 15 | - The [👋 good first issue](https://github.com/wesionaryTEAM/go_clean_architecture/issues?q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%91%8B+good+first+issue%22) is a good place to start exploring issues that are well-groomed for newcomers. 16 | - Do not hesitate to ask for more details or clarifying questions on the issue! 17 | - Communicate on the issue you are intended to pick up _before_ starting working on it. 18 | - Every issue that gets picked up will have an expected timeline for the implementation, the issue may be reassigned after the expected timeline. Please be responsible and proactive on the communication 🙇‍♂️ 19 | 20 | ## Pull requests 21 | 22 | When you're finished with the changes, create a pull request, or a series of pull requests if necessary. 23 | 24 | Contributing to another codebase is not as simple as code changes, it is also about contributing influence to the design. Therefore, we kindly ask you that: 25 | 26 | - Please acknowledge that no pull request is guaranteed to be merged. 27 | - Please always do a self-review before requesting reviews from others. 28 | - Please expect code review to be strict and may have multiple rounds. 29 | - Please make self-contained incremental changes, pull requests with huge diff may be rejected for review. 30 | - Please use English in code comments and docstring. 31 | - Please do not force push unless absolutely necessary. Force pushes make review much harder in multiple rounds. 32 | 33 | ## Raising Pull Requests 34 | 35 | - Please resolve linting and formatting issue produced by `golangci-lint run`. 36 | - Please keep the PR's small and focused on one thing 37 | - Please follow the format of creating branches 38 | - feature/[feature name]: This branch should contain changes for a specific feature 39 | - Example: feature/email-service 40 | - bugfix/[bug name]: This branch should contain only bug fixes for a specific bug 41 | - Example bugfix/db-connection 42 | -------------------------------------------------------------------------------- /dbconfig.yml: -------------------------------------------------------------------------------- 1 | development: 2 | dialect: mysql 3 | datasource: ${DB_USER}:${DB_PASS}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}?parseTime=true 4 | -------------------------------------------------------------------------------- /devguide/AddingEndpoints.md: -------------------------------------------------------------------------------- 1 | ## Adding API Endpoint in the architecture 2 | 3 | - If package name is not known, read package name from `go.mod` file when importing internal packages. 4 | - If new feature is required, create inside `domain//`. Feature generally has controller, route, service, module, serializer (dto) and repository all in separate files. 5 | - Strictly adhere to the request and response structure if present, you need to create DTO layer to convert the db structure (created at `models/`) to req/resp structure (created at `domain//`). 6 | - Before adding dependencies for controller, route, service, repositories etc. check how its done in other features and make similar changes. Especially check for pointer or non-pointer dependencies in return type of provider function for the dependencies. 7 | for example: 8 | ```go 9 | package infrastructure 10 | 11 | // NewDatabase creates a new database instance 12 | func NewDatabase(logger framework.Logger, env *framework.Env) Database { 13 | } 14 | ``` 15 | the dependency Database is non pointer type. where as env is pointer type. 16 | 17 | - After all these files are created, the module should contain dependency injection setup for created controller, route, service and repository. Route registration is done using `fx.Invoke` which runs route registraion function on application start automatically. 18 | for example: 19 | ```go 20 | var Module = fx.Module("user", 21 | fx.Options( 22 | fx.Provide( 23 | NewRepository, 24 | NewService, 25 | NewController, 26 | NewRoute, 27 | ), 28 | 29 | fx.Invoke(RegisterRoute), 30 | )) 31 | ``` 32 | - The `domain//module.go` module, should be linked with `domain/module.go` so that, it is added to dependency injection tree. 33 | 34 | ### Example of Using `infrastructure.Database` in Repository 35 | 36 | When creating a repository, you can use `infrastructure.Database` to interact with the database. Below is an example of how to create a repository: 37 | 38 | ```go 39 | package user 40 | 41 | import ( 42 | "clean-architecture/domain/models" 43 | "clean-architecture/pkg/framework" 44 | "clean-architecture/pkg/infrastructure" 45 | ) 46 | 47 | // Repository represents the user repository structure 48 | type Repository struct { 49 | infrastructure.Database 50 | logger framework.Logger 51 | } 52 | 53 | // NewRepository initializes a new user repository 54 | func NewRepository(db infrastructure.Database, logger framework.Logger) Repository { 55 | return Repository{db, logger} 56 | } 57 | 58 | // ExampleMethod demonstrates a database query using infrastructure.Database 59 | func (r *Repository) ExampleMethod() error { 60 | r.logger.Info("[Repository...ExampleMethod]") 61 | 62 | var users []models.User 63 | err := r.DB.Find(&users).Error 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | ``` 71 | 72 | ### Defining Routes for a Feature 73 | 74 | To define routes for a feature, you need to create a `route.go` file inside the respective feature's directory (e.g., `domain//route.go`). Below is an example of how routes are defined for the `user` domain: 75 | 76 | ```go 77 | package user 78 | 79 | import ( 80 | "clean-architecture/pkg/framework" 81 | "clean-architecture/pkg/infrastructure" 82 | ) 83 | 84 | // Route struct 85 | type Route struct { 86 | logger framework.Logger 87 | handler infrastructure.Router 88 | controller *Controller 89 | } 90 | 91 | // NewRoute initializes a new Route instance 92 | func NewRoute( 93 | logger framework.Logger, 94 | handler infrastructure.Router, 95 | controller *Controller, 96 | ) *Route { 97 | return &Route{ 98 | handler: handler, 99 | logger: logger, 100 | controller: controller, 101 | } 102 | } 103 | 104 | // RegisterRoute sets up the routes for the feature 105 | func RegisterRoute(r *Route) { 106 | r.logger.Info("Setting up routes") 107 | 108 | api := r.handler.Group("/api") 109 | 110 | api.POST("/user", r.controller.CreateUser) 111 | api.GET("/user/:id", r.controller.GetUserByID) 112 | } 113 | ``` 114 | 115 | #### Key Points: 116 | - The `Route` struct holds dependencies like the logger, router, and controller. 117 | - The `NewRoute` function initializes the `Route` struct. 118 | - The `RegisterRoute` function defines the actual routes and associates them with controller methods. 119 | - Use the `Group` method of the router to group routes under a common prefix (e.g., `/api`). 120 | 121 | This structure ensures that routes are modular and easy to manage for each feature. 122 | 123 | ### Using `fx.Invoke` for Route Registration 124 | 125 | After defining the routes for a feature, you need to register them in the module file using `fx.Invoke`. This ensures that the route registration function is automatically executed when the application starts. Below is an example of how this is done for the `user` domain: 126 | 127 | ```go 128 | package user 129 | 130 | import ( 131 | "go.uber.org/fx" 132 | ) 133 | 134 | // Module provides the dependencies for the user domain 135 | var Module = fx.Module("user", 136 | fx.Provide( 137 | NewRepository, 138 | NewService, 139 | NewController, 140 | NewRoute, 141 | ), 142 | fx.Invoke(RegisterRoute), 143 | ) 144 | ``` 145 | 146 | #### Key Points: 147 | - The `fx.Provide` function is used to declare the dependencies (e.g., repository, service, controller, and route) for the feature. 148 | - The `fx.Invoke` function is used to call the `RegisterRoute` function, which sets up the routes for the feature. 149 | - This setup ensures that the routes are registered as part of the application's dependency injection lifecycle. 150 | 151 | By following this pattern, you can maintain a clean and modular structure for route registration in your application. 152 | 153 | ## Adding new models for a feature 154 | 155 | - For adding new db models for a feature, models are added to `domain/models` folder. 156 | - After adding models, it is essential to diff the database with models and generate migration using atlas go. Since, makefile already contains the command for migration, you can check it. 157 | - The generated migrations, need to be run as well. 158 | - Some datatypes for new model generation; 159 | UUID -> types.BinaryUUID 160 | - Database we are using in MySQL so other variant of SQL in model definition might not work. 161 | 162 | Here is a sample model definition, that you might need. 163 | ```go 164 | package models 165 | 166 | import ( 167 | "clean-architecture/domain/constants" 168 | "clean-architecture/pkg/types" 169 | 170 | _ "ariga.io/atlas-provider-gorm/gormschema" 171 | 172 | "github.com/google/uuid" 173 | "gorm.io/gorm" 174 | ) 175 | 176 | // User model 177 | type User struct { 178 | gorm.Model 179 | UUID types.BinaryUUID `json:"uuid" gorm:"index;notnull;unique"` 180 | CognitoUID *string `json:"-" gorm:"index;size:50;unique"` 181 | 182 | FirstName string `json:"first_name" gorm:"size:255"` 183 | LastName string `json:"last_name" gorm:"size:255"` 184 | 185 | Email string `json:"email" gorm:"notnull;index,unique;size:255"` 186 | Role constants.UserRole `json:"role" gorm:"size:25" copier:"-"` 187 | } 188 | 189 | // BeforeCreate auto generate uuid before creating if it's not present already 190 | func (u *User) BeforeCreate(tx *gorm.DB) error { 191 | if u.UUID.String() == (types.BinaryUUID{}).String() { 192 | id, err := uuid.NewRandom() 193 | u.UUID = types.BinaryUUID(id) 194 | return err 195 | } 196 | return nil 197 | } 198 | 199 | func (*User) TableName() string { 200 | return "users" 201 | } 202 | ``` 203 | 204 | 205 | ### 📦 Available Migration Commands 206 | 207 | Below are the supported `make` commands for managing database migrations: 208 | 209 | | Make Command | Description | 210 | | --------------------- | --------------------------------------------------------------------------- | 211 | | `make migrate-status` | Show the current migration status | 212 | | `make migrate-diff` | Generate a new migration by comparing models to the current DB (`gorm` env) | 213 | | `make migrate-apply` | Apply all pending migrations | 214 | | `make migrate-down` | Roll back the most recent migration (`gorm` env) | 215 | | `make migrate-hash` | Hash migration files for integrity checking | -------------------------------------------------------------------------------- /devguide/HandlingErrors.md: -------------------------------------------------------------------------------- 1 | ### `responses` package 2 | 3 | The `responses` package provides utility functions to standardize the structure of HTTP responses in the application. It includes functions for handling both success and error responses, ensuring consistency and clarity in API responses. 4 | 5 | #### Key Functions: 6 | 7 | 1. **JSON**: Sends a JSON response with a given status code and data payload. 8 | 2. **ErrorJSON**: Sends a JSON response specifically for errors, with a given status code and error message. 9 | 3. **SuccessJSON**: Sends a JSON response for successful operations, with a given status code and success message. 10 | 4. **JSONWithPagination**: Sends a JSON response with pagination details, including data and pagination metadata like `has_next` and `count`. 11 | 12 | #### Error Handling: 13 | 14 | The `responses` package also includes functions for handling errors effectively: 15 | 16 | 1. **HandleValidationError**: Logs and sends a `400 Bad Request` response for validation errors. 17 | 2. **HandleErrorWithStatus**: Logs and sends a response with a custom status code for specific errors. 18 | 3. **HandleError**: A comprehensive error handler that: 19 | - Handles custom `APIError` types. 20 | - Handles `gorm.ErrRecordNotFound` with a `404 Not Found` response. 21 | - Logs and sends a generic `500 Internal Server Error` response for unhandled errors. 22 | - Captures unhandled exceptions using Sentry for further analysis. 23 | 24 | ### Examples for `responses` package 25 | 26 | #### Example: Sending a JSON Response 27 | ```go 28 | import ( 29 | "github.com/gin-gonic/gin" 30 | "clean-architecture/pkg/responses" 31 | ) 32 | 33 | func ExampleHandler(c *gin.Context) { 34 | data := map[string]string{"message": "Hello, World!"} 35 | responses.JSON(c, http.StatusOK, data) 36 | } 37 | ``` 38 | 39 | #### Example: Sending an Error Response 40 | ```go 41 | import ( 42 | "github.com/gin-gonic/gin" 43 | "clean-architecture/pkg/responses" 44 | ) 45 | 46 | func ErrorHandler(c *gin.Context) { 47 | err := "Something went wrong" 48 | responses.ErrorJSON(c, http.StatusBadRequest, err) 49 | } 50 | ``` 51 | 52 | #### Example: Sending a Success Response 53 | ```go 54 | import ( 55 | "github.com/gin-gonic/gin" 56 | "clean-architecture/pkg/responses" 57 | ) 58 | 59 | func SuccessHandler(c *gin.Context) { 60 | msg := "Operation successful" 61 | responses.SuccessJSON(c, http.StatusOK, msg) 62 | } 63 | ``` 64 | 65 | ### Examples for `HandleValidationError` and `HandleError` 66 | 67 | #### Example: Using `HandleValidationError` 68 | ```go 69 | import ( 70 | "github.com/gin-gonic/gin" 71 | "clean-architecture/pkg/framework" 72 | "clean-architecture/pkg/responses" 73 | ) 74 | 75 | func ValidationErrorHandler(c *gin.Context) { 76 | logger := framework.NewLogger() 77 | err := errors.New("Invalid input data") 78 | responses.HandleValidationError(logger, c, err) 79 | } 80 | ``` 81 | 82 | #### Example: Using `HandleError` with `errorz` package 83 | ```go 84 | import ( 85 | "github.com/gin-gonic/gin" 86 | "clean-architecture/pkg/errorz" 87 | "clean-architecture/pkg/framework" 88 | "clean-architecture/pkg/responses" 89 | ) 90 | 91 | func APIErrorHandler(c *gin.Context) { 92 | logger := framework.NewLogger() 93 | 94 | // Example of a custom API error 95 | apiErr := &errorz.APIError{ 96 | StatusCode: 404, 97 | Message: "Resource not found", 98 | } 99 | 100 | responses.HandleError(logger, c, apiErr) 101 | } 102 | 103 | func GenericErrorHandler(c *gin.Context) { 104 | logger := framework.NewLogger() 105 | 106 | // Example of a generic error 107 | err := errors.New("Something went wrong") 108 | responses.HandleError(logger, c, err) 109 | } 110 | ``` 111 | 112 | ### Defining Common Errors with `errorz` Package 113 | 114 | The `errorz` package allows you to define commonly used errors in a centralized manner, making it easier to reuse them across different parts of the application. This approach ensures consistency in error handling and reduces duplication. 115 | 116 | #### Steps to Define and Use Common Errors: 117 | 118 | 1. **Define Common Errors**: 119 | Use the `errorz` package to define errors that are frequently used, such as validation errors, authentication errors, or resource not found errors. 120 | 121 | ```go 122 | package errorz 123 | 124 | import "net/http" 125 | 126 | var ( 127 | ErrUnauthorized = &APIError{ 128 | StatusCode: http.StatusUnauthorized, 129 | Message: "Unauthorized access", 130 | } 131 | 132 | ErrResourceNotFound = &APIError{ 133 | StatusCode: http.StatusNotFound, 134 | Message: "The requested resource was not found", 135 | } 136 | 137 | ErrInvalidInput = &APIError{ 138 | StatusCode: http.StatusBadRequest, 139 | Message: "Invalid input provided", 140 | } 141 | ) 142 | ``` 143 | 144 | 2. **Use Defined Errors in Handlers**: 145 | Use these predefined errors in your handlers or services to ensure consistent error responses. 146 | 147 | ```go 148 | import ( 149 | "github.com/gin-gonic/gin" 150 | "clean-architecture/pkg/errorz" 151 | "clean-architecture/pkg/framework" 152 | "clean-architecture/pkg/responses" 153 | ) 154 | 155 | func ExampleHandler(c *gin.Context) { 156 | logger := framework.NewLogger() 157 | 158 | // Simulate an error condition 159 | if true { // Replace with actual condition 160 | responses.HandleError(logger, c, errorz.ErrInvalidInput) 161 | return 162 | } 163 | 164 | responses.SuccessJSON(c, http.StatusOK, "Operation successful") 165 | } 166 | ``` 167 | 168 | 3. **Benefits of Centralized Error Definitions**: 169 | - **Consistency**: Ensures that the same error messages and status codes are used across the application. 170 | - **Maintainability**: Makes it easier to update error messages or status codes in one place. 171 | - **Reusability**: Reduces duplication by allowing the same error definitions to be reused in multiple places. 172 | -------------------------------------------------------------------------------- /devguide/UnhandledExceptions.md: -------------------------------------------------------------------------------- 1 | ### Handling Unhandled Exceptions with Sentry 2 | 3 | Unhandled exceptions can occur in any application and need to be captured and logged effectively to ensure they are addressed promptly. In this project, we use Sentry to capture unhandled exceptions and provide detailed context for debugging. 4 | 5 | #### Capturing Unhandled Exceptions 6 | 7 | The `utils.SendSentryMsg` function can be used to send custom error messages to Sentry. For unhandled exceptions, you can use the `utils.CurrentSentryService.CaptureException` method to capture the exception and send it to Sentry. 8 | 9 | Example: 10 | 11 | ```go 12 | import ( 13 | "errors" 14 | "github.com/gin-gonic/gin" 15 | "clean-architecture/pkg/utils" 16 | ) 17 | 18 | func ExampleUnhandledExceptionHandler(c *gin.Context) { 19 | defer func() { 20 | if r := recover(); r != nil { 21 | err := errors.New("Unhandled exception occurred") 22 | utils.CurrentSentryService.CaptureException(err) 23 | c.JSON(500, gin.H{"error": "An unexpected error occurred. Please try again later."}) 24 | } 25 | }() 26 | 27 | // Simulate an unhandled exception 28 | panic("Simulated panic") 29 | } 30 | ``` 31 | 32 | 33 | #### Best Practices 34 | 35 | 1. Use `defer` and `recover` to handle panics and capture unhandled exceptions. 36 | 2. Pass relevant context to Sentry to make debugging easier. 37 | 3. Ensure sensitive information is not sent to Sentry. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | web: 5 | build: 6 | context: . 7 | dockerfile: ./docker/web.Dockerfile 8 | ports: 9 | - ${SERVER_PORT}:${SERVER_PORT} 10 | - ${DEBUG_PORT}:2345 11 | volumes: 12 | - .:/clean_web 13 | env_file: .env 14 | container_name: clean-web 15 | security_opt: 16 | - seccomp:unconfined 17 | depends_on: 18 | database: 19 | condition: service_healthy 20 | 21 | database: 22 | build: 23 | context: . 24 | dockerfile: ./docker/db.Dockerfile 25 | environment: 26 | MYSQL_ROOT_PASSWORD: "${DB_PASS}" 27 | MYSQL_DATABASE: "${DB_NAME}" 28 | MYSQL_ROOT_HOST: "%" 29 | container_name: clean-db 30 | command: 31 | [ 32 | "--character-set-server=utf8mb4", 33 | "--collation-server=utf8mb4_unicode_ci", 34 | "--default-authentication-plugin=mysql_native_password", 35 | ] 36 | ports: 37 | - "${DB_FORWARD_PORT}:${DB_PORT}" 38 | volumes: 39 | - clean_db:/var/lib/mysql 40 | healthcheck: 41 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 42 | interval: "1s" 43 | timeout: "10s" 44 | retries: 10 45 | start_period: "1s" 46 | 47 | adminer: 48 | image: adminer 49 | ports: 50 | - ${ADMINER_PORT}:8080 51 | 52 | volumes: 53 | clean_db: 54 | -------------------------------------------------------------------------------- /docker/custom.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | character-set-server=utf8mb4 3 | 4 | [client] 5 | default-character-set=utf8mb4 -------------------------------------------------------------------------------- /docker/db.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql/mysql-server:8.0 2 | 3 | COPY ./docker/custom.cnf /etc/mysql/conf.d/custom.cnf 4 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | while true; do 2 | echo "[run.sh] Starting debugging..." 3 | dlv debug --headless --log --listen=:2345 --api-version=2 --output ./__debug_bin --accept-multiclient --continue -- app:serve & 4 | 5 | PID=$! 6 | 7 | inotifywait -e modify -e move -e create -e delete -e attrib --exclude '(__debug_bin|\.git)' -r . 8 | 9 | echo "[run.sh] Stopping process id: $PID" 10 | 11 | kill -9 $PID 12 | pkill -f __debug_bin 13 | done -------------------------------------------------------------------------------- /docker/web.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | # Required because go requires gcc to build 4 | RUN apk add build-base git inotify-tools 5 | RUN echo $GOPATH 6 | RUN go install github.com/go-delve/delve/cmd/dlv@latest 7 | 8 | COPY . /clean_web 9 | WORKDIR /clean_web 10 | RUN go mod download 11 | 12 | CMD sh /clean_web/docker/run.sh 13 | -------------------------------------------------------------------------------- /domain/constants/user.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | type UserRole string 4 | 5 | const ( 6 | UserRoleAdmin UserRole = "admin" 7 | ) 8 | -------------------------------------------------------------------------------- /domain/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "clean-architecture/domain/constants" 5 | "clean-architecture/pkg/types" 6 | 7 | _ "ariga.io/atlas-provider-gorm/gormschema" 8 | 9 | "github.com/google/uuid" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | // User model 14 | type User struct { 15 | gorm.Model 16 | UUID types.BinaryUUID `json:"uuid" gorm:"index;notnull;unique"` 17 | CognitoUID *string `json:"-" gorm:"index;size:50;unique"` 18 | 19 | FirstName string `json:"first_name" gorm:"size:255"` 20 | LastName string `json:"last_name" gorm:"size:255"` 21 | FirstNameJa string `json:"first_name_ja" gorm:"size:255"` 22 | LastNameJa string `json:"last_name_ja" gorm:"size:255"` 23 | 24 | Email string `json:"email" gorm:"notnull;index,unique;size:255"` 25 | Role constants.UserRole `json:"role" gorm:"size:25" copier:"-"` 26 | 27 | IsActive bool `json:"is_active" gorm:"default:false"` 28 | IsEmailVerified bool `json:"is_email_verified" gorm:"default:false"` 29 | } 30 | 31 | func (u *User) BeforeCreate(tx *gorm.DB) error { 32 | if u.UUID.String() == (types.BinaryUUID{}).String() { 33 | id, err := uuid.NewRandom() 34 | u.UUID = types.BinaryUUID(id) 35 | return err 36 | } 37 | return nil 38 | } 39 | 40 | func (*User) TableName() string { 41 | return "users" 42 | } 43 | -------------------------------------------------------------------------------- /domain/module.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "clean-architecture/domain/user" 5 | 6 | "go.uber.org/fx" 7 | ) 8 | 9 | var Module = fx.Module("domain", 10 | fx.Options( 11 | user.Module, 12 | ), 13 | ) 14 | -------------------------------------------------------------------------------- /domain/user/api_error.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "clean-architecture/pkg/errorz" 5 | "net/http" 6 | ) 7 | 8 | var ( 9 | ErrInvalidUserID = errorz.NewAPIError(http.StatusBadRequest, "Invalid user ID") 10 | ) 11 | -------------------------------------------------------------------------------- /domain/user/controller.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "clean-architecture/domain/models" 5 | "clean-architecture/pkg/framework" 6 | "clean-architecture/pkg/responses" 7 | "clean-architecture/pkg/types" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // UserController data type 13 | type Controller struct { 14 | service *Service 15 | logger framework.Logger 16 | env *framework.Env 17 | } 18 | 19 | type URLObject struct { 20 | Name string `json:"name"` 21 | URL string `json:"url"` 22 | } 23 | 24 | // NewUserController creates new user controller 25 | func NewController( 26 | userService *Service, 27 | logger framework.Logger, 28 | env *framework.Env, 29 | ) *Controller { 30 | return &Controller{ 31 | service: userService, 32 | logger: logger, 33 | env: env, 34 | } 35 | } 36 | 37 | // CreateUser creates the new user 38 | func (u *Controller) CreateUser(c *gin.Context) { 39 | var user models.User 40 | 41 | if err := c.Bind(&user); err != nil { 42 | responses.HandleError(u.logger, c, err) 43 | return 44 | } 45 | 46 | // check if the user already exists 47 | 48 | if err := u.service.Create(&user); err != nil { 49 | responses.HandleError(u.logger, c, err) 50 | return 51 | } 52 | 53 | c.JSON(200, gin.H{"data": "user created"}) 54 | } 55 | 56 | // GetOneUser gets one user 57 | func (u *Controller) GetUserByID(c *gin.Context) { 58 | paramID := c.Param("id") 59 | 60 | userID, err := types.ShouldParseUUID(paramID) 61 | if err != nil { 62 | responses.HandleValidationError(u.logger, c, ErrInvalidUserID) 63 | return 64 | } 65 | 66 | user, err := u.service.GetUserByID(userID) 67 | if err != nil { 68 | responses.HandleError(u.logger, c, err) 69 | return 70 | } 71 | 72 | c.JSON(200, gin.H{ 73 | "data": user, 74 | }) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /domain/user/module.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "go.uber.org/fx" 4 | 5 | var Module = fx.Module("user", 6 | fx.Options( 7 | fx.Provide( 8 | NewRepository, 9 | NewService, 10 | NewController, 11 | NewRoute, 12 | ), 13 | //If you want to enable auto-migrate add Migrate as shown below 14 | // fx.Invoke(Migrate, RegisterRoute), 15 | 16 | fx.Invoke(RegisterRoute), 17 | )) 18 | -------------------------------------------------------------------------------- /domain/user/repository.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "clean-architecture/domain/models" 5 | "clean-architecture/pkg/framework" 6 | "clean-architecture/pkg/infrastructure" 7 | ) 8 | 9 | // UserRepository database structure 10 | type Repository struct { 11 | infrastructure.Database 12 | logger framework.Logger 13 | } 14 | 15 | // NewUserRepository creates a new user repository 16 | func NewRepository(db infrastructure.Database, logger framework.Logger) Repository { 17 | return Repository{db, logger} 18 | } 19 | 20 | // ExistsByEmail checks if the user exists by email 21 | func (r *Repository) ExistsByEmail(email string) (bool, error) { 22 | r.logger.Info("[UserRepository...Exists]") 23 | 24 | users := make([]models.User, 0, 1) 25 | query := r.DB.Where("email = ?", email).Limit(1).Find(&users) 26 | 27 | return query.RowsAffected > 0, query.Error 28 | } 29 | -------------------------------------------------------------------------------- /domain/user/route.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "clean-architecture/pkg/infrastructure" 6 | ) 7 | 8 | // UserRoutes struct 9 | type Route struct { 10 | logger framework.Logger 11 | handler infrastructure.Router 12 | controller *Controller 13 | } 14 | 15 | func NewRoute( 16 | logger framework.Logger, 17 | handler infrastructure.Router, 18 | controller *Controller, 19 | ) *Route { 20 | return &Route{ 21 | handler: handler, 22 | logger: logger, 23 | controller: controller, 24 | } 25 | 26 | } 27 | 28 | // Setup user routes 29 | func RegisterRoute(r *Route) { 30 | r.logger.Info("Setting up routes") 31 | 32 | api := r.handler.Group("/api") 33 | 34 | api.POST("/user", r.controller.CreateUser) 35 | api.GET("/user/:id", r.controller.GetUserByID) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /domain/user/service.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "clean-architecture/domain/models" 5 | "clean-architecture/pkg/framework" 6 | "clean-architecture/pkg/types" 7 | ) 8 | 9 | // UserService service layer 10 | type Service struct { 11 | logger framework.Logger 12 | repository Repository 13 | } 14 | 15 | // NewUserService creates a new userservice 16 | func NewService( 17 | logger framework.Logger, 18 | userRepository Repository, 19 | ) *Service { 20 | return &Service{ 21 | logger: logger, 22 | repository: userRepository, 23 | } 24 | } 25 | 26 | // Create creates the user in database 27 | func (s Service) Create(user *models.User) error { 28 | return s.repository.Create(user).Error 29 | } 30 | 31 | // GetOneUser gets one user 32 | func (s Service) GetUserByID(userID types.BinaryUUID) (user models.User, err error) { 33 | return user, s.repository.First(&user, "id = ?", userID).Error 34 | } 35 | 36 | // GetRawUserFromID gets the raw user from id 37 | func (r *Repository) GetRawUserFromID(userID uint) (user *models.User, err error) { 38 | r.logger.Info("[UserRepository...GetRawUserFromID]") 39 | 40 | query := r.Model(&models.User{}).Where("id = ?", userID).First(&user) 41 | 42 | return user, query.Error 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module clean-architecture 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.4 6 | 7 | require ( 8 | ariga.io/atlas-provider-gorm v0.4.0 9 | github.com/aws/aws-sdk-go-v2 v1.27.1 10 | github.com/aws/aws-sdk-go-v2/config v1.27.17 11 | github.com/aws/aws-sdk-go-v2/credentials v1.17.17 12 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.23 13 | github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.38.4 14 | github.com/aws/aws-sdk-go-v2/service/s3 v1.55.0 15 | github.com/aws/aws-sdk-go-v2/service/sesv2 v1.29.5 16 | github.com/aws/smithy-go v1.20.2 17 | github.com/chai2010/webp v1.1.1 18 | github.com/getsentry/sentry-go v0.28.0 19 | github.com/gin-contrib/cors v1.7.2 20 | github.com/gin-gonic/gin v1.10.0 21 | github.com/go-sql-driver/mysql v1.8.1 22 | github.com/google/uuid v1.6.0 23 | github.com/joho/godotenv v1.5.1 24 | github.com/lestrrat-go/jwx v1.2.29 25 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 26 | github.com/spf13/cobra v1.8.0 27 | github.com/spf13/viper v1.19.0 28 | github.com/stretchr/testify v1.9.0 29 | github.com/ulule/limiter/v3 v3.11.2 30 | go.uber.org/fx v1.22.0 31 | go.uber.org/zap v1.27.0 32 | golang.org/x/sync v0.7.0 33 | gorm.io/driver/mysql v1.5.6 34 | gorm.io/gorm v1.25.10 35 | ) 36 | 37 | require ( 38 | ariga.io/atlas-go-sdk v0.5.3 // indirect 39 | filippo.io/edwards25519 v1.1.0 // indirect 40 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect 41 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.4 // indirect 42 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8 // indirect 43 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8 // indirect 44 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 45 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.8 // indirect 46 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect 47 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.10 // indirect 48 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.10 // indirect 49 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.8 // indirect 50 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.10 // indirect 51 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.4 // indirect 52 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.11 // indirect 53 | github.com/bytedance/sonic v1.11.8 // indirect 54 | github.com/bytedance/sonic/loader v0.1.1 // indirect 55 | github.com/cloudwego/base64x v0.1.4 // indirect 56 | github.com/cloudwego/iasm v0.2.0 // indirect 57 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 58 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 59 | github.com/fsnotify/fsnotify v1.7.0 // indirect 60 | github.com/gabriel-vasile/mimetype v1.4.4 // indirect 61 | github.com/gin-contrib/sse v0.1.0 // indirect 62 | github.com/go-playground/locales v0.14.1 // indirect 63 | github.com/go-playground/universal-translator v0.18.1 // indirect 64 | github.com/go-playground/validator/v10 v10.21.0 // indirect 65 | github.com/goccy/go-json v0.10.3 // indirect 66 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 67 | github.com/golang-sql/sqlexp v0.1.0 // indirect 68 | github.com/hashicorp/hcl v1.0.0 // indirect 69 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 70 | github.com/jackc/pgpassfile v1.0.0 // indirect 71 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect 72 | github.com/jackc/pgx/v5 v5.6.0 // indirect 73 | github.com/jackc/puddle/v2 v2.2.1 // indirect 74 | github.com/jinzhu/inflection v1.0.0 // indirect 75 | github.com/jinzhu/now v1.1.5 // indirect 76 | github.com/jmespath/go-jmespath v0.4.0 // indirect 77 | github.com/json-iterator/go v1.1.12 // indirect 78 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 79 | github.com/leodido/go-urn v1.4.0 // indirect 80 | github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect 81 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 82 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 83 | github.com/lestrrat-go/iter v1.0.2 // indirect 84 | github.com/lestrrat-go/option v1.0.1 // indirect 85 | github.com/magiconair/properties v1.8.7 // indirect 86 | github.com/mattn/go-isatty v0.0.20 // indirect 87 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 88 | github.com/microsoft/go-mssqldb v1.7.2 // indirect 89 | github.com/mitchellh/mapstructure v1.5.0 // indirect 90 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 91 | github.com/modern-go/reflect2 v1.0.2 // indirect 92 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 93 | github.com/pkg/errors v0.9.1 // indirect 94 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 95 | github.com/sagikazarmark/locafero v0.6.0 // indirect 96 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 97 | github.com/sourcegraph/conc v0.3.0 // indirect 98 | github.com/spf13/afero v1.11.0 // indirect 99 | github.com/spf13/cast v1.6.0 // indirect 100 | github.com/spf13/pflag v1.0.5 // indirect 101 | github.com/subosito/gotenv v1.6.0 // indirect 102 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 103 | github.com/ugorji/go/codec v1.2.12 // indirect 104 | go.uber.org/dig v1.17.1 // indirect 105 | go.uber.org/multierr v1.11.0 // indirect 106 | golang.org/x/arch v0.8.0 // indirect 107 | golang.org/x/crypto v0.24.0 // indirect 108 | golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect 109 | golang.org/x/net v0.26.0 // indirect 110 | golang.org/x/sys v0.21.0 // indirect 111 | golang.org/x/text v0.16.0 // indirect 112 | google.golang.org/protobuf v1.34.1 // indirect 113 | gopkg.in/ini.v1 v1.67.0 // indirect 114 | gopkg.in/yaml.v3 v3.0.1 // indirect 115 | gorm.io/driver/postgres v1.5.7 // indirect 116 | gorm.io/driver/sqlite v1.5.5 // indirect 117 | gorm.io/driver/sqlserver v1.5.3 // indirect 118 | ) 119 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | ariga.io/atlas-go-sdk v0.5.3 h1:KlLWPUnKm/gv3iaUDXAwUfQPZuEFbrAcqDIAFIImXZ0= 2 | ariga.io/atlas-go-sdk v0.5.3/go.mod h1:wCso3QwMboXPUD5vNjBPDc3z086Ix3kfooanvcdlwV4= 3 | ariga.io/atlas-provider-gorm v0.4.0 h1:x4kEgGf6LbrIiaZNBR+Tz+HG9oguzVt8XNyuVzdfMes= 4 | ariga.io/atlas-provider-gorm v0.4.0/go.mod h1:8m6+N6+IgWMzPcR63c9sNOBoxfNk6yV6txBZBrgLg1o= 5 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 6 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 7 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= 8 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= 9 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= 10 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= 11 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= 12 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= 13 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= 14 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= 15 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= 16 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= 17 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= 18 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= 19 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= 20 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= 21 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= 22 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= 23 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= 24 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= 25 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= 26 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= 27 | github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= 28 | github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 29 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= 30 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 31 | github.com/aws/aws-sdk-go-v2 v1.27.1 h1:xypCL2owhog46iFxBKKpBcw+bPTX/RJzwNj8uSilENw= 32 | github.com/aws/aws-sdk-go-v2 v1.27.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= 33 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= 34 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= 35 | github.com/aws/aws-sdk-go-v2/config v1.27.17 h1:L0JZN7Gh7pT6u5CJReKsLhGKparqNKui+mcpxMXjDZc= 36 | github.com/aws/aws-sdk-go-v2/config v1.27.17/go.mod h1:MzM3balLZeaafYcPz8IihAmam/aCz6niPQI0FdprxW0= 37 | github.com/aws/aws-sdk-go-v2/credentials v1.17.17 h1:b3Dk9uxQByS9sc6r0sc2jmxsJKO75eOcb9nNEiaUBLM= 38 | github.com/aws/aws-sdk-go-v2/credentials v1.17.17/go.mod h1:e4khg9iY08LnFK/HXQDWMf9GDaiMari7jWPnXvKAuBU= 39 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.4 h1:0cSfTYYL9qiRcdi4Dvz+8s3JUgNR2qvbgZkXcwPEEEk= 40 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.4/go.mod h1:Wjn5O9eS7uSi7vlPKt/v0MLTncANn9EMmoDvnzJli6o= 41 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.23 h1:g6IHovcexw51hcP0hxsT7Mr3/PG76hZvoodm9tuKuUc= 42 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.23/go.mod h1:8KSZ0CibxgOaPk28CFL4DGBdGrscHJr8FuxB+jnJBaM= 43 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8 h1:RnLB7p6aaFMRfyQkD6ckxR7myCC9SABIqSz4czYUUbU= 44 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8/go.mod h1:XH7dQJd+56wEbP1I4e4Duo+QhSMxNArE8VP7NuUOTeM= 45 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8 h1:jzApk2f58L9yW9q1GEab3BMMFWUkkiZhyrRUtbwUbKU= 46 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8/go.mod h1:WqO+FftfO3tGePUtQxPXM6iODVfqMwsVMgTbG/ZXIdQ= 47 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 48 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 49 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.8 h1:jH33S0y5Bo5ZVML62JgZhjd/LrtU+vbR8W7XnIE3Srk= 50 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.8/go.mod h1:hD5YwHLOy6k7d6kqcn3me1bFWHOtzhaXstMd6BpdB68= 51 | github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.38.4 h1:8C/vQ86wicCVx8tNtQxRqBB5mZSxvW/wzCCc9Mh8id4= 52 | github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.38.4/go.mod h1:Yw+3uzj5EcIo/pmmYda2VfZz0adIF4o6Ga89qEB4JZk= 53 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= 54 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= 55 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.10 h1:pkYC5zTOSPXEYJj56b2SOik9AL432i5MT1YVTQbKOK0= 56 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.10/go.mod h1:/WNsBOlKWZCG3PMh2aSp8vkyyT/clpMZqOtrnIKqGfk= 57 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.10 h1:7kZqP7akv0enu6ykJhb9OYlw16oOrSy+Epus8o/VqMY= 58 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.10/go.mod h1:gYVF3nM1ApfTRDj9pvdhootBb8WbiIejuqn4w8ruMes= 59 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.8 h1:iQNXVs1vtaq+y9M90M4ZIVNORje0qXTscqHLqoOnFS0= 60 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.8/go.mod h1:yUQPRlWqGG0lfNsmjbRWKVwgilfBtZTOFSLEYALlAig= 61 | github.com/aws/aws-sdk-go-v2/service/s3 v1.55.0 h1:6kq0Xql9qiwNGL/Go87ZqR4otg9jnKs71OfWCVbPxLM= 62 | github.com/aws/aws-sdk-go-v2/service/s3 v1.55.0/go.mod h1:oSkRFuHVWmUY4Ssk16ErGzBqvYEbvORJFzFXzWhTB2s= 63 | github.com/aws/aws-sdk-go-v2/service/sesv2 v1.29.5 h1:TRQLLU2t4ftJInFxdaJznmgxRoGc3MmucfQjOCQLoFg= 64 | github.com/aws/aws-sdk-go-v2/service/sesv2 v1.29.5/go.mod h1:illLUYpxYsuNYYAmUXNRmrPENgDTEpRChpO7cnIPHrs= 65 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.10 h1:ItKVmFwbyb/ZnCWf+nu3XBVmUirpO9eGEQd7urnBA0s= 66 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.10/go.mod h1:5XKooCTi9VB/xZmJDvh7uZ+v3uQ7QdX6diOyhvPA+/w= 67 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.4 h1:QMSCYDg3Iyls0KZc/dk3JtS2c1lFfqbmYO10qBPPkJk= 68 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.4/go.mod h1:MZ/PVYU/mRbmSF6WK3ybCYHjA2mig8utVokDEVLDgE0= 69 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.11 h1:HYS0csS7UJxdYRoG+bGgUYrSwVnV3/ece/wHm90TApM= 70 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.11/go.mod h1:QXnthRM35zI92048MMwfFChjFmoufTdhtHmouwNfhhU= 71 | github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= 72 | github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 73 | github.com/bytedance/sonic v1.11.8 h1:Zw/j1KfiS+OYTi9lyB3bb0CFxPJVkM17k1wyDG32LRA= 74 | github.com/bytedance/sonic v1.11.8/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 75 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 76 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 77 | github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= 78 | github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= 79 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 80 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 81 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 82 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 83 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 84 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 85 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 86 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 87 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 88 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 89 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 90 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 91 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 92 | github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= 93 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 94 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 95 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 96 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 97 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 98 | github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= 99 | github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= 100 | github.com/getsentry/sentry-go v0.28.0 h1:7Rqx9M3ythTKy2J6uZLHmc8Sz9OGgIlseuO1iBX/s0M= 101 | github.com/getsentry/sentry-go v0.28.0/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= 102 | github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= 103 | github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= 104 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 105 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 106 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 107 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 108 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 109 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 110 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 111 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 112 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 113 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 114 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 115 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 116 | github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0= 117 | github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 118 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 119 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 120 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 121 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 122 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 123 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 124 | github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 125 | github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 126 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 127 | github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= 128 | github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 129 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 130 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 131 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 132 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 133 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 134 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 135 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 136 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 137 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 138 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 139 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 140 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 141 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 142 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 143 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 144 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 145 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 146 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 147 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 148 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 149 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= 150 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 151 | github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= 152 | github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= 153 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 154 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 155 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 156 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 157 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 158 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 159 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 160 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 161 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 162 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 163 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 164 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 165 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 166 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 167 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 168 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 169 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 170 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 171 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 172 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 173 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 174 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 175 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 176 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 177 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 178 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 179 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 180 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 181 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 182 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 183 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 184 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 185 | github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= 186 | github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= 187 | github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 188 | github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 189 | github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 190 | github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 191 | github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 192 | github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 193 | github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= 194 | github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= 195 | github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 196 | github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 197 | github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 198 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 199 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 200 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 201 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 202 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 203 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 204 | github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= 205 | github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= 206 | github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= 207 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 208 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 209 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 210 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 211 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 212 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 213 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 214 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 215 | github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 216 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 217 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 218 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 219 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 220 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 221 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 222 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 223 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 224 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 225 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 226 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 227 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 228 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 229 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 230 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 231 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 232 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 233 | github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= 234 | github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= 235 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 236 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 237 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 238 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 239 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 240 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 241 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 242 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 243 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 244 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 245 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 246 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 247 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 248 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 249 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 250 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 251 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 252 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 253 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 254 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 255 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 256 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 257 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 258 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 259 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 260 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 261 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 262 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 263 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 264 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 265 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 266 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 267 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 268 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 269 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 270 | github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA= 271 | github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= 272 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 273 | go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= 274 | go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= 275 | go.uber.org/fx v1.22.0 h1:pApUK7yL0OUHMd8vkunWSlLxZVFFk70jR2nKde8X2NM= 276 | go.uber.org/fx v1.22.0/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= 277 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 278 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 279 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 280 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 281 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 282 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 283 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 284 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 285 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 286 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 287 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 288 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 289 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 290 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 291 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 292 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 293 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 294 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 295 | golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= 296 | golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 297 | golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= 298 | golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= 299 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 300 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 301 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 302 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 303 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 304 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 305 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 306 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 307 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 308 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 309 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 310 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 311 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 312 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 313 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 314 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 315 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 316 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 317 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 318 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 319 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 320 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 321 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 325 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 326 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 327 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 328 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 329 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 330 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 331 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 332 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 333 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 334 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 335 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 336 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 337 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 338 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 339 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 340 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 341 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 342 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 343 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 344 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 345 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 346 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 347 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 348 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 349 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 350 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 351 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 352 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 353 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 354 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 355 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 356 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 357 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 358 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 359 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 360 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 361 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 362 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 363 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 364 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 365 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 366 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 367 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 368 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 369 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 370 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 371 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 372 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 373 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 374 | gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= 375 | gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 376 | gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= 377 | gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= 378 | gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= 379 | gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= 380 | gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= 381 | gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= 382 | gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 383 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 384 | gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= 385 | gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 386 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 387 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 388 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this will retrieve all of the .go files that have been 4 | # changed since the last commit 5 | STAGED_GO_FILES=$(git diff --cached --name-only -- '*.go') 6 | 7 | # we can check to see if this is empty 8 | if [[ "$STAGED_GO_FILES" = "" ]]; then 9 | exit 0 10 | fi 11 | 12 | PASS=true 13 | 14 | # On the staged file 15 | for FILE in $STAGED_GO_FILES 16 | do 17 | # Run goimports 18 | goimports -w $FILE 19 | 20 | # Run golangci-lint 21 | golangci-lint run $FILE 22 | if [[ $? == 1 ]]; then 23 | PASS=false 24 | fi 25 | done 26 | 27 | if ! $PASS; then 28 | printf "COMMIT FAILED\n" 29 | exit 1 30 | else 31 | printf "COMMIT SUCCEEDED\n" 32 | git add . 33 | fi 34 | 35 | exit 0 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clean-architecture/bootstrap" 5 | 6 | "github.com/joho/godotenv" 7 | ) 8 | 9 | func main() { 10 | _ = godotenv.Load() 11 | _ = bootstrap.RootApp.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /migrations/20240606114654.sql: -------------------------------------------------------------------------------- 1 | -- Create "users" table 2 | CREATE TABLE `users` ( 3 | `id` bigint unsigned NOT NULL AUTO_INCREMENT, 4 | `created_at` datetime(3) NULL, 5 | `updated_at` datetime(3) NULL, 6 | `deleted_at` datetime(3) NULL, 7 | `uuid` binary(16) NOT NULL, 8 | `cognito_uid` varchar(50) NULL, 9 | `first_name` varchar(255) NULL, 10 | `last_name` varchar(255) NULL, 11 | `first_name_ja` varchar(255) NULL, 12 | `last_name_ja` varchar(255) NULL, 13 | `email` varchar(255) NOT NULL, 14 | `role` varchar(25) NULL, 15 | `is_active` bool NULL DEFAULT 0, 16 | `is_email_verified` bool NULL DEFAULT 0, 17 | PRIMARY KEY (`id`), 18 | INDEX `idx_users_cognito_uid` (`cognito_uid`), 19 | INDEX `idx_users_deleted_at` (`deleted_at`), 20 | INDEX `idx_users_uuid` (`uuid`), 21 | UNIQUE INDEX `uni_users_cognito_uid` (`cognito_uid`), 22 | UNIQUE INDEX `uni_users_uuid` (`uuid`) 23 | ) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci; 24 | -------------------------------------------------------------------------------- /migrations/atlas.sum: -------------------------------------------------------------------------------- 1 | h1:BI5GqWcKQr37TA5p+GoU8+uj5gPCo5ymhVfohowa+J8= 2 | 20240606114654.sql h1:2tDAB4KV1ZZO2vIZDmzuqcr3FpgrraqUcp28ghcyojY= 3 | -------------------------------------------------------------------------------- /pkg/errorz/base.go: -------------------------------------------------------------------------------- 1 | package errorz 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | var ( 9 | ErrBadRequest = NewAPIError(http.StatusBadRequest, "Bad Request") 10 | ErrUnauthorized = NewAPIError(http.StatusUnauthorized, "Unauthorized") 11 | ErrForbidden = NewAPIError(http.StatusForbidden, "Forbidden") 12 | ErrNotFound = NewAPIError(http.StatusNotFound, "Not Found") 13 | ErrConflict = NewAPIError(http.StatusConflict, "Conflict") 14 | ErrUnprocessable = NewAPIError(http.StatusUnprocessableEntity, "Unable to process the contained instructions") 15 | ErrInternal = NewAPIError(http.StatusInternalServerError, "Internal Server Error") 16 | ErrServiceUnavailable = NewAPIError(http.StatusServiceUnavailable, "Service Unavailable") 17 | ErrAlreadyExists = JoinError("Already Exists", ErrConflict) 18 | ErrSomethingWentWrong = JoinError("something went wrong", ErrInternal) 19 | ) 20 | 21 | func JoinError(message string, base error) error { 22 | if base.Error() == "" { 23 | return fmt.Errorf("%v%w", message, base) 24 | } 25 | return fmt.Errorf("%v %w", message, base) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/errorz/common_errors.go: -------------------------------------------------------------------------------- 1 | package errorz 2 | 3 | var ( 4 | ErrUnauthorizedAccess = ErrUnauthorized.JoinError("Unauthorized access") 5 | ErrForbiddenAccess = ErrForbidden.JoinError("Forbidden access") 6 | ErrInvalidToken = ErrBadRequest.JoinError("Invalid token") 7 | ErrInvalidUUID = ErrBadRequest.JoinError("Invalid UUID") 8 | ErrRecordNotFound = ErrNotFound.JoinError("Record not found") 9 | ErrInvalidUserNameOrPassword = ErrBadRequest.JoinError("Invalid username and password") 10 | ErrExtensionMismatch = ErrBadRequest.JoinError("file extension not supported") 11 | ErrThumbExtensionMismatch = ErrBadRequest.JoinError("file extension not supported for thumbnail") 12 | ErrFileRead = ErrBadRequest.JoinError("file read error") 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/errorz/errors.go: -------------------------------------------------------------------------------- 1 | package errorz //nolint:revive // underscore in package name is okay 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // for dynamic error 8 | type ErrTokenVerification struct { 9 | id string 10 | } 11 | 12 | func NewErrTokenVerification(id string) error { 13 | return ErrTokenVerification{ 14 | id: id, 15 | } 16 | } 17 | 18 | func (e ErrTokenVerification) Error() string { 19 | return fmt.Sprintf("error verifying id token %v\n", e.id) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/errorz/type.go: -------------------------------------------------------------------------------- 1 | package errorz 2 | 3 | import "fmt" 4 | 5 | type APIError struct { 6 | StatusCode int 7 | Message string 8 | } 9 | 10 | func (e *APIError) Error() string { 11 | return "" 12 | } 13 | 14 | func NewAPIError(statusCode int, message string) *APIError { 15 | return &APIError{ 16 | StatusCode: statusCode, 17 | Message: message, 18 | } 19 | } 20 | 21 | func (a *APIError) JoinError(message string) error { 22 | if a == nil { 23 | return nil 24 | } 25 | return fmt.Errorf("%v%w", message, a) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/framework/command.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | type CommandRunner any 8 | 9 | // Command interface is used to implement sub-commands in the system. 10 | type Command interface { 11 | // Short returns string about short description of the command 12 | // the string is shown in help screen of cobra command 13 | Short() string 14 | 15 | // Setup is used to setup flags or pre-run steps for the command. 16 | // 17 | // For example, 18 | // cmd.Flags().IntVarP(&r.num, "num", "n", 5, "description") 19 | // 20 | Setup(cmd *cobra.Command) 21 | 22 | // Run runs the command runner 23 | // run returns command runner which is a function with dependency 24 | // injected arguments. 25 | // 26 | // For example, 27 | // Command{ 28 | // Run: func(l lib.Logger) { 29 | // l.Info("i am working") 30 | // }, 31 | // } 32 | // 33 | Run() CommandRunner 34 | } 35 | -------------------------------------------------------------------------------- /pkg/framework/context_constants.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | const ( 4 | // Claims -> authentication claims 5 | Claims = "Claims" 6 | 7 | // UID -> authenticated user's id 8 | UID = "UID" 9 | 10 | // File uploaded file from file upload middleware 11 | File = "@uploaded_file" 12 | 13 | // Limit for get all api 14 | Limit = "Limit" 15 | 16 | // Page 17 | Page = "Page" 18 | 19 | // Rate Limit 20 | RateLimit = "RateLimit" 21 | 22 | // Token -> bearer token 23 | Token = "Token" 24 | 25 | CognitoPass = "CognitoPass" 26 | 27 | Role = "Role" 28 | ) 29 | -------------------------------------------------------------------------------- /pkg/framework/env.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | ) 6 | 7 | type Env struct { 8 | LogLevel string `mapstructure:"LOG_LEVEL"` 9 | ServerPort string `mapstructure:"SERVER_PORT"` 10 | Environment string `mapstructure:"ENVIRONMENT"` 11 | 12 | DBUsername string `mapstructure:"DB_USER"` 13 | DBPassword string `mapstructure:"DB_PASS"` 14 | DBHost string `mapstructure:"DB_HOST"` 15 | DBPort string `mapstructure:"DB_PORT"` 16 | DBName string `mapstructure:"DB_NAME"` 17 | DBType string `mapstructure:"DB_TYPE"` 18 | 19 | SentryDSN string `mapstructure:"SENTRY_DSN"` 20 | MaxMultipartMemory int64 `mapstructure:"MAX_MULTIPART_MEMORY"` 21 | StorageBucketName string `mapstructure:"STORAGE_BUCKET_NAME"` 22 | 23 | TimeZone string `mapstructure:"TIMEZONE"` 24 | AdminEmail string `mapstructure:"ADMIN_EMAIL"` 25 | AdminPassword string `mapstructure:"ADMIN_PASSWORD"` 26 | 27 | AWSRegion string `mapstructure:"AWS_REGION"` 28 | AWSAccessKey string `mapstructure:"AWS_ACCESS_KEY_ID"` 29 | ClientID string `mapstructure:"COGNITO_CLIENT_ID"` 30 | UserPoolID string `mapstructure:"COGNITO_USER_POOL_ID"` 31 | AWSSecretAccessKey string `mapstructure:"AWS_SECRET_ACCESS_KEY"` 32 | DBFORWARDPORT string `mapstructure:"DB_FORWARD_PORT"` 33 | } 34 | 35 | var globalEnv = Env{ 36 | MaxMultipartMemory: 10 << 20, // 10 MB 37 | } 38 | 39 | func GetEnv() Env { 40 | return globalEnv 41 | } 42 | 43 | func NewEnv(logger Logger) *Env { 44 | viper.SetConfigFile(".env") 45 | 46 | err := viper.ReadInConfig() 47 | if err != nil { 48 | logger.Fatal("cannot read cofiguration", err) 49 | } 50 | 51 | viper.SetDefault("TIMEZONE", "UTC") 52 | 53 | err = viper.Unmarshal(&globalEnv) 54 | if err != nil { 55 | logger.Fatal("environment cant be loaded: ", err) 56 | } 57 | 58 | return &globalEnv 59 | } 60 | -------------------------------------------------------------------------------- /pkg/framework/logger.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "go.uber.org/fx/fxevent" 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | "go.uber.org/zap/zaptest" 14 | 15 | gormlogger "gorm.io/gorm/logger" 16 | ) 17 | 18 | var globalLog *Logger 19 | var zapLogger *zap.Logger 20 | 21 | // Logger structure 22 | type Logger struct { 23 | *zap.SugaredLogger 24 | } 25 | 26 | // GormLogger logger for gorm logging [subbed from main logger] 27 | type GormLogger struct { 28 | *Logger 29 | gormlogger.Config 30 | } 31 | 32 | // FxLogger logger for go-fx [subbed from main logger] 33 | type FxLogger struct { 34 | *Logger 35 | } 36 | 37 | // GinLogger logger for gin framework [subbed from main logger] 38 | type GinLogger struct { 39 | *Logger 40 | } 41 | 42 | // GetLogger gets the global instance of the logger 43 | func GetLogger() Logger { 44 | if globalLog != nil { 45 | return *globalLog 46 | } 47 | globalLog := newLogger() 48 | return *globalLog 49 | } 50 | 51 | // newLogger sets up logger the main logger 52 | func newLogger() *Logger { 53 | 54 | env := os.Getenv("ENVIRONMENT") 55 | logLevel := os.Getenv("LOG_LEVEL") 56 | 57 | config := zap.NewDevelopmentConfig() 58 | 59 | if env == "local" { 60 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 61 | } 62 | 63 | var level zapcore.Level 64 | switch logLevel { 65 | case "debug": 66 | level = zapcore.DebugLevel 67 | case "info": 68 | level = zapcore.InfoLevel 69 | case "warn": 70 | level = zapcore.WarnLevel 71 | case "error": 72 | level = zapcore.ErrorLevel 73 | case "fatal": 74 | level = zapcore.FatalLevel 75 | default: 76 | level = zap.PanicLevel 77 | } 78 | config.Level.SetLevel(level) 79 | zapLogger, _ = config.Build() 80 | 81 | globalLog := zapLogger.Sugar() 82 | 83 | return &Logger{ 84 | SugaredLogger: globalLog, 85 | } 86 | 87 | } 88 | 89 | func newSugaredLogger(logger *zap.Logger) *Logger { 90 | return &Logger{ 91 | SugaredLogger: logger.Sugar(), 92 | } 93 | } 94 | 95 | // GetGormLogger build gorm logger from zap logger (sub-logger) 96 | func (l *Logger) GetGormLogger() gormlogger.Interface { 97 | 98 | logger := zapLogger.WithOptions( 99 | zap.AddCaller(), 100 | zap.AddCallerSkip(3), 101 | ) 102 | 103 | return &GormLogger{ 104 | Logger: newSugaredLogger(logger), 105 | Config: gormlogger.Config{ 106 | LogLevel: gormlogger.Info, 107 | }, 108 | } 109 | } 110 | 111 | // GetFxLogger gets logger for go-fx 112 | func (l *Logger) GetFxLogger() fxevent.Logger { 113 | logger := zapLogger.WithOptions( 114 | zap.WithCaller(false), 115 | ) 116 | return &FxLogger{Logger: newSugaredLogger(logger)} 117 | } 118 | 119 | func (l *FxLogger) LogEvent(event fxevent.Event) { 120 | switch e := event.(type) { 121 | case *fxevent.OnStartExecuting: 122 | l.Logger.Debug("OnStart hook executing: ", 123 | zap.String("callee", e.FunctionName), 124 | zap.String("caller", e.CallerName), 125 | ) 126 | case *fxevent.OnStartExecuted: 127 | if e.Err != nil { 128 | l.Logger.Debug("OnStart hook failed: ", 129 | zap.String("callee", e.FunctionName), 130 | zap.String("caller", e.CallerName), 131 | zap.Error(e.Err), 132 | ) 133 | } else { 134 | l.Logger.Debug("OnStart hook executed: ", 135 | zap.String("callee", e.FunctionName), 136 | zap.String("caller", e.CallerName), 137 | zap.String("runtime", e.Runtime.String()), 138 | ) 139 | } 140 | case *fxevent.OnStopExecuting: 141 | l.Logger.Debug("OnStop hook executing: ", 142 | zap.String("callee", e.FunctionName), 143 | zap.String("caller", e.CallerName), 144 | ) 145 | case *fxevent.OnStopExecuted: 146 | if e.Err != nil { 147 | l.Logger.Debug("OnStop hook failed: ", 148 | zap.String("callee", e.FunctionName), 149 | zap.String("caller", e.CallerName), 150 | zap.Error(e.Err), 151 | ) 152 | } else { 153 | l.Logger.Debug("OnStop hook executed: ", 154 | zap.String("callee", e.FunctionName), 155 | zap.String("caller", e.CallerName), 156 | zap.String("runtime", e.Runtime.String()), 157 | ) 158 | } 159 | case *fxevent.Supplied: 160 | l.Logger.Debug("supplied: ", zap.String("type", e.TypeName), zap.Error(e.Err)) 161 | case *fxevent.Provided: 162 | for _, rtype := range e.OutputTypeNames { 163 | l.Logger.Debug("provided: ", e.ConstructorName, " => ", rtype) 164 | } 165 | case *fxevent.Decorated: 166 | for _, rtype := range e.OutputTypeNames { 167 | l.Logger.Debug("decorated: ", 168 | zap.String("decorator", e.DecoratorName), 169 | zap.String("type", rtype), 170 | ) 171 | } 172 | case *fxevent.Invoking: 173 | l.Logger.Debug("invoking: ", e.FunctionName) 174 | case *fxevent.Started: 175 | if e.Err == nil { 176 | l.Logger.Debug("started") 177 | } 178 | case *fxevent.LoggerInitialized: 179 | if e.Err == nil { 180 | l.Logger.Debug("initialized: custom fxevent.Logger -> ", e.ConstructorName) 181 | } 182 | } 183 | } 184 | 185 | // GetGinLogger gets logger for gin framework debugging 186 | func (l *Logger) GetGinLogger() io.Writer { 187 | logger := zapLogger.WithOptions( 188 | zap.WithCaller(false), 189 | ) 190 | return GinLogger{ 191 | Logger: newSugaredLogger(logger), 192 | } 193 | } 194 | 195 | // ------ GORM logger interface implementation ----- 196 | 197 | // LogMode set log mode 198 | func (l *GormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface { 199 | newlogger := *l 200 | newlogger.LogLevel = level 201 | return &newlogger 202 | } 203 | 204 | // Info prints info 205 | func (l GormLogger) Info(_ context.Context, str string, args ...any) { 206 | if l.LogLevel >= gormlogger.Info { 207 | l.Debugf(str, args...) 208 | } 209 | } 210 | 211 | // Warn prints warn messages 212 | func (l GormLogger) Warn(_ context.Context, str string, args ...any) { 213 | if l.LogLevel >= gormlogger.Warn { 214 | l.Warnf(str, args...) 215 | } 216 | 217 | } 218 | 219 | // Error prints error messages 220 | func (l GormLogger) Error(_ context.Context, str string, args ...any) { 221 | if l.LogLevel >= gormlogger.Error { 222 | l.Errorf(str, args...) 223 | } 224 | } 225 | 226 | // Trace prints trace messages 227 | func (l GormLogger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), _ error) { 228 | if l.LogLevel <= 0 { 229 | return 230 | } 231 | elapsed := time.Since(begin) 232 | if l.LogLevel >= gormlogger.Info { 233 | sql, rows := fc() 234 | l.Debug("[", elapsed.Milliseconds(), " ms, ", rows, " rows] ", "sql -> ", sql) 235 | return 236 | } 237 | 238 | if l.LogLevel >= gormlogger.Warn { 239 | sql, rows := fc() 240 | l.SugaredLogger.Warn("[", elapsed.Milliseconds(), " ms, ", rows, " rows] ", "sql -> ", sql) 241 | return 242 | } 243 | 244 | if l.LogLevel >= gormlogger.Error { 245 | sql, rows := fc() 246 | l.SugaredLogger.Error("[", elapsed.Milliseconds(), " ms, ", rows, " rows] ", "sql -> ", sql) 247 | return 248 | } 249 | } 250 | 251 | // Printf prints go-fx logs 252 | func (l FxLogger) Printf(str string, args ...any) { 253 | if len(args) > 0 { 254 | l.Debugf(str, args) 255 | } 256 | l.Debug(str) 257 | } 258 | 259 | // Writer interface implementation for gin-framework 260 | func (l GinLogger) Write(p []byte) (n int, err error) { 261 | l.Info(string(p)) 262 | return len(p), nil 263 | } 264 | 265 | func CreateTestLogger(t *testing.T) Logger { 266 | zapLogger := zaptest.NewLogger(t) 267 | 268 | sugaredLogger := zapLogger.Sugar() 269 | 270 | return Logger{SugaredLogger: sugaredLogger} 271 | } 272 | -------------------------------------------------------------------------------- /pkg/infrastructure/aws.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "context" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/aws-sdk-go-v2/credentials" 11 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 12 | "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider" 13 | "github.com/aws/aws-sdk-go-v2/service/s3" 14 | "github.com/aws/aws-sdk-go-v2/service/sesv2" 15 | ) 16 | 17 | // NewAWSConfig create a new aws config 18 | func NewAWSConfig( 19 | env *framework.Env, 20 | ) aws.Config { 21 | c := aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider( 22 | env.AWSAccessKey, env.AWSSecretAccessKey, ""), 23 | ) 24 | conf, _ := config.LoadDefaultConfig( 25 | context.Background(), 26 | config.WithRegion(env.AWSRegion), 27 | config.WithCredentialsProvider(c), 28 | config.WithClientLogMode(aws.LogRetries), 29 | ) 30 | 31 | return conf 32 | } 33 | 34 | func NewCognitoClient(cfg aws.Config) *cognitoidentityprovider.Client { 35 | return cognitoidentityprovider.NewFromConfig(cfg) 36 | } 37 | 38 | func NewSESClient(cfg aws.Config) *sesv2.Client { 39 | return sesv2.NewFromConfig(cfg) 40 | } 41 | 42 | func NewS3Uploader(client *s3.Client) *manager.Uploader { 43 | return manager.NewUploader(client) 44 | } 45 | 46 | // NewPresignClient new presign client 47 | func NewPresignClient(client *s3.Client) *s3.PresignClient { 48 | return s3.NewPresignClient(client, s3.WithPresignExpires(15*time.Minute)) 49 | } 50 | 51 | func NewS3Client(cfg aws.Config) *s3.Client { 52 | return s3.NewFromConfig(cfg) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/infrastructure/db.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "fmt" 6 | "time" 7 | 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // Database modal 13 | type Database struct { 14 | *gorm.DB 15 | } 16 | 17 | // NewDatabase creates a new database instance 18 | func NewDatabase(logger framework.Logger, env *framework.Env) Database { 19 | url := fmt.Sprintf("%s:%s@tcp(%s:%s)/?charset=utf8mb4&parseTime=True&loc=Local", env.DBUsername, env.DBPassword, env.DBHost, env.DBPort) 20 | 21 | logger.Info("opening db connection") 22 | db, err := gorm.Open(mysql.Open(url), &gorm.Config{Logger: logger.GetGormLogger()}) 23 | if err != nil { 24 | logger.Panic(err) 25 | } 26 | 27 | logger.Info("creating database if it doesn't exist") 28 | if err = db.Exec("CREATE DATABASE IF NOT EXISTS " + env.DBName).Error; err != nil { 29 | logger.Info("couldn't create database") 30 | logger.Panic(err) 31 | } 32 | 33 | // close the current connection 34 | sqlDb, err := db.DB() 35 | if err != nil { 36 | logger.Panic(err) 37 | } 38 | if dbErr := sqlDb.Close(); dbErr != nil { 39 | logger.Panic(err) 40 | } 41 | 42 | // reopen connection with the given database, after creating or checking if the database exists 43 | logger.Info("using given database") 44 | urlWithDB := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", env.DBUsername, env.DBPassword, env.DBHost, env.DBPort, env.DBName) 45 | db, err = gorm.Open(mysql.Open(urlWithDB), &gorm.Config{Logger: logger.GetGormLogger()}) 46 | if err != nil { 47 | logger.Panic(err) 48 | } 49 | 50 | conn, err := db.DB() 51 | if err != nil { 52 | logger.Info("couldn't get db connection") 53 | logger.Panic(err) 54 | } 55 | 56 | conn.SetConnMaxLifetime(time.Minute * 5) 57 | conn.SetMaxOpenConns(5) 58 | conn.SetMaxIdleConns(1) 59 | 60 | return Database{DB: db} 61 | } 62 | -------------------------------------------------------------------------------- /pkg/infrastructure/module.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import "go.uber.org/fx" 4 | 5 | // Module exports dependency 6 | var Module = fx.Options( 7 | fx.Provide( 8 | NewRouter, 9 | NewDatabase, 10 | //NewS3Client, 11 | //NewAWSConfig, 12 | //NewPresignClient, 13 | //NewS3Uploader, 14 | //NewCognitoClient, 15 | //NewSESClient, 16 | ), 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/infrastructure/router.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "net/http" 6 | 7 | sentrygin "github.com/getsentry/sentry-go/gin" 8 | "github.com/gin-contrib/cors" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // Router -> Gin Router 13 | type Router struct { 14 | *gin.Engine 15 | } 16 | 17 | // NewRouter : all the routes are defined here 18 | func NewRouter( 19 | env *framework.Env, 20 | logger framework.Logger, 21 | ) Router { 22 | 23 | gin.DefaultWriter = logger.GetGinLogger() 24 | appEnv := env.Environment 25 | if appEnv == "production" { 26 | gin.SetMode(gin.ReleaseMode) 27 | } else { 28 | gin.SetMode(gin.DebugMode) 29 | } 30 | 31 | httpRouter := gin.Default() 32 | 33 | httpRouter.MaxMultipartMemory = env.MaxMultipartMemory 34 | 35 | httpRouter.Use(cors.New(cors.Config{ 36 | AllowOrigins: []string{"*"}, 37 | AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "OPTIONS", "DELETE"}, 38 | AllowHeaders: []string{"*"}, 39 | AllowCredentials: true, 40 | })) 41 | 42 | // Attach sentry middleware 43 | httpRouter.Use(sentrygin.New(sentrygin.Options{ 44 | Repanic: true, 45 | })) 46 | 47 | httpRouter.GET("/health-check", func(c *gin.Context) { 48 | c.JSON(http.StatusOK, gin.H{"data": "clean architecture 📺 API Up and Running"}) 49 | }) 50 | 51 | return Router{ 52 | httpRouter, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/middlewares/auth_middleware.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // AuthMiddleware interface has basic methods to authenticate and autorize user 8 | // Can be updated as per project requirements 9 | type AuthMiddleware interface { 10 | HandleAuthWithRole(roles ...string) gin.HandlerFunc 11 | } 12 | 13 | type CognitoMiddleWare interface { 14 | Handle() gin.HandlerFunc 15 | } 16 | -------------------------------------------------------------------------------- /pkg/middlewares/cognito_middleware.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "clean-architecture/pkg/errorz" 5 | "clean-architecture/pkg/framework" 6 | "clean-architecture/pkg/responses" 7 | "clean-architecture/pkg/services" 8 | "fmt" 9 | "net/http" 10 | 11 | "strings" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/lestrrat-go/jwx/jwt" 15 | ) 16 | 17 | type CognitoAuthMiddleware struct { 18 | service services.CognitoAuthService 19 | } 20 | 21 | func NewCognitoAuthMiddleware(service services.CognitoAuthService) CognitoAuthMiddleware { 22 | return CognitoAuthMiddleware{ 23 | service: service, 24 | } 25 | } 26 | 27 | func (am CognitoAuthMiddleware) Handle() gin.HandlerFunc { 28 | return func(ctx *gin.Context) { 29 | if err := am.addClaimsToContext(ctx); err != nil { 30 | responses.ErrorJSON(ctx, http.StatusUnauthorized, err.Error()) 31 | ctx.Abort() 32 | return 33 | } 34 | } 35 | } 36 | 37 | func (am CognitoAuthMiddleware) getTokenFromHeader(ctx *gin.Context) (jwt.Token, error) { 38 | header := ctx.GetHeader("Authorization") 39 | idToken := strings.TrimSpace(strings.Replace(header, "Bearer", "", 1)) 40 | token, err := am.service.VerifyToken(idToken) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return token, nil 45 | } 46 | 47 | func (am CognitoAuthMiddleware) addClaimsToContext(ctx *gin.Context) error { 48 | token, err := am.getTokenFromHeader(ctx) 49 | if err != nil { 50 | return errorz.ErrUnauthorizedAccess 51 | } 52 | 53 | claims := token.PrivateClaims() 54 | username := claims["cognito:username"] 55 | authCogUser, err := am.service.GetUserByUsername(fmt.Sprint(username)) 56 | if err != nil { 57 | return err 58 | } 59 | if !authCogUser.Enabled { 60 | return errorz.ErrUnauthorizedAccess 61 | } 62 | 63 | ctx.Set(framework.Claims, claims) 64 | ctx.Set(framework.UID, username) 65 | 66 | role, ok := claims["custom:role"] 67 | if ok { 68 | ctx.Set(framework.Role, role) 69 | } 70 | ctx.Set(framework.CognitoPass, true) 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/middlewares/middlewares.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import "go.uber.org/fx" 4 | 5 | // Module Middleware exported 6 | var Module = fx.Options( 7 | fx.Provide( 8 | NewUploadMiddleware, 9 | NewRateLimitMiddleware, 10 | NewMiddlewares, 11 | NewCognitoAuthMiddleware, 12 | ), 13 | ) 14 | 15 | // IMiddleware middleware interface 16 | type IMiddleware interface { 17 | Setup() 18 | } 19 | 20 | // Middlewares contains multiple middleware 21 | type Middlewares []IMiddleware 22 | 23 | // NewMiddlewares creates new middlewares 24 | // Register the middleware that should be applied directly (globally) 25 | func NewMiddlewares() Middlewares { 26 | return Middlewares{} 27 | } 28 | 29 | // Setup sets up middlewares 30 | func (m Middlewares) Setup() { 31 | for _, middleware := range m { 32 | middleware.Setup() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/middlewares/rate_limit_middleware.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/ulule/limiter/v3" 11 | "github.com/ulule/limiter/v3/drivers/store/memory" 12 | ) 13 | 14 | // Global store 15 | // using in-memory store with goroutine which clears expired keys. 16 | var store = memory.NewStore() 17 | 18 | type RateLimitOption struct { 19 | period time.Duration 20 | limit int64 21 | } 22 | 23 | const ( 24 | RateLimitPeriod = 15 * time.Minute 25 | RateLimitRequests = int64(200) 26 | ) 27 | 28 | type Option func(*RateLimitOption) 29 | 30 | type RateLimitMiddleware struct { 31 | logger framework.Logger 32 | option RateLimitOption 33 | } 34 | 35 | func NewRateLimitMiddleware(logger framework.Logger) RateLimitMiddleware { 36 | return RateLimitMiddleware{ 37 | logger: logger, 38 | option: RateLimitOption{ 39 | period: RateLimitPeriod, 40 | limit: RateLimitRequests, 41 | }, 42 | } 43 | } 44 | 45 | func (lm RateLimitMiddleware) Handle(options ...Option) gin.HandlerFunc { 46 | return func(c *gin.Context) { 47 | key := c.ClientIP() // Gets cient IP Address 48 | 49 | lm.logger.Info("Setting up rate limit middleware") 50 | 51 | // Setting up rate limit 52 | // Limit -> # of API Calls 53 | // Period -> in a given time frame 54 | // setting default values 55 | opt := RateLimitOption{ 56 | period: lm.option.period, 57 | limit: lm.option.limit, 58 | } 59 | 60 | for _, o := range options { 61 | o(&opt) 62 | } 63 | 64 | rate := limiter.Rate{ 65 | Limit: opt.limit, 66 | Period: opt.period, 67 | } 68 | 69 | // Limiter instance 70 | instance := limiter.New(store, rate) 71 | 72 | // Returns the rate limit details for given identifier. 73 | // FullPath is appended with IP address. `/api/users&&127.0.0.1` as key 74 | context, err := instance.Get(c, c.FullPath()+"&&"+key) 75 | 76 | if err != nil { 77 | lm.logger.Panic(err.Error()) 78 | } 79 | 80 | c.Set(framework.RateLimit, instance) 81 | 82 | // Setting custom headers 83 | c.Header("X-RateLimit-Limit", strconv.FormatInt(context.Limit, 10)) 84 | c.Header("X-RateLimit-Remaining", strconv.FormatInt(context.Remaining, 10)) 85 | c.Header("X-RateLimit-Reset", strconv.FormatInt(context.Reset, 10)) 86 | 87 | // Limit exceeded 88 | if context.Reached { 89 | c.JSON(http.StatusTooManyRequests, gin.H{ 90 | "message": "Rate Limit Exceed", 91 | }) 92 | c.Abort() 93 | return 94 | } 95 | 96 | c.Next() 97 | } 98 | } 99 | 100 | func WithOptions(period time.Duration, limit int64) Option { 101 | return func(o *RateLimitOption) { 102 | o.period = period 103 | o.limit = limit 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/middlewares/upload_middleware.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "bytes" 5 | "clean-architecture/pkg/errorz" 6 | "clean-architecture/pkg/framework" 7 | "clean-architecture/pkg/responses" 8 | "clean-architecture/pkg/services" 9 | "clean-architecture/pkg/types" 10 | "context" 11 | "errors" 12 | "fmt" 13 | "image" 14 | "image/jpeg" 15 | "image/png" 16 | "io" 17 | "mime/multipart" 18 | "net/http" 19 | "path/filepath" 20 | "strings" 21 | 22 | "github.com/chai2010/webp" 23 | "github.com/gin-gonic/gin" 24 | "github.com/google/uuid" 25 | "github.com/nfnt/resize" 26 | "golang.org/x/sync/errgroup" 27 | ) 28 | 29 | type Extension string 30 | 31 | const ( 32 | JPEGFile Extension = ".jpeg" 33 | JPGFile Extension = ".jpg" 34 | PNGFile Extension = ".png" 35 | ) 36 | 37 | type UploadConfig struct { 38 | // FieldName where to pull multipart files from 39 | FieldName string 40 | 41 | // Extensions array of extensions 42 | Extensions []Extension 43 | 44 | // ThumbnailEnabled set whether thumbnail is enabled or nor 45 | ThumbnailEnabled bool 46 | 47 | // ThumbnailWidth set thumbnail width 48 | ThumbnailWidth uint 49 | 50 | // WebpEnabled set whether thumbnail is enabled or nor 51 | WebpEnabled bool 52 | 53 | // Multiple set whether to upload multiple files with same key name 54 | Multiple bool 55 | } 56 | 57 | type UploadMiddleware struct { 58 | logger framework.Logger 59 | bucket services.S3Service 60 | config []UploadConfig 61 | } 62 | 63 | func NewUploadMiddleware( 64 | logger framework.Logger, 65 | bucket services.S3Service, 66 | ) UploadMiddleware { 67 | m := UploadMiddleware{ 68 | bucket: bucket, 69 | logger: logger, 70 | } 71 | return m 72 | } 73 | 74 | func (u UploadMiddleware) Config() UploadConfig { 75 | return UploadConfig{ 76 | FieldName: "file", 77 | Extensions: []Extension{JPEGFile, PNGFile, JPGFile}, 78 | ThumbnailEnabled: false, 79 | ThumbnailWidth: 100, 80 | Multiple: false, 81 | } 82 | } 83 | 84 | // Field modify field of upload 85 | func (cfg UploadConfig) Field(name string) UploadConfig { 86 | cfg.FieldName = name 87 | return cfg 88 | } 89 | 90 | // Extension modify upload extension 91 | func (cfg UploadConfig) Extension(ext ...Extension) UploadConfig { 92 | cfg.Extensions = ext 93 | return cfg 94 | } 95 | 96 | // ThumbEnable enable thumbnail generation 97 | func (cfg UploadConfig) ThumbEnable(enable bool) UploadConfig { 98 | cfg.ThumbnailEnabled = enable 99 | return cfg 100 | } 101 | 102 | // WEBpEnabled enable thumbnail generation 103 | func (cfg UploadConfig) WebpEnable(enable bool) UploadConfig { 104 | cfg.WebpEnabled = enable 105 | return cfg 106 | } 107 | 108 | // MultipleFilesUpload enable multiple files to be uploaded with same key name 109 | func (cfg UploadConfig) MultipleFilesUpload(enable bool) UploadConfig { 110 | cfg.Multiple = enable 111 | return cfg 112 | } 113 | 114 | // Push adds file upload configuration 115 | func (u *UploadMiddleware) Push(config UploadConfig) UploadMiddleware { 116 | u.config = append(u.config, config) 117 | return *u 118 | } 119 | 120 | // Handle handles file upload 121 | func (u UploadMiddleware) Handle() gin.HandlerFunc { 122 | return func(c *gin.Context) { 123 | 124 | if len(u.config) == 0 { 125 | u.logger.Info("no file upload configuration has been attached") 126 | } 127 | errGroup, ctx := errgroup.WithContext(c.Request.Context()) 128 | 129 | var uploadedFiles []types.UploadMetadata 130 | 131 | for _, conf := range u.config { 132 | if conf.Multiple { 133 | form, _ := c.MultipartForm() 134 | files := form.File[conf.FieldName] 135 | for _, fileHeader := range files { 136 | file, err := fileHeader.Open() 137 | if err != nil { 138 | responses.ErrorJSON(c, http.StatusInternalServerError, err) 139 | c.Abort() 140 | return 141 | } 142 | defer file.Close() //nolint 143 | 144 | err = u.uploadFile(ctx, errGroup, conf, file, fileHeader, &uploadedFiles) 145 | if err != nil { 146 | u.logger.Error("file-upload-error: ", err.Error()) 147 | responses.ErrorJSON(c, http.StatusInternalServerError, err.Error()) 148 | c.Abort() 149 | return 150 | } 151 | } 152 | } else { 153 | file, fileHeader, _ := c.Request.FormFile(conf.FieldName) 154 | err := u.uploadFile(ctx, errGroup, conf, file, fileHeader, &uploadedFiles) 155 | if err != nil { 156 | u.logger.Error("file-upload-error: ", err.Error()) 157 | responses.ErrorJSON(c, http.StatusInternalServerError, err.Error()) 158 | c.Abort() 159 | return 160 | } 161 | } 162 | 163 | } 164 | if err := errGroup.Wait(); err != nil { 165 | u.logger.Error("file-upload-error: ", err.Error()) 166 | if errors.Is(err, errorz.ErrThumbExtensionMismatch) { 167 | responses.ErrorJSON(c, http.StatusBadRequest, err) 168 | } else { 169 | responses.ErrorJSON(c, http.StatusInternalServerError, err) 170 | } 171 | c.Abort() 172 | return 173 | } 174 | 175 | c.Set(framework.File, types.UploadedFiles(uploadedFiles)) 176 | c.Next() 177 | 178 | } 179 | } 180 | 181 | func (u UploadMiddleware) uploadFile( 182 | ctx context.Context, 183 | errGroup *errgroup.Group, 184 | conf UploadConfig, 185 | file multipart.File, 186 | fileHeader *multipart.FileHeader, 187 | uploadedFiles *[]types.UploadMetadata, 188 | ) error { 189 | 190 | if file == nil || fileHeader == nil { 191 | u.logger.Info("file and fileheader nil value is passed") 192 | return nil 193 | } 194 | 195 | ext := strings.ToLower(filepath.Ext(fileHeader.Filename)) 196 | if !u.matchesExtension(conf, ext) { 197 | return errorz.ErrExtensionMismatch 198 | } 199 | 200 | fileByte, err := io.ReadAll(file) 201 | if err != nil { 202 | return errorz.ErrFileRead 203 | } 204 | 205 | uploadFileName, fileUID := u.randomFileName(ext) 206 | fileReader := bytes.NewReader(fileByte) 207 | errGroup.Go(func() error { 208 | urlResponse, err := u.bucket.UploadFile(ctx, fileReader, uploadFileName) 209 | *uploadedFiles = append(*uploadedFiles, types.UploadMetadata{ 210 | FieldName: conf.FieldName, 211 | FileName: fileHeader.Filename, 212 | URL: urlResponse, 213 | FileUID: fileUID, 214 | Size: fileHeader.Size, 215 | }) 216 | return err 217 | }) 218 | 219 | // original image 220 | if conf.WebpEnabled && u.properExtension(ext) { 221 | origWebpReader := bytes.NewReader(fileByte) 222 | errGroup.Go(func() error { 223 | var webpBuf bytes.Buffer 224 | img, err := u.getImage(origWebpReader, ext) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | if err := webp.Encode(&webpBuf, img, &webp.Options{Lossless: true}); err != nil { 230 | return err 231 | } 232 | 233 | webpReader := bytes.NewReader(webpBuf.Bytes()) 234 | resizeFileName := fmt.Sprintf("%s_webp%s", fileUID, ext) 235 | 236 | if _, err := u.bucket.UploadFile(ctx, webpReader, resizeFileName); err != nil { 237 | return err 238 | } 239 | 240 | return nil 241 | }) 242 | } 243 | 244 | if conf.ThumbnailEnabled { 245 | thumbReader := bytes.NewReader(fileByte) 246 | errGroup.Go(func() error { 247 | if !u.properExtension(ext) { 248 | return errorz.ErrExtensionMismatch 249 | } 250 | // Genrate non-webp thumbnail 251 | img, err := u.createThumbnail(conf, thumbReader, ext) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | resizeFileName := fmt.Sprintf("%s_thumb%s", fileUID, ext) 257 | _, err = u.bucket.UploadFile(ctx, img, resizeFileName) 258 | if err != nil { 259 | return err 260 | } 261 | return nil 262 | }) 263 | 264 | if conf.WebpEnabled && u.properExtension(ext) { 265 | webpReader := bytes.NewReader(fileByte) 266 | errGroup.Go(func() error { 267 | var webpBuf bytes.Buffer 268 | img, err := u.getImage(webpReader, ext) 269 | if err != nil { 270 | return err 271 | } 272 | 273 | resizeImage := resize.Resize(conf.ThumbnailWidth, 0, img, resize.Lanczos3) 274 | err = webp.Encode(&webpBuf, resizeImage, &webp.Options{Lossless: true}) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | webpReader := bytes.NewReader(webpBuf.Bytes()) 280 | resizeFileName := fmt.Sprintf("%s_thumb%s", fileUID, ".webp") 281 | 282 | _, err = u.bucket.UploadFile(ctx, webpReader, resizeFileName) 283 | if err != nil { 284 | return err 285 | } 286 | 287 | return nil 288 | }) 289 | } 290 | } 291 | return nil 292 | } 293 | 294 | func (u *UploadMiddleware) properExtension(ext string) bool { 295 | e := Extension(ext) 296 | return e == JPEGFile || e == JPGFile || e == PNGFile 297 | } 298 | 299 | func (u *UploadMiddleware) matchesExtension(c UploadConfig, ext string) bool { 300 | for _, e := range c.Extensions { 301 | if e == Extension(ext) { 302 | return true 303 | } 304 | } 305 | return false 306 | } 307 | 308 | func (u *UploadMiddleware) randomFileName(ext string) (randomName, uid string) { 309 | randUUID, _ := uuid.NewRandom() 310 | fileName := randUUID.String() + ext 311 | return fileName, randUUID.String() 312 | } 313 | 314 | func (u *UploadMiddleware) getImage(file io.Reader, ext string) (image.Image, error) { 315 | if Extension(ext) == JPGFile || Extension(ext) == JPEGFile { 316 | return jpeg.Decode(file) 317 | } 318 | if Extension(ext) == PNGFile { 319 | return png.Decode(file) 320 | } 321 | return nil, errorz.ErrExtensionMismatch 322 | } 323 | 324 | // createThumbnail creates thumbnail from multipart file 325 | func (u UploadMiddleware) createThumbnail(c UploadConfig, file io.Reader, ext string) (*bytes.Buffer, error) { 326 | img, err := u.getImage(file, ext) 327 | if err != nil { 328 | return nil, err 329 | } 330 | 331 | resizeImage := resize.Resize(c.ThumbnailWidth, 0, img, resize.Lanczos3) 332 | buff := new(bytes.Buffer) 333 | if Extension(ext) == JPGFile || Extension(ext) == JPEGFile { 334 | if err := jpeg.Encode(buff, resizeImage, nil); err != nil { 335 | return nil, err 336 | } 337 | } 338 | if Extension(ext) == PNGFile { 339 | if err := png.Encode(buff, resizeImage); err != nil { 340 | return nil, err 341 | } 342 | } 343 | 344 | return buff, nil 345 | } 346 | -------------------------------------------------------------------------------- /pkg/module.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "clean-architecture/pkg/infrastructure" 6 | "clean-architecture/pkg/middlewares" 7 | "clean-architecture/pkg/services" 8 | 9 | "go.uber.org/fx" 10 | ) 11 | 12 | var Module = fx.Module("pkg", 13 | fx.Options( 14 | fx.Provide( 15 | framework.NewEnv, 16 | framework.GetLogger, 17 | ), 18 | ), 19 | services.Module, 20 | infrastructure.Module, 21 | middlewares.Module, 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/responses/handle_errors.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "clean-architecture/pkg/errorz" 5 | "clean-architecture/pkg/framework" 6 | "clean-architecture/pkg/utils" 7 | "errors" 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func HandleValidationError(logger framework.Logger, c *gin.Context, err error) { 15 | logger.Error(err) 16 | c.JSON(http.StatusBadRequest, gin.H{ 17 | "error": err.Error(), 18 | }) 19 | } 20 | 21 | func HandleErrorWithStatus(logger framework.Logger, c *gin.Context, statusCode int, err error) { 22 | logger.Error(err) 23 | c.JSON(statusCode, gin.H{ 24 | "error": err.Error(), 25 | }) 26 | } 27 | 28 | func HandleError(logger framework.Logger, c *gin.Context, err error) { 29 | msgForUnhandledError := "An error occurred while processing your request. Please try again later." 30 | 31 | var apiErr *errorz.APIError 32 | msg := err.Error() 33 | if ok := errors.As(err, &apiErr); ok { 34 | if msg == "" { 35 | msg = apiErr.Message 36 | } 37 | c.JSON(apiErr.StatusCode, gin.H{ 38 | "error": msg, 39 | }) 40 | return 41 | } 42 | 43 | if errors.Is(err, gorm.ErrRecordNotFound) { 44 | c.JSON(http.StatusNotFound, gin.H{ 45 | "error": gorm.ErrRecordNotFound.Error(), 46 | }) 47 | return 48 | } 49 | 50 | c.JSON(http.StatusInternalServerError, gin.H{ 51 | "error": msgForUnhandledError, 52 | }) 53 | 54 | utils.CurrentSentryService.CaptureException(err) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/responses/handle_errors_test.go: -------------------------------------------------------------------------------- 1 | package responses_test 2 | 3 | import ( 4 | "clean-architecture/pkg/errorz" 5 | "clean-architecture/pkg/framework" 6 | "clean-architecture/pkg/responses" 7 | "clean-architecture/pkg/utils" 8 | "errors" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/stretchr/testify/assert" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | type MockLogger struct { 19 | Errors []error 20 | } 21 | 22 | func (m *MockLogger) Error(err error) { 23 | m.Errors = append(m.Errors, err) 24 | } 25 | 26 | func TestHandleError(t *testing.T) { 27 | testCases := []struct { 28 | name string 29 | err error 30 | expectedStatusCode int 31 | expectedBody string 32 | expectSentryCapture bool 33 | }{ 34 | { 35 | name: "Handle API Error", 36 | err: errorz.ErrBadRequest, 37 | expectedStatusCode: http.StatusBadRequest, 38 | expectedBody: `{"error":"Bad Request"}`, 39 | expectSentryCapture: false, 40 | }, 41 | { 42 | name: "Handle API Error With Custom Message", 43 | err: errorz.ErrBadRequest.JoinError("Bad Request"), 44 | expectedStatusCode: http.StatusBadRequest, 45 | expectedBody: `{"error":"Bad Request"}`, 46 | expectSentryCapture: false, 47 | }, 48 | { 49 | name: "Handle Already Exists API Error", 50 | err: errorz.ErrAlreadyExists, 51 | expectedStatusCode: http.StatusConflict, 52 | expectedBody: `{"error":"Already Exists"}`, 53 | expectSentryCapture: false, 54 | }, 55 | { 56 | name: "Handle Record Not Found Error", 57 | err: gorm.ErrRecordNotFound, 58 | expectedStatusCode: http.StatusNotFound, 59 | expectedBody: `{"error":"record not found"}`, 60 | expectSentryCapture: false, 61 | }, 62 | { 63 | name: "Handle Generic Error", 64 | err: errors.New("something went wrong"), 65 | expectedStatusCode: http.StatusInternalServerError, 66 | expectedBody: `{"error":"An error occurred while processing your request. Please try again later."}`, 67 | expectSentryCapture: true, 68 | }, 69 | } 70 | 71 | for _, tc := range testCases { 72 | t.Run(tc.name, func(t *testing.T) { 73 | mockService := &MockSentryService{} 74 | originalService := utils.CurrentSentryService 75 | utils.CurrentSentryService = mockService 76 | defer func() { 77 | utils.CurrentSentryService = originalService 78 | mockService.Reset() 79 | }() 80 | 81 | w := httptest.NewRecorder() 82 | c, _ := gin.CreateTestContext(w) 83 | c.Request, _ = http.NewRequest("POST", "/", nil) 84 | 85 | testLogger := framework.CreateTestLogger(t) 86 | 87 | responses.HandleError(testLogger, c, tc.err) 88 | assert.Equal(t, tc.expectedStatusCode, w.Code) 89 | assert.JSONEq(t, tc.expectedBody, w.Body.String()) 90 | 91 | if tc.expectSentryCapture { 92 | assert.True(t, mockService.WasCalled(), "Expected Sentry to capture the error") 93 | } else { 94 | assert.False(t, mockService.WasCalled(), "Sentry should not capture this error") 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/responses/mock_sentry_service_test.go: -------------------------------------------------------------------------------- 1 | package responses_test 2 | 3 | type MockSentryService struct { 4 | CapturedErrors []error 5 | CallCount int 6 | } 7 | 8 | func (m *MockSentryService) CaptureException(err error) { 9 | m.CapturedErrors = append(m.CapturedErrors, err) 10 | m.CallCount++ 11 | } 12 | 13 | func (m *MockSentryService) WasCalled() bool { 14 | return m.CallCount > 0 15 | } 16 | 17 | func (m *MockSentryService) Reset() { 18 | m.CapturedErrors = []error{} 19 | m.CallCount = 0 20 | } 21 | -------------------------------------------------------------------------------- /pkg/responses/response.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // JSON : json response function 10 | func JSON(c *gin.Context, statusCode int, data any) { 11 | c.JSON(statusCode, gin.H{"data": data}) 12 | } 13 | 14 | // ErrorJSON : json error response function 15 | func ErrorJSON(c *gin.Context, statusCode int, data any) { 16 | c.JSON(statusCode, gin.H{"error": data}) 17 | } 18 | 19 | // SuccessJSON : json error response function 20 | func SuccessJSON(c *gin.Context, statusCode int, data any) { 21 | c.JSON(statusCode, gin.H{"msg": data}) 22 | } 23 | 24 | // JSONWithPagination : json response function 25 | func JSONWithPagination(c *gin.Context, statusCode int, response map[string]any) { 26 | limit, _ := c.MustGet(framework.Limit).(int64) 27 | size, _ := c.MustGet(framework.Page).(int64) 28 | 29 | c.JSON( 30 | statusCode, 31 | gin.H{ 32 | "data": response["data"], 33 | "pagination": gin.H{"has_next": (response["count"].(int64) - limit*size) > 0, "count": response["count"]}, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/services/cognito.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "clean-architecture/domain/constants" 5 | "clean-architecture/pkg/framework" 6 | "clean-architecture/pkg/utils" 7 | 8 | "context" 9 | "strconv" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider" 13 | "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types" 14 | "github.com/lestrrat-go/jwx/jwk" 15 | "github.com/lestrrat-go/jwx/jwt" 16 | ) 17 | 18 | var jwkURL = "" 19 | var issuer = "" 20 | var keySet jwk.Set = jwk.NewSet() 21 | 22 | type CognitoAuthService struct { 23 | client *cognitoidentityprovider.Client 24 | env *framework.Env 25 | logger framework.Logger 26 | } 27 | 28 | func NewCognitoAuthService( 29 | client *cognitoidentityprovider.Client, 30 | env *framework.Env, 31 | logger framework.Logger, 32 | ) CognitoAuthService { 33 | 34 | issuer = "https://cognito-idp." + env.AWSRegion + ".amazonaws.com/" + env.UserPoolID 35 | jwkURL = issuer + "/.well-known/jwks.json" 36 | 37 | keySet, _ = jwk.Fetch(context.Background(), jwkURL) 38 | 39 | return CognitoAuthService{ 40 | client: client, 41 | env: env, 42 | logger: logger, 43 | } 44 | } 45 | 46 | func (cg *CognitoAuthService) VerifyToken(tokenString string) (jwt.Token, error) { 47 | parsedToken, err := jwt.Parse( 48 | []byte(tokenString), 49 | jwt.WithKeySet(keySet), 50 | jwt.WithValidate(true), 51 | jwt.WithIssuer(issuer), 52 | ) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return parsedToken, nil 58 | } 59 | 60 | func (cg *CognitoAuthService) CreateUser(email, password, role string) (string, error) { 61 | _, err := cg.client.AdminCreateUser(context.Background(), &cognitoidentityprovider.AdminCreateUserInput{ 62 | UserPoolId: &cg.env.UserPoolID, 63 | Username: &email, 64 | MessageAction: types.MessageActionTypeSuppress, 65 | UserAttributes: []types.AttributeType{ 66 | { 67 | Name: aws.String("email"), 68 | Value: aws.String(email), 69 | }, 70 | { 71 | Name: aws.String("email_verified"), 72 | Value: aws.String("true"), 73 | }, 74 | }, 75 | ValidationData: []types.AttributeType{}, 76 | }) 77 | 78 | if err != nil { 79 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 80 | return "", awsErr 81 | } 82 | return "", err 83 | } 84 | _, err = cg.client.AdminSetUserPassword(context.Background(), &cognitoidentityprovider.AdminSetUserPasswordInput{ 85 | 86 | Username: &email, 87 | Password: &password, 88 | Permanent: true, 89 | UserPoolId: &cg.env.UserPoolID, 90 | }) 91 | if err != nil { 92 | _, delErr := cg.client.AdminDeleteUser(context.Background(), &cognitoidentityprovider.AdminDeleteUserInput{Username: &email, UserPoolId: &cg.env.UserPoolID}) 93 | awsErr := utils.MapAWSError(cg.logger, delErr) 94 | if awsErr != nil { 95 | return "", awsErr 96 | } 97 | return "", utils.MapAWSError(cg.logger, err) 98 | } 99 | 100 | err = cg.setCustomClaimToOneUser(email, map[string]string{ 101 | "role": role, 102 | "change-password": strconv.FormatBool(false), 103 | }) 104 | if err != nil { 105 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 106 | return "", awsErr 107 | } 108 | return "", err 109 | } 110 | 111 | // fetching created user 112 | user, err := cg.GetUserByEmail(email) 113 | if err != nil { 114 | cg.logger.Error(err) 115 | return "", err 116 | } 117 | 118 | var cognitoUUID string 119 | // Access the user attributes tp find sub i.e cognito internal uuid 120 | for _, attr := range user.UserAttributes { 121 | if *attr.Name == "sub" { 122 | cognitoUUID = *attr.Value 123 | break 124 | } 125 | } 126 | 127 | return cognitoUUID, nil 128 | } 129 | 130 | func (cg *CognitoAuthService) setCustomClaimToOneUser(user string, c map[string]string) error { 131 | var claim = make([]types.AttributeType, 0, 5) 132 | var create = make([]types.SchemaAttributeType, 0, 5) 133 | 134 | developerOnly := false 135 | mutable := true 136 | required := false 137 | 138 | for key, val := range c { 139 | 140 | attribute := types.AttributeType{ 141 | Name: aws.String("custom:" + key), 142 | Value: aws.String(val), 143 | } 144 | 145 | schemaAttribute := types.SchemaAttributeType{ 146 | AttributeDataType: "String", 147 | DeveloperOnlyAttribute: &developerOnly, 148 | Mutable: &mutable, 149 | Name: aws.String(key), 150 | Required: &required, 151 | } 152 | claim = append(claim, attribute) 153 | create = append(create, schemaAttribute) 154 | } 155 | 156 | _, _ = cg.client.AddCustomAttributes(context.Background(), &cognitoidentityprovider.AddCustomAttributesInput{ 157 | CustomAttributes: create, 158 | UserPoolId: &cg.env.UserPoolID, 159 | }) 160 | 161 | _, err := cg.client.AdminUpdateUserAttributes(context.Background(), &cognitoidentityprovider.AdminUpdateUserAttributesInput{ 162 | UserAttributes: claim, 163 | UserPoolId: &cg.env.UserPoolID, 164 | Username: &user, 165 | }) 166 | if err != nil { 167 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 168 | return awsErr 169 | } 170 | return err 171 | } 172 | 173 | return nil 174 | } 175 | 176 | func (cg *CognitoAuthService) GetUserByUsername(username string) (*cognitoidentityprovider.AdminGetUserOutput, error) { 177 | user, err := cg.client.AdminGetUser(context.Background(), &cognitoidentityprovider.AdminGetUserInput{ 178 | Username: &username, 179 | UserPoolId: &cg.env.UserPoolID, 180 | }) 181 | if err != nil { 182 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 183 | return nil, awsErr 184 | } 185 | return nil, err 186 | } 187 | 188 | return user, nil 189 | } 190 | 191 | func (cg *CognitoAuthService) GetUserByEmail(email string) (*cognitoidentityprovider.AdminGetUserOutput, error) { 192 | user, err := cg.client.AdminGetUser(context.Background(), &cognitoidentityprovider.AdminGetUserInput{ 193 | Username: &email, 194 | UserPoolId: &cg.env.UserPoolID, 195 | }) 196 | if err != nil { 197 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 198 | return nil, awsErr 199 | } 200 | return nil, err 201 | } 202 | 203 | return user, nil 204 | } 205 | 206 | func (cg *CognitoAuthService) CreateAdminUser(email, password string, isPermanent bool) (string, error) { 207 | _, err := cg.client.AdminCreateUser(context.Background(), &cognitoidentityprovider.AdminCreateUserInput{ 208 | UserPoolId: &cg.env.UserPoolID, 209 | Username: &email, 210 | MessageAction: types.MessageActionTypeSuppress, 211 | UserAttributes: []types.AttributeType{ 212 | { 213 | Name: aws.String("email"), 214 | Value: aws.String(email), 215 | }, 216 | { 217 | Name: aws.String("email_verified"), 218 | Value: aws.String("true"), 219 | }, 220 | }, 221 | ValidationData: []types.AttributeType{}, 222 | }) 223 | 224 | if err != nil { 225 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 226 | return "", awsErr 227 | } 228 | return "", err 229 | } 230 | _, err = cg.client.AdminSetUserPassword(context.Background(), &cognitoidentityprovider.AdminSetUserPasswordInput{ 231 | Username: &email, 232 | Password: &password, 233 | Permanent: true, 234 | UserPoolId: &cg.env.UserPoolID, 235 | }) 236 | 237 | if err != nil { 238 | _, err = cg.client.AdminDeleteUser(context.Background(), &cognitoidentityprovider.AdminDeleteUserInput{Username: &email, UserPoolId: &cg.env.UserPoolID}) 239 | awsErr := utils.MapAWSError(cg.logger, err) 240 | if awsErr != nil { 241 | return "", awsErr 242 | } 243 | return "", err 244 | } 245 | 246 | err = cg.setCustomClaimToOneUser(email, map[string]string{ 247 | "role": string(constants.UserRoleAdmin), 248 | "change-password": strconv.FormatBool(!isPermanent), 249 | }) 250 | if err != nil { 251 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 252 | return "", awsErr 253 | } 254 | return "", err 255 | } 256 | 257 | // fetching created admin user 258 | adminUser, err := cg.GetUserByEmail(email) 259 | if err != nil { 260 | cg.logger.Error(err) 261 | return "", err 262 | } 263 | 264 | var cognitoUUID string 265 | // Access the user attributes tp find sub i.e cognito internal uuid 266 | for _, attr := range adminUser.UserAttributes { 267 | if *attr.Name == "sub" { 268 | cognitoUUID = *attr.Value 269 | break 270 | } 271 | } 272 | 273 | return cognitoUUID, nil 274 | } 275 | 276 | func (cg *CognitoAuthService) DeleteCognitoUser(token *string) error { 277 | _, err := cg.client.DeleteUser(context.Background(), &cognitoidentityprovider.DeleteUserInput{ 278 | AccessToken: token, 279 | }) 280 | if err != nil { 281 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 282 | return awsErr 283 | } 284 | return err 285 | } 286 | 287 | return nil 288 | } 289 | 290 | // UpdateUserAttribute updates user attribute from user's access token 291 | func (cg *CognitoAuthService) UpdateUserAttribute(username *string, attr []types.AttributeType) (*cognitoidentityprovider.AdminUpdateUserAttributesOutput, error) { 292 | op, err := cg.client.AdminUpdateUserAttributes(context.Background(), 293 | &cognitoidentityprovider.AdminUpdateUserAttributesInput{ 294 | UserPoolId: &cg.env.UserPoolID, 295 | Username: username, 296 | UserAttributes: attr, 297 | }, 298 | ) 299 | if err != nil { 300 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 301 | return nil, awsErr 302 | } 303 | return nil, err 304 | } 305 | return op, nil 306 | } 307 | 308 | // UpdateEmailAddress update email address of the user by checking 309 | // if the proper password is provided or not 310 | func (cg *CognitoAuthService) UpdateEmailAddress(uid, token, password, email *string) error { 311 | _, err := cg.client.ChangePassword(context.Background(), &cognitoidentityprovider.ChangePasswordInput{ 312 | AccessToken: token, 313 | PreviousPassword: password, 314 | ProposedPassword: password, 315 | }) 316 | if err != nil { 317 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 318 | return awsErr 319 | } 320 | return err 321 | } 322 | 323 | _, err = cg.UpdateUserAttribute(uid, []types.AttributeType{ 324 | { 325 | Name: aws.String("email"), 326 | Value: email, 327 | }, 328 | }) 329 | return err 330 | } 331 | 332 | // SetUserPassword sets cognito users password from admin 333 | func (cg *CognitoAuthService) SetUserPassword(email, password string) error { 334 | _, err := cg.client.AdminSetUserPassword(context.Background(), &cognitoidentityprovider.AdminSetUserPasswordInput{ 335 | Password: &password, 336 | Username: &email, 337 | Permanent: true, 338 | UserPoolId: &cg.env.UserPoolID, 339 | }) 340 | if err != nil { 341 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 342 | return awsErr 343 | } 344 | return err 345 | } 346 | return nil 347 | } 348 | 349 | func (cg *CognitoAuthService) DeleteUserAsAdmin(username string) error { 350 | _, err := cg.client.AdminDeleteUser(context.Background(), 351 | &cognitoidentityprovider.AdminDeleteUserInput{ 352 | UserPoolId: &cg.env.UserPoolID, 353 | Username: &username, 354 | }, 355 | ) 356 | if err != nil { 357 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 358 | return awsErr 359 | } 360 | return err 361 | } 362 | return nil 363 | } 364 | 365 | func (cg *CognitoAuthService) UpdateUserRole(email, newRole string) error { 366 | err := cg.setCustomClaimToOneUser(email, map[string]string{ 367 | "role": newRole, 368 | }) 369 | if err != nil { 370 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 371 | return awsErr 372 | } 373 | return err 374 | } 375 | return nil 376 | } 377 | 378 | func (cg *CognitoAuthService) DisableUser(username string) error { 379 | _, err := cg.client.AdminDisableUser(context.Background(), 380 | &cognitoidentityprovider.AdminDisableUserInput{ 381 | UserPoolId: &cg.env.UserPoolID, 382 | Username: &username, 383 | }, 384 | ) 385 | if err != nil { 386 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 387 | return awsErr 388 | } 389 | return err 390 | } 391 | return nil 392 | } 393 | 394 | func (cg *CognitoAuthService) EnableUser(username string) error { 395 | _, err := cg.client.AdminEnableUser(context.Background(), 396 | &cognitoidentityprovider.AdminEnableUserInput{ 397 | UserPoolId: &cg.env.UserPoolID, 398 | Username: &username, 399 | }, 400 | ) 401 | if err != nil { 402 | if awsErr := utils.MapAWSError(cg.logger, err); awsErr != nil { 403 | return awsErr 404 | } 405 | return err 406 | } 407 | return nil 408 | } 409 | -------------------------------------------------------------------------------- /pkg/services/module.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "go.uber.org/fx" 5 | ) 6 | 7 | // Module exports services present 8 | var Module = fx.Options( 9 | fx.Provide( 10 | NewS3Service, 11 | NewCognitoAuthService, 12 | NewSESService, 13 | ), 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/services/s3.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "context" 6 | "io" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/s3" 11 | ) 12 | 13 | type S3Service struct { 14 | logger framework.Logger 15 | env *framework.Env 16 | client *s3.Client 17 | } 18 | 19 | func NewS3Service( 20 | logger framework.Logger, 21 | env *framework.Env, 22 | client *s3.Client, 23 | ) S3Service { 24 | return S3Service{ 25 | logger: logger, 26 | env: env, 27 | client: client, 28 | } 29 | } 30 | 31 | func (s *S3Service) UploadFile( 32 | ctx context.Context, 33 | file io.Reader, 34 | fileName string, 35 | ) (string, error) { 36 | 37 | bucketName := s.env.StorageBucketName 38 | 39 | if bucketName == "" { 40 | s.logger.Fatal("Bucket name missing.") 41 | } 42 | 43 | _, err := s.client.HeadBucket(context.Background(), &s3.HeadBucketInput{ 44 | Bucket: &bucketName, 45 | }) 46 | 47 | if err != nil { 48 | s.logger.Fatalf("%v Bucket doesn't exists. Error because of %v", bucketName, err.Error()) 49 | } 50 | 51 | _, err = s.client.PutObject(context.Background(), &s3.PutObjectInput{ 52 | Bucket: &bucketName, 53 | Key: &fileName, 54 | Body: file, 55 | }) 56 | 57 | if err != nil { 58 | s.logger.Fatal("Failed to upload the file in the bucket.", err.Error()) 59 | return "", err 60 | } 61 | 62 | return fileName, nil 63 | } 64 | 65 | func (s *S3Service) GetSignedURL(key string) (string, error) { 66 | bucketName := s.env.StorageBucketName 67 | 68 | if bucketName == "" { 69 | s.logger.Fatal("Bucket name missing.") 70 | } 71 | 72 | _, err := s.client.HeadBucket(context.Background(), &s3.HeadBucketInput{ 73 | Bucket: &bucketName, 74 | }) 75 | 76 | if err != nil { 77 | s.logger.Fatalf("%v Bucket doesn't exists. Error because of %v", bucketName, err.Error()) 78 | } 79 | 80 | presignClient := s3.NewPresignClient(s.client) 81 | presignedUrl, err := presignClient.PresignGetObject(context.Background(), 82 | &s3.GetObjectInput{ 83 | Bucket: aws.String(bucketName), 84 | Key: aws.String(key), 85 | }, 86 | s3.WithPresignExpires(time.Minute*15)) 87 | if err != nil { 88 | s.logger.Fatal(err) 89 | } 90 | return presignedUrl.URL, nil 91 | 92 | } 93 | -------------------------------------------------------------------------------- /pkg/services/ses_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/sesv2" 7 | "github.com/aws/aws-sdk-go-v2/service/sesv2/types" 8 | ) 9 | 10 | type SESService struct { 11 | *sesv2.Client 12 | } 13 | 14 | func NewSESService(client *sesv2.Client) SESService { 15 | return SESService{ 16 | Client: client, 17 | } 18 | } 19 | 20 | type EmailParams struct { 21 | From string 22 | To []string `json:"to"` 23 | Subject string `json:"subject"` 24 | Body string `json:"body"` 25 | } 26 | 27 | func (s SESService) SendEmail(params *EmailParams) error { 28 | charset := "UTF-8" 29 | 30 | input := &sesv2.SendEmailInput{ 31 | Content: &types.EmailContent{ 32 | Simple: &types.Message{ 33 | Body: &types.Body{ 34 | Text: &types.Content{ 35 | Charset: &charset, 36 | Data: ¶ms.Body, 37 | }, 38 | }, 39 | Subject: &types.Content{ 40 | Charset: &charset, 41 | Data: ¶ms.Subject, 42 | }, 43 | }, 44 | }, 45 | Destination: &types.Destination{ 46 | ToAddresses: params.To, 47 | }, 48 | FromEmailAddress: ¶ms.From, 49 | } 50 | if _, err := s.Client.SendEmail(context.Background(), input); err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/types/base.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ModelBase struct { 4 | ID BinaryUUID `json:"id"` 5 | } 6 | -------------------------------------------------------------------------------- /pkg/types/binary_uuid.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "clean-architecture/pkg/errorz" 5 | "database/sql/driver" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // BinaryUUID -> binary uuid wrapper over uuid.UUID 13 | type BinaryUUID uuid.UUID 14 | 15 | // ParseUUID -> parses string uuid to binary uuid 16 | func ParseUUID(id string) BinaryUUID { 17 | return BinaryUUID(uuid.MustParse(id)) 18 | } 19 | 20 | // ShouldParseUUID -> parses string uuid to binary uuid with error 21 | func ShouldParseUUID(id string) (BinaryUUID, error) { 22 | UUID, err := uuid.Parse(id) 23 | if err != nil { 24 | return BinaryUUID{}, errorz.ErrInvalidUUID 25 | } 26 | return BinaryUUID(UUID), err 27 | } 28 | 29 | func (b BinaryUUID) String() string { 30 | return uuid.UUID(b).String() 31 | } 32 | 33 | // MarshalJSON -> convert to json string 34 | func (b BinaryUUID) MarshalJSON() ([]byte, error) { 35 | s := uuid.UUID(b) 36 | str := "\"" + s.String() + "\"" 37 | return []byte(str), nil 38 | } 39 | 40 | // UnmarshalJSON -> convert from json string 41 | func (b *BinaryUUID) UnmarshalJSON(by []byte) error { 42 | s, err := uuid.ParseBytes(by) 43 | *b = BinaryUUID(s) 44 | return err 45 | } 46 | 47 | // GormDataType -> sql data type for gorm 48 | func (BinaryUUID) GormDataType() string { 49 | return "binary(16)" 50 | } 51 | 52 | // Scan -> scan value into BinaryUUID 53 | func (b *BinaryUUID) Scan(value any) error { 54 | bytes, ok := value.([]byte) 55 | if !ok { 56 | return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 57 | } 58 | 59 | data, err := uuid.FromBytes(bytes) 60 | *b = BinaryUUID(data) 61 | return err 62 | } 63 | 64 | // Value -> return BinaryUUID to []bytes binary(16) 65 | func (b BinaryUUID) Value() (driver.Value, error) { 66 | return uuid.UUID(b).MarshalBinary() 67 | } 68 | -------------------------------------------------------------------------------- /pkg/types/file_metadata.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // UploadMetadata metadata received after uploading file 4 | type UploadMetadata struct { 5 | FieldName string 6 | URL string 7 | FileName string 8 | FileUID string 9 | Size int64 10 | } 11 | 12 | type UploadedFiles []UploadMetadata 13 | 14 | func (f UploadedFiles) GetFile(fieldName string) UploadMetadata { 15 | for _, file := range f { 16 | if file.FieldName == fieldName { 17 | return file 18 | } 19 | } 20 | return UploadMetadata{} 21 | } 22 | 23 | func (f UploadedFiles) GetMultipleFiles(fieldName string) []UploadMetadata { 24 | var files []UploadMetadata 25 | for _, file := range f { 26 | if file.FieldName == fieldName { 27 | files = append(files, file) 28 | } 29 | } 30 | return files 31 | } 32 | -------------------------------------------------------------------------------- /pkg/utils/aws_error_mapper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "strings" 9 | 10 | "github.com/aws/smithy-go" 11 | ) 12 | 13 | type AWSError struct { 14 | OE *smithy.OperationError 15 | StatusCode string 16 | RequestID string 17 | ExceptionType string 18 | ExceptionMessage string 19 | } 20 | 21 | func MapAWSError(logger framework.Logger, err error) (awsError *AWSError) { 22 | 23 | defer func() { 24 | if r := recover(); r != nil { 25 | logger.Error(r) 26 | } 27 | }() 28 | 29 | var oe *smithy.OperationError 30 | if errors.As(err, &oe) { 31 | errorStr := oe.Err.Error() 32 | errorData := strings.Split(errorStr, ",") 33 | log.Println(errorData) 34 | if len(errorData) == 3 { 35 | awsError = &AWSError{ 36 | OE: oe, 37 | StatusCode: strings.Split(strings.TrimSpace(errorData[0]), ": ")[1], 38 | RequestID: strings.Split(strings.TrimSpace(errorData[1]), ": ")[1], 39 | ExceptionType: strings.Split(strings.TrimSpace(errorData[2]), ": ")[0], 40 | ExceptionMessage: strings.Split(strings.TrimSpace(errorData[2]), "Exception: ")[1], 41 | } 42 | return awsError 43 | } 44 | ex := strings.Split(errorStr, "Exception: ") 45 | if len(ex) == 2 { 46 | awsError = &AWSError{ 47 | ExceptionMessage: ex[1], 48 | } 49 | return awsError 50 | } 51 | } 52 | return awsError 53 | } 54 | 55 | func (e AWSError) String() string { 56 | return fmt.Sprintf( 57 | "StatusCode: %s, RequestID: %s, ExceptionType: %s, ExceptionMessage: %s", 58 | e.StatusCode, 59 | e.RequestID, 60 | e.ExceptionType, 61 | e.ExceptionMessage, 62 | ) 63 | } 64 | 65 | func (e AWSError) Error() string { 66 | return strings.TrimSpace(e.ExceptionMessage) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/utils/custom_bind.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "clean-architecture/pkg/types" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "reflect" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | // CustomBind custom bind the data 15 | func CustomBind(source *http.Request, dest any) error { 16 | err := source.ParseMultipartForm(1000) 17 | if err != nil { 18 | return err 19 | } 20 | if source == nil { 21 | return nil 22 | } 23 | 24 | formSource := source.MultipartForm.Value 25 | 26 | destValue := reflect.ValueOf(dest) 27 | destType := reflect.TypeOf(dest) 28 | 29 | if destType.Kind() != reflect.Ptr { 30 | return errors.New("only pointers can be binded") 31 | } 32 | 33 | if destType.Elem().Kind() != reflect.Struct { 34 | return errors.New("only struct pointers are allowed") 35 | } 36 | 37 | for i := 0; i < destType.Elem().NumField(); i++ { 38 | currentField := destType.Elem().Field(i) 39 | fieldValue := destValue.Elem().Field(i) 40 | 41 | if currentField.Type == reflect.TypeOf(types.ModelBase{}) { 42 | if err := CustomBind(source, fieldValue.Addr().Interface()); err != nil { 43 | return err 44 | } 45 | } 46 | 47 | key := currentField.Tag.Get("form") 48 | if key == "" { 49 | continue 50 | } 51 | 52 | targetValue, ok := formSource[key] 53 | if !ok { 54 | continue 55 | } 56 | 57 | if len(targetValue) == 0 { 58 | continue 59 | } 60 | 61 | kind := currentField.Type.Kind() 62 | 63 | switch kind { 64 | case reflect.String: 65 | fieldValue.SetString(targetValue[0]) 66 | case reflect.Bool: 67 | b, err := strconv.ParseBool(targetValue[0]) 68 | if err != nil { 69 | return err 70 | } 71 | fieldValue.SetBool(b) 72 | case reflect.Int: 73 | i, err := strconv.Atoi(targetValue[0]) 74 | if err != nil { 75 | return err 76 | } 77 | fieldValue.SetInt(int64(i)) 78 | default: 79 | 80 | _, ok := fieldValue.Interface().(time.Time) 81 | if ok { 82 | val, _ := time.Parse("2006-01-02 15:04:05", targetValue[0]) 83 | fieldValue.Set(reflect.ValueOf(val)) 84 | continue 85 | } 86 | 87 | if fieldValue.CanInterface() && fieldValue.Type().NumMethod() > 0 { 88 | val, ok := fieldValue.Addr().Interface().(json.Unmarshaler) 89 | if !ok { 90 | return fmt.Errorf("data type %s doesn't implement unmarshaler interface", fieldValue.Type()) 91 | } 92 | err := val.UnmarshalJSON([]byte(targetValue[0])) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/utils/datatype_converter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | func ConvertStringToInt(value string) (int, error) { 8 | converted, err := strconv.Atoi(value) 9 | 10 | if err != nil { 11 | return 0, err 12 | } 13 | 14 | return converted, nil 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/is_cli.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // IsCli checks if app is running in cli mode 8 | func IsCli() bool { 9 | if len(os.Args) > 1 { 10 | commandLine := os.Args[1] 11 | if commandLine != "" { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/pagination.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "clean-architecture/pkg/framework" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type Pagination struct { 11 | Page int 12 | Limit int 13 | Offset int 14 | } 15 | 16 | func BuildPagination(ctx *gin.Context) Pagination { 17 | // setting pagination 18 | pageStr := ctx.Query("page") 19 | limitStr := ctx.Query("limit") 20 | 21 | var ( 22 | err error 23 | page int 24 | limit int 25 | ) 26 | 27 | page, err = strconv.Atoi(pageStr) 28 | if err != nil || page == 0 { 29 | page = 1 30 | } 31 | 32 | limit, err = strconv.Atoi(limitStr) 33 | if err != nil || limit == 0 { 34 | limit = 10 35 | } 36 | 37 | ctx.Set(framework.Page, page) 38 | ctx.Set(framework.Limit, limit) 39 | 40 | return Pagination{ 41 | Page: page, 42 | Limit: limit, 43 | Offset: (page - 1) * limit, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/utils/send_sentry_msg.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/getsentry/sentry-go" 5 | sentrygin "github.com/getsentry/sentry-go/gin" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // SendSentryMsg -> send message to sentry 10 | func SendSentryMsg(ctx *gin.Context, msg string) { 11 | if hub := sentrygin.GetHubFromContext(ctx); hub != nil { 12 | hub.WithScope(func(scope *sentry.Scope) { 13 | hub.CaptureMessage("Error Occurred: " + msg) 14 | }) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /pkg/utils/sentry_service.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import sentry "github.com/getsentry/sentry-go" 4 | 5 | type SentryService interface { 6 | CaptureException(err error) 7 | } 8 | 9 | type DefaultSentryService struct{} 10 | 11 | func (s *DefaultSentryService) CaptureException(err error) { 12 | sentry.CaptureException(err) 13 | } 14 | 15 | var CurrentSentryService SentryService = &DefaultSentryService{} 16 | -------------------------------------------------------------------------------- /pkg/utils/status_in_list.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // StatusInList -> checks if the given status is in the list 4 | func StatusInList(status int, statusList []int) bool { 5 | for _, i := range statusList { 6 | if i == status { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /seeds/admin_seed.go: -------------------------------------------------------------------------------- 1 | package seeds 2 | 3 | import ( 4 | "clean-architecture/domain/constants" 5 | "clean-architecture/domain/models" 6 | "clean-architecture/domain/user" 7 | "clean-architecture/pkg/framework" 8 | "clean-architecture/pkg/services" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | ) 12 | 13 | type AdminSeed struct { 14 | logger framework.Logger 15 | cognitoService services.CognitoAuthService 16 | userService *user.Service 17 | env *framework.Env 18 | } 19 | 20 | // NewAdminSeed creates admin seed 21 | func NewAdminSeed( 22 | logger framework.Logger, 23 | cognitoService services.CognitoAuthService, 24 | userService *user.Service, 25 | env *framework.Env, 26 | ) AdminSeed { 27 | return AdminSeed{ 28 | logger: logger, 29 | cognitoService: cognitoService, 30 | userService: userService, 31 | env: env, 32 | } 33 | } 34 | 35 | // Run the admin seed 36 | func (s AdminSeed) Setup() { 37 | email := s.env.AdminEmail 38 | password := s.env.AdminPassword 39 | 40 | s.logger.Info("🌱 seeding admin data...") 41 | 42 | if _, err := s.cognitoService.GetUserByUsername(email); err != nil { 43 | cognitoUUID, err := s.cognitoService.CreateAdminUser(email, password, true) 44 | if err != nil { 45 | s.logger.Error("failed to create the admin user in cognito", err.Error()) 46 | return 47 | } 48 | s.logger.Info("Successfully created admin user in cognito") 49 | 50 | adminUser := models.User{ 51 | Email: email, 52 | CognitoUID: aws.String(cognitoUUID), 53 | Role: constants.UserRoleAdmin, 54 | IsEmailVerified: true, 55 | IsActive: true, 56 | } 57 | if err := s.userService.Create(&adminUser); err != nil { 58 | s.logger.Error(err.Error()) 59 | return 60 | } 61 | } 62 | s.logger.Info("Admin user already exists") 63 | } 64 | -------------------------------------------------------------------------------- /seeds/seeds.go: -------------------------------------------------------------------------------- 1 | package seeds 2 | 3 | import "go.uber.org/fx" 4 | 5 | // Module exports seed module 6 | var Module = fx.Options( 7 | // fx.Provide(NewAdminSeed), 8 | // fx.Provide(NewSeeds), 9 | ) 10 | 11 | // Seed db seed 12 | type Seed interface { 13 | Setup() 14 | } 15 | 16 | // Seeds listing of seeds 17 | type Seeds []Seed 18 | 19 | // Run run the seed data 20 | func (s Seeds) Setup() { 21 | for _, seed := range s { 22 | seed.Setup() 23 | } 24 | } 25 | 26 | // NewSeeds creates new seeds 27 | func NewSeeds(adminSeed AdminSeed) Seeds { 28 | return Seeds{ 29 | adminSeed, 30 | } 31 | } 32 | --------------------------------------------------------------------------------