├── .dockerignore ├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── main.go ├── show-content.bash └── src ├── application └── usecases │ ├── auth │ ├── auth.go │ ├── auth_test.go │ ├── mappers.go │ └── structures.go │ ├── medicine │ ├── medicine.go │ ├── medicine_test.go │ └── structures.go │ └── user │ ├── structures.go │ ├── user.go │ └── user_test.go ├── domain ├── Types.go ├── errors │ ├── Errors.go │ └── Gorm.go ├── medicine │ └── Medicine.go └── user │ └── user.go └── infrastructure ├── repository ├── initDB.go ├── medicine │ ├── mappers.go │ ├── medicine.go │ └── structures.go ├── user │ ├── mappers.go │ ├── structures.go │ └── user.go └── utils │ └── Utils.go ├── rest ├── adapter │ ├── auth.go │ ├── medicine.go │ └── user.go ├── controllers │ ├── BindTools.go │ ├── Utils.go │ ├── auth │ │ ├── Auth.go │ │ └── Structures.go │ ├── medicine │ │ ├── Mapper.go │ │ ├── Medicines.go │ │ ├── Requests.go │ │ ├── Responses.go │ │ └── Validation.go │ └── user │ │ ├── Mapper.go │ │ ├── Requests.go │ │ ├── Responses.go │ │ ├── User.go │ │ └── Validation.go ├── middlewares │ ├── Headers.go │ ├── Interceptor.go │ ├── RequiresLogin.go │ └── errorHandler.go └── routes │ ├── auth.go │ ├── medicine.go │ ├── routes.go │ └── user.go └── security └── jwt └── jwt.go /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .git 3 | .gitignore -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=boilerplate_go 2 | POSTGRES_USER=appuser 3 | POSTGRES_PASSWORD=youShouldChangeThisPassword 4 | 5 | DB_HOST=postgres 6 | DB_PORT=5432 7 | DB_NAME=boilerplate_go 8 | DB_USER=appuser 9 | DB_PASS=youShouldChangeThisPassword 10 | 11 | JWT_ACCESS_SECRET=accesskeyyoumayneedtochangeit 12 | JWT_ACCESS_TIME_MINUTE=10 13 | JWT_REFRESH_SECRET=refreshkeyyoumayneedtochangeit 14 | JWT_REFRESH_TIME_HOUR=10 15 | 16 | SERVER_PORT=8080 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [gbrayhan] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: BossonH # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | 29 | - OS: [e.g. iOS] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "bundler" # See documentation for possible values 9 | vendor: true 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.24 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Folders and Files 2 | .idea 3 | .DS_Store 4 | .env 5 | 6 | 7 | *.txt 8 | *.log 9 | 10 | microservices 11 | config.json 12 | 13 | microservices-go 14 | 15 | 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at gbrayhan@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing Guidelines 2 | Thank you for considering contributing to our project. To ensure that all contributors have a positive experience, we have established some guidelines that you should follow. 3 | 4 | Before you begin contributing, make sure to read our Code of Conduct to understand how we expect community members to interact with each other. 5 | 6 | ### How to Contribute. 7 | 1. Fork the repository on GitHub. 8 | 2. Create a branch in your fork with a descriptive name indicating the change you are making. 9 | 3. Make the changes in your branch. 10 | 4. Make sure your changes follow clean architecture patterns and coding best practices in Go. 11 | 5. Run tests to ensure you haven't introduced any errors. 12 | 6. Create a pull request from your branch to the main branch of the repository. 13 | 7. Wait for the team to review your pull request. If changes are needed, make sure to make them before submitting a new review. 14 | 8. Once your pull request is approved, the team will merge it into the main branch. 15 | 16 | ### How to Report an Issue. 17 | 18 | If you find a bug or issue in the project, please follow these steps to report it: 19 | 20 | 1. Open an issue on the repository describing the problem as detailed as possible. 21 | 2. Provide an example of the incorrect behavior and how it can be reproduced. 22 | 3. Provide information about the environment in which you are running the code, including the version of Go you are using. 23 | 4. If you have a solution for the problem, please submit a pull request following the guidelines established above. 24 | 25 | ### Pull Request Guidelines 26 | To ensure that pull requests integrate smoothly, please follow these guidelines: 27 | 28 | 1. Follow the guidelines established in our Code of Conduct. 29 | 2. Make sure your code follows Go's coding conventions, including formatting and style. 30 | 3. Make sure your code follows clean architecture patterns and coding best practices in Go. 31 | 4. Make sure your changes do not break any existing tests, and you have added tests for any new functionality. 32 | 5. Create a detailed description of the changes you made and why you made them. 33 | 34 | 35 | Thank you for contributing to our project. Your time and effort are greatly appreciated. 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /srv/go-app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 10 | go build -a -installsuffix cgo -o microservice . 11 | 12 | FROM gcr.io/distroless/static:nonroot 13 | 14 | WORKDIR /srv/go-app 15 | 16 | COPY --from=builder /srv/go-app/microservice . 17 | 18 | USER nonroot:nonroot 19 | 20 | CMD ["./microservice"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (ctx) 2020 gbrayhan@gmail.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Microservices Boilerplate - Clean Architecture 2 | 3 | [![issues](https://img.shields.io/github/issues/gbrayhan/microservices-go)](https://github.com/gbrayhan/microservices-go/tree/master/.github/ISSUE_TEMPLATE) 4 | [![forks](https://img.shields.io/github/forks/gbrayhan/microservices-go)](https://github.com/gbrayhan/microservices-go/network/members) 5 | [![stars](https://img.shields.io/github/stars/gbrayhan/microservices-go)](https://github.com/gbrayhan/microservices-go/stargazers) 6 | [![license](https://img.shields.io/github/license/gbrayhan/microservices-go)](https://github.com/gbrayhan/microservices-go/tree/master/LICENSE) 7 | [![CodeFactor](https://www.codefactor.io/repository/github/gbrayhan/microservices-go/badge/main)](https://www.codefactor.io/repository/github/gbrayhan/microservices-go/overview/main) 8 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/6c10cc49928447f38952edaab67a94a4)](https://www.codacy.com/gh/gbrayhan/microservices-go/dashboard?utm_source=github.com&utm_medium=referral&utm_content=gbrayhan/microservices-go&utm_campaign=Badge_Grade) 9 | 10 | Example structure to start a microservices project with golang. Using a MySQL databaseSQL. Using a Hexagonal 11 | Architecture tha is a Clean Architecture. 12 | 13 | ## Manual Installation 14 | 15 | If you would still prefer to do the installation manually, follow these steps: 16 | 17 | Clone the repo: 18 | 19 | ```bash 20 | git clone https://github.com/gbrayhan/microservices-go 21 | ``` 22 | 23 | 24 | **TL;DR command list** 25 | 26 | git clone https://github.com/gbrayhan/microservices-go 27 | cd microservices-go 28 | cp .env.example .env 29 | docker-compose up --build -d 30 | 31 | ## Table of Contents 32 | 33 | - [Features](#features) 34 | - [Commands](#commands) 35 | - [Environment Variables](#environment-variables) 36 | - [Project Structure](#project-structure) 37 | - [API Documentation](#api-documentation) 38 | - [Error Handling](#error-handling) 39 | - [Validation](#validation) 40 | - [Linting](#linting) 41 | 42 | ## Features 43 | 44 | - **Golang v1.24.2**: Stable version of go 45 | - **Framework**: A stable version of [gin-go](https://github.com/gin-gonic/gin) 46 | - **Token Security**: with [JWT](https://jwt.io) 47 | - **SQL databaseSQL**: [Postgresql](https://www.postgresql.org/) using internal sql package of 48 | go [sql](https://golang.org/pkg/databaseSQL/sql/) 49 | - **Testing**: unit and integration tests using package of go [testing](https://golang.org/pkg/testing/) 50 | - **API documentation**: with [swaggo](https://github.com/swaggo/swag) @latest version that is a go implementation 51 | of [swagger](https://swagger.io/) 52 | - **Dependency management**: with [go modules](https://golang.org/ref/mod) 53 | - **Docker support** 54 | - **Code quality**: with [CodeFactor](https://www.codefactor.io/) and [Codacy](https://www.codacy.com/) 55 | - **Linting**: with [golangci-lint](https://golangci-lint.run/usage/install/) an implementation of a Golang linter 56 | 57 | 58 | ## Security Checks using Trivy 59 | 60 | https://github.com/aquasecurity/trivy?tab=readme-ov-file 61 | 62 | command: 63 | ```bash 64 | trivy fs . 65 | ``` 66 | 67 | ## Commands 68 | 69 | ### Build and run image of docker 70 | 71 | ```bash 72 | docker-compose up --build -d 73 | ``` 74 | 75 | ### Swagger Implementation 76 | 77 | ```bash 78 | swag init -g src/infrastructure/rest/routes/routes.go 79 | ``` 80 | 81 | To visualize the swagger documentation on local use 82 | 83 | http://localhost:8080/v1/swagger/index.html 84 | 85 | To see the postman collection use 86 | 87 | https://www.postman.com/kts-mexico/workspace/boilerplategomicroservice 88 | 89 | 90 | ### Unit test command 91 | 92 | ```bash 93 | # run recursive test 94 | go test ./test/unit/... 95 | # clean go test results in cache 96 | go clean -testcache 97 | ``` 98 | 99 | ### Lint inspection of go 100 | 101 | ```bash 102 | golangci-lint run ./... 103 | ``` 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 1.1.x | :white_check_mark: | 11 | | 1.0.x | :white_check_mark: | 12 | | < 0.1 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | Use this section to tell people how to report a vulnerability. 17 | 18 | Tell them where to go, how often they can expect to get an update on a 19 | reported vulnerability, what to expect if the vulnerability is accepted or 20 | declined, etc. 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | postgres: 5 | image: postgres:17.4 6 | restart: always 7 | env_file: 8 | - .env 9 | environment: 10 | - POSTGRES_DB=${POSTGRES_DB} 11 | - POSTGRES_USER=${POSTGRES_USER} 12 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 13 | healthcheck: 14 | test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] 15 | interval: 10s 16 | timeout: 5s 17 | start_period: 30s 18 | retries: 5 19 | ports: 20 | - "5432:5432" 21 | volumes: 22 | - pgdata:/var/lib/postgresql/data 23 | networks: 24 | - go-network 25 | 26 | go-microservice: 27 | build: 28 | context: . 29 | image: go-microservice 30 | restart: on-failure 31 | env_file: 32 | - .env 33 | ports: 34 | - "8080:8080" 35 | depends_on: 36 | postgres: 37 | condition: service_healthy 38 | networks: 39 | - go-network 40 | 41 | volumes: 42 | pgdata: 43 | 44 | networks: 45 | go-network: 46 | driver: bridge 47 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "termsOfService": "http://swagger.io/terms/", 13 | "contact": { 14 | "name": "Alejandro Gabriel Guerrero", 15 | "url": "http://github.com/gbrayhan", 16 | "email": "gbrayhan@gmail.com" 17 | }, 18 | "license": { 19 | "name": "Apache 2.0", 20 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 21 | }, 22 | "version": "{{.Version}}" 23 | }, 24 | "host": "{{.Host}}", 25 | "basePath": "{{.BasePath}}", 26 | "paths": { 27 | "/auth/access-token": { 28 | "post": { 29 | "description": "Auth user by email and password", 30 | "tags": [ 31 | "auth" 32 | ], 33 | "summary": "GetAccessTokenByRefreshToken UserName", 34 | "parameters": [ 35 | { 36 | "description": "body data", 37 | "name": "data", 38 | "in": "body", 39 | "required": true, 40 | "schema": { 41 | "$ref": "#/definitions/auth.AccessTokenRequest" 42 | } 43 | } 44 | ], 45 | "responses": { 46 | "200": { 47 | "description": "OK", 48 | "schema": { 49 | "$ref": "#/definitions/auth.DataUserAuthenticated" 50 | } 51 | }, 52 | "400": { 53 | "description": "Bad Request", 54 | "schema": { 55 | "$ref": "#/definitions/controllers.MessageResponse" 56 | } 57 | }, 58 | "500": { 59 | "description": "Internal Server Error", 60 | "schema": { 61 | "$ref": "#/definitions/controllers.MessageResponse" 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "/auth/login": { 68 | "post": { 69 | "description": "Auth user by email and password", 70 | "tags": [ 71 | "auth" 72 | ], 73 | "summary": "Login UserName", 74 | "parameters": [ 75 | { 76 | "description": "body data", 77 | "name": "data", 78 | "in": "body", 79 | "required": true, 80 | "schema": { 81 | "$ref": "#/definitions/auth.LoginRequest" 82 | } 83 | } 84 | ], 85 | "responses": { 86 | "200": { 87 | "description": "OK", 88 | "schema": { 89 | "$ref": "#/definitions/auth.DataUserAuthenticated" 90 | } 91 | }, 92 | "400": { 93 | "description": "Bad Request", 94 | "schema": { 95 | "$ref": "#/definitions/controllers.MessageResponse" 96 | } 97 | }, 98 | "500": { 99 | "description": "Internal Server Error", 100 | "schema": { 101 | "$ref": "#/definitions/controllers.MessageResponse" 102 | } 103 | } 104 | } 105 | } 106 | }, 107 | "/medicine": { 108 | "get": { 109 | "description": "Get all Medicines on the system", 110 | "tags": [ 111 | "medicine" 112 | ], 113 | "summary": "Get all Medicines", 114 | "parameters": [ 115 | { 116 | "type": "string", 117 | "description": "limit", 118 | "name": "limit", 119 | "in": "query", 120 | "required": true 121 | }, 122 | { 123 | "type": "string", 124 | "description": "page", 125 | "name": "page", 126 | "in": "query", 127 | "required": true 128 | } 129 | ], 130 | "responses": { 131 | "200": { 132 | "description": "OK", 133 | "schema": { 134 | "type": "array", 135 | "items": { 136 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_application_usecases_medicine.PaginationResultMedicine" 137 | } 138 | } 139 | }, 140 | "400": { 141 | "description": "Bad Request", 142 | "schema": { 143 | "$ref": "#/definitions/medicine.MessageResponse" 144 | } 145 | }, 146 | "500": { 147 | "description": "Internal Server Error", 148 | "schema": { 149 | "$ref": "#/definitions/medicine.MessageResponse" 150 | } 151 | } 152 | } 153 | }, 154 | "post": { 155 | "description": "Create new medicine on the system", 156 | "consumes": [ 157 | "application/json" 158 | ], 159 | "produces": [ 160 | "application/json" 161 | ], 162 | "tags": [ 163 | "medicine" 164 | ], 165 | "summary": "Create New Medicine", 166 | "parameters": [ 167 | { 168 | "description": "body data", 169 | "name": "data", 170 | "in": "body", 171 | "required": true, 172 | "schema": { 173 | "$ref": "#/definitions/medicine.NewMedicineRequest" 174 | } 175 | } 176 | ], 177 | "responses": { 178 | "200": { 179 | "description": "OK", 180 | "schema": { 181 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine" 182 | } 183 | }, 184 | "400": { 185 | "description": "Bad Request", 186 | "schema": { 187 | "$ref": "#/definitions/medicine.MessageResponse" 188 | } 189 | }, 190 | "500": { 191 | "description": "Internal Server Error", 192 | "schema": { 193 | "$ref": "#/definitions/medicine.MessageResponse" 194 | } 195 | } 196 | } 197 | } 198 | }, 199 | "/medicine/{medicine_id}": { 200 | "get": { 201 | "description": "Get Medicines by ID on the system", 202 | "tags": [ 203 | "medicine" 204 | ], 205 | "summary": "Get medicines by ID", 206 | "parameters": [ 207 | { 208 | "type": "integer", 209 | "description": "id of medicine", 210 | "name": "medicine_id", 211 | "in": "path", 212 | "required": true 213 | } 214 | ], 215 | "responses": { 216 | "200": { 217 | "description": "OK", 218 | "schema": { 219 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine" 220 | } 221 | }, 222 | "400": { 223 | "description": "Bad Request", 224 | "schema": { 225 | "$ref": "#/definitions/medicine.MessageResponse" 226 | } 227 | }, 228 | "500": { 229 | "description": "Internal Server Error", 230 | "schema": { 231 | "$ref": "#/definitions/medicine.MessageResponse" 232 | } 233 | } 234 | } 235 | } 236 | }, 237 | "/user": { 238 | "get": { 239 | "security": [ 240 | { 241 | "ApiKeyAuth": [] 242 | } 243 | ], 244 | "description": "Get all Users on the system", 245 | "tags": [ 246 | "user" 247 | ], 248 | "summary": "Get all Users", 249 | "responses": { 250 | "200": { 251 | "description": "OK", 252 | "schema": { 253 | "type": "array", 254 | "items": { 255 | "$ref": "#/definitions/user.ResponseUser" 256 | } 257 | } 258 | }, 259 | "400": { 260 | "description": "Bad Request", 261 | "schema": { 262 | "$ref": "#/definitions/user.MessageResponse" 263 | } 264 | }, 265 | "500": { 266 | "description": "Internal Server Error", 267 | "schema": { 268 | "$ref": "#/definitions/user.MessageResponse" 269 | } 270 | } 271 | } 272 | }, 273 | "post": { 274 | "security": [ 275 | { 276 | "ApiKeyAuth": [] 277 | } 278 | ], 279 | "description": "Create new user on the system", 280 | "consumes": [ 281 | "application/json" 282 | ], 283 | "produces": [ 284 | "application/json" 285 | ], 286 | "tags": [ 287 | "user" 288 | ], 289 | "summary": "Create New UserName", 290 | "parameters": [ 291 | { 292 | "description": "body data", 293 | "name": "data", 294 | "in": "body", 295 | "required": true, 296 | "schema": { 297 | "$ref": "#/definitions/user.NewUserRequest" 298 | } 299 | } 300 | ], 301 | "responses": { 302 | "200": { 303 | "description": "OK", 304 | "schema": { 305 | "$ref": "#/definitions/user.ResponseUser" 306 | } 307 | }, 308 | "400": { 309 | "description": "Bad Request", 310 | "schema": { 311 | "$ref": "#/definitions/user.MessageResponse" 312 | } 313 | }, 314 | "500": { 315 | "description": "Internal Server Error", 316 | "schema": { 317 | "$ref": "#/definitions/user.MessageResponse" 318 | } 319 | } 320 | } 321 | } 322 | }, 323 | "/user/{user_id}": { 324 | "get": { 325 | "security": [ 326 | { 327 | "ApiKeyAuth": [] 328 | } 329 | ], 330 | "description": "Get Users by ID on the system", 331 | "tags": [ 332 | "user" 333 | ], 334 | "summary": "Get users by ID", 335 | "parameters": [ 336 | { 337 | "type": "integer", 338 | "description": "id of user", 339 | "name": "user_id", 340 | "in": "path", 341 | "required": true 342 | } 343 | ], 344 | "responses": { 345 | "200": { 346 | "description": "OK", 347 | "schema": { 348 | "$ref": "#/definitions/user.MessageResponse" 349 | } 350 | }, 351 | "400": { 352 | "description": "Bad Request", 353 | "schema": { 354 | "$ref": "#/definitions/user.MessageResponse" 355 | } 356 | }, 357 | "500": { 358 | "description": "Internal Server Error", 359 | "schema": { 360 | "$ref": "#/definitions/user.MessageResponse" 361 | } 362 | } 363 | } 364 | } 365 | } 366 | }, 367 | "definitions": { 368 | "auth.AccessTokenRequest": { 369 | "type": "object", 370 | "required": [ 371 | "refreshToken" 372 | ], 373 | "properties": { 374 | "refreshToken": { 375 | "type": "string", 376 | "example": "badbunybabybebe" 377 | } 378 | } 379 | }, 380 | "auth.DataUserAuthenticated": { 381 | "type": "object", 382 | "properties": { 383 | "email": { 384 | "type": "string", 385 | "example": "some@mail.com" 386 | }, 387 | "firstName": { 388 | "type": "string", 389 | "example": "John" 390 | }, 391 | "id": { 392 | "type": "integer", 393 | "example": 123 394 | }, 395 | "lastName": { 396 | "type": "string", 397 | "example": "Doe" 398 | }, 399 | "role": { 400 | "type": "string", 401 | "example": "admin" 402 | }, 403 | "status": { 404 | "type": "boolean", 405 | "example": true 406 | }, 407 | "userName": { 408 | "type": "string", 409 | "example": "UserName" 410 | } 411 | } 412 | }, 413 | "auth.LoginRequest": { 414 | "type": "object", 415 | "required": [ 416 | "email", 417 | "password" 418 | ], 419 | "properties": { 420 | "email": { 421 | "type": "string", 422 | "example": "gbrayhan@gmail.com" 423 | }, 424 | "password": { 425 | "type": "string", 426 | "example": "Password123" 427 | } 428 | } 429 | }, 430 | "controllers.MessageResponse": { 431 | "type": "object", 432 | "properties": { 433 | "message": { 434 | "type": "string" 435 | } 436 | } 437 | }, 438 | "github_com_gbrayhan_microservices-go_src_application_usecases_medicine.PaginationResultMedicine": { 439 | "type": "object", 440 | "properties": { 441 | "current": { 442 | "type": "integer" 443 | }, 444 | "data": { 445 | "type": "array", 446 | "items": { 447 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine" 448 | } 449 | }, 450 | "limit": { 451 | "type": "integer" 452 | }, 453 | "nextCursor": { 454 | "type": "integer" 455 | }, 456 | "numPages": { 457 | "type": "integer" 458 | }, 459 | "prevCursor": { 460 | "type": "integer" 461 | }, 462 | "total": { 463 | "type": "integer" 464 | } 465 | } 466 | }, 467 | "github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine": { 468 | "type": "object", 469 | "properties": { 470 | "createdAt": { 471 | "type": "string" 472 | }, 473 | "description": { 474 | "type": "string" 475 | }, 476 | "eanCode": { 477 | "type": "string" 478 | }, 479 | "id": { 480 | "type": "integer" 481 | }, 482 | "laboratory": { 483 | "type": "string" 484 | }, 485 | "name": { 486 | "type": "string" 487 | }, 488 | "updatedAt": { 489 | "type": "string" 490 | } 491 | } 492 | }, 493 | "medicine.MessageResponse": { 494 | "type": "object", 495 | "properties": { 496 | "message": { 497 | "type": "string" 498 | } 499 | } 500 | }, 501 | "medicine.NewMedicineRequest": { 502 | "type": "object", 503 | "required": [ 504 | "description", 505 | "eanCode", 506 | "laboratory", 507 | "name" 508 | ], 509 | "properties": { 510 | "description": { 511 | "type": "string", 512 | "example": "Something" 513 | }, 514 | "eanCode": { 515 | "type": "string", 516 | "example": "122000000021" 517 | }, 518 | "laboratory": { 519 | "type": "string", 520 | "example": "Roche" 521 | }, 522 | "name": { 523 | "type": "string", 524 | "example": "Paracetamol" 525 | } 526 | } 527 | }, 528 | "medicine.ResponseMedicine": { 529 | "type": "object", 530 | "properties": { 531 | "createdAt": { 532 | "type": "string", 533 | "example": "2021-02-24 20:19:39" 534 | }, 535 | "description": { 536 | "type": "string", 537 | "example": "Some Description" 538 | }, 539 | "eanCode": { 540 | "type": "string", 541 | "example": "Some EanCode" 542 | }, 543 | "id": { 544 | "type": "integer", 545 | "example": 1099 546 | }, 547 | "laboratory": { 548 | "type": "string", 549 | "example": "Some Laboratory" 550 | }, 551 | "name": { 552 | "type": "string", 553 | "example": "Aspirina" 554 | }, 555 | "updatedAt": { 556 | "type": "string", 557 | "example": "2021-02-24 20:19:39" 558 | } 559 | } 560 | }, 561 | "user.MessageResponse": { 562 | "type": "object", 563 | "properties": { 564 | "message": { 565 | "type": "string" 566 | } 567 | } 568 | }, 569 | "user.NewUserRequest": { 570 | "type": "object", 571 | "required": [ 572 | "email", 573 | "firstName", 574 | "lastName", 575 | "password", 576 | "role", 577 | "user" 578 | ], 579 | "properties": { 580 | "email": { 581 | "type": "string", 582 | "example": "mail@mail.com" 583 | }, 584 | "firstName": { 585 | "type": "string", 586 | "example": "John" 587 | }, 588 | "lastName": { 589 | "type": "string", 590 | "example": "Doe" 591 | }, 592 | "password": { 593 | "type": "string", 594 | "example": "Password123" 595 | }, 596 | "role": { 597 | "type": "string", 598 | "example": "admin" 599 | }, 600 | "user": { 601 | "type": "string", 602 | "example": "someUser" 603 | } 604 | } 605 | }, 606 | "user.ResponseUser": { 607 | "type": "object", 608 | "properties": { 609 | "createdAt": { 610 | "type": "string", 611 | "example": "2021-02-24 20:19:39" 612 | }, 613 | "email": { 614 | "type": "string", 615 | "example": "some@mail.com" 616 | }, 617 | "firstName": { 618 | "type": "string", 619 | "example": "John" 620 | }, 621 | "id": { 622 | "type": "integer", 623 | "example": 1099 624 | }, 625 | "lastName": { 626 | "type": "string", 627 | "example": "Doe" 628 | }, 629 | "status": { 630 | "type": "boolean", 631 | "example": false 632 | }, 633 | "updatedAt": { 634 | "type": "string", 635 | "example": "2021-02-24 20:19:39" 636 | }, 637 | "user": { 638 | "type": "string", 639 | "example": "BossonH" 640 | } 641 | } 642 | } 643 | }, 644 | "securityDefinitions": { 645 | "ApiKeyAuth": { 646 | "type": "apiKey", 647 | "name": "Authorization", 648 | "in": "header" 649 | } 650 | } 651 | }` 652 | 653 | // SwaggerInfo holds exported Swagger Info so clients can modify it 654 | var SwaggerInfo = &swag.Spec{ 655 | Version: "1.2", 656 | Host: "localhost:8080", 657 | BasePath: "/v1", 658 | Schemes: []string{}, 659 | Title: "Boilerplate Golang", 660 | Description: "Documentation's Boilerplate Golang", 661 | InfoInstanceName: "swagger", 662 | SwaggerTemplate: docTemplate, 663 | LeftDelim: "{{", 664 | RightDelim: "}}", 665 | } 666 | 667 | func init() { 668 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 669 | } 670 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Documentation's Boilerplate Golang", 5 | "title": "Boilerplate Golang", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "Alejandro Gabriel Guerrero", 9 | "url": "http://github.com/gbrayhan", 10 | "email": "gbrayhan@gmail.com" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | }, 16 | "version": "1.2" 17 | }, 18 | "host": "localhost:8080", 19 | "basePath": "/v1", 20 | "paths": { 21 | "/auth/access-token": { 22 | "post": { 23 | "description": "Auth user by email and password", 24 | "tags": [ 25 | "auth" 26 | ], 27 | "summary": "GetAccessTokenByRefreshToken UserName", 28 | "parameters": [ 29 | { 30 | "description": "body data", 31 | "name": "data", 32 | "in": "body", 33 | "required": true, 34 | "schema": { 35 | "$ref": "#/definitions/auth.AccessTokenRequest" 36 | } 37 | } 38 | ], 39 | "responses": { 40 | "200": { 41 | "description": "OK", 42 | "schema": { 43 | "$ref": "#/definitions/auth.DataUserAuthenticated" 44 | } 45 | }, 46 | "400": { 47 | "description": "Bad Request", 48 | "schema": { 49 | "$ref": "#/definitions/controllers.MessageResponse" 50 | } 51 | }, 52 | "500": { 53 | "description": "Internal Server Error", 54 | "schema": { 55 | "$ref": "#/definitions/controllers.MessageResponse" 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "/auth/login": { 62 | "post": { 63 | "description": "Auth user by email and password", 64 | "tags": [ 65 | "auth" 66 | ], 67 | "summary": "Login UserName", 68 | "parameters": [ 69 | { 70 | "description": "body data", 71 | "name": "data", 72 | "in": "body", 73 | "required": true, 74 | "schema": { 75 | "$ref": "#/definitions/auth.LoginRequest" 76 | } 77 | } 78 | ], 79 | "responses": { 80 | "200": { 81 | "description": "OK", 82 | "schema": { 83 | "$ref": "#/definitions/auth.DataUserAuthenticated" 84 | } 85 | }, 86 | "400": { 87 | "description": "Bad Request", 88 | "schema": { 89 | "$ref": "#/definitions/controllers.MessageResponse" 90 | } 91 | }, 92 | "500": { 93 | "description": "Internal Server Error", 94 | "schema": { 95 | "$ref": "#/definitions/controllers.MessageResponse" 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "/medicine": { 102 | "get": { 103 | "description": "Get all Medicines on the system", 104 | "tags": [ 105 | "medicine" 106 | ], 107 | "summary": "Get all Medicines", 108 | "parameters": [ 109 | { 110 | "type": "string", 111 | "description": "limit", 112 | "name": "limit", 113 | "in": "query", 114 | "required": true 115 | }, 116 | { 117 | "type": "string", 118 | "description": "page", 119 | "name": "page", 120 | "in": "query", 121 | "required": true 122 | } 123 | ], 124 | "responses": { 125 | "200": { 126 | "description": "OK", 127 | "schema": { 128 | "type": "array", 129 | "items": { 130 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_application_usecases_medicine.PaginationResultMedicine" 131 | } 132 | } 133 | }, 134 | "400": { 135 | "description": "Bad Request", 136 | "schema": { 137 | "$ref": "#/definitions/medicine.MessageResponse" 138 | } 139 | }, 140 | "500": { 141 | "description": "Internal Server Error", 142 | "schema": { 143 | "$ref": "#/definitions/medicine.MessageResponse" 144 | } 145 | } 146 | } 147 | }, 148 | "post": { 149 | "description": "Create new medicine on the system", 150 | "consumes": [ 151 | "application/json" 152 | ], 153 | "produces": [ 154 | "application/json" 155 | ], 156 | "tags": [ 157 | "medicine" 158 | ], 159 | "summary": "Create New Medicine", 160 | "parameters": [ 161 | { 162 | "description": "body data", 163 | "name": "data", 164 | "in": "body", 165 | "required": true, 166 | "schema": { 167 | "$ref": "#/definitions/medicine.NewMedicineRequest" 168 | } 169 | } 170 | ], 171 | "responses": { 172 | "200": { 173 | "description": "OK", 174 | "schema": { 175 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine" 176 | } 177 | }, 178 | "400": { 179 | "description": "Bad Request", 180 | "schema": { 181 | "$ref": "#/definitions/medicine.MessageResponse" 182 | } 183 | }, 184 | "500": { 185 | "description": "Internal Server Error", 186 | "schema": { 187 | "$ref": "#/definitions/medicine.MessageResponse" 188 | } 189 | } 190 | } 191 | } 192 | }, 193 | "/medicine/{medicine_id}": { 194 | "get": { 195 | "description": "Get Medicines by ID on the system", 196 | "tags": [ 197 | "medicine" 198 | ], 199 | "summary": "Get medicines by ID", 200 | "parameters": [ 201 | { 202 | "type": "integer", 203 | "description": "id of medicine", 204 | "name": "medicine_id", 205 | "in": "path", 206 | "required": true 207 | } 208 | ], 209 | "responses": { 210 | "200": { 211 | "description": "OK", 212 | "schema": { 213 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine" 214 | } 215 | }, 216 | "400": { 217 | "description": "Bad Request", 218 | "schema": { 219 | "$ref": "#/definitions/medicine.MessageResponse" 220 | } 221 | }, 222 | "500": { 223 | "description": "Internal Server Error", 224 | "schema": { 225 | "$ref": "#/definitions/medicine.MessageResponse" 226 | } 227 | } 228 | } 229 | } 230 | }, 231 | "/user": { 232 | "get": { 233 | "security": [ 234 | { 235 | "ApiKeyAuth": [] 236 | } 237 | ], 238 | "description": "Get all Users on the system", 239 | "tags": [ 240 | "user" 241 | ], 242 | "summary": "Get all Users", 243 | "responses": { 244 | "200": { 245 | "description": "OK", 246 | "schema": { 247 | "type": "array", 248 | "items": { 249 | "$ref": "#/definitions/user.ResponseUser" 250 | } 251 | } 252 | }, 253 | "400": { 254 | "description": "Bad Request", 255 | "schema": { 256 | "$ref": "#/definitions/user.MessageResponse" 257 | } 258 | }, 259 | "500": { 260 | "description": "Internal Server Error", 261 | "schema": { 262 | "$ref": "#/definitions/user.MessageResponse" 263 | } 264 | } 265 | } 266 | }, 267 | "post": { 268 | "security": [ 269 | { 270 | "ApiKeyAuth": [] 271 | } 272 | ], 273 | "description": "Create new user on the system", 274 | "consumes": [ 275 | "application/json" 276 | ], 277 | "produces": [ 278 | "application/json" 279 | ], 280 | "tags": [ 281 | "user" 282 | ], 283 | "summary": "Create New UserName", 284 | "parameters": [ 285 | { 286 | "description": "body data", 287 | "name": "data", 288 | "in": "body", 289 | "required": true, 290 | "schema": { 291 | "$ref": "#/definitions/user.NewUserRequest" 292 | } 293 | } 294 | ], 295 | "responses": { 296 | "200": { 297 | "description": "OK", 298 | "schema": { 299 | "$ref": "#/definitions/user.ResponseUser" 300 | } 301 | }, 302 | "400": { 303 | "description": "Bad Request", 304 | "schema": { 305 | "$ref": "#/definitions/user.MessageResponse" 306 | } 307 | }, 308 | "500": { 309 | "description": "Internal Server Error", 310 | "schema": { 311 | "$ref": "#/definitions/user.MessageResponse" 312 | } 313 | } 314 | } 315 | } 316 | }, 317 | "/user/{user_id}": { 318 | "get": { 319 | "security": [ 320 | { 321 | "ApiKeyAuth": [] 322 | } 323 | ], 324 | "description": "Get Users by ID on the system", 325 | "tags": [ 326 | "user" 327 | ], 328 | "summary": "Get users by ID", 329 | "parameters": [ 330 | { 331 | "type": "integer", 332 | "description": "id of user", 333 | "name": "user_id", 334 | "in": "path", 335 | "required": true 336 | } 337 | ], 338 | "responses": { 339 | "200": { 340 | "description": "OK", 341 | "schema": { 342 | "$ref": "#/definitions/user.MessageResponse" 343 | } 344 | }, 345 | "400": { 346 | "description": "Bad Request", 347 | "schema": { 348 | "$ref": "#/definitions/user.MessageResponse" 349 | } 350 | }, 351 | "500": { 352 | "description": "Internal Server Error", 353 | "schema": { 354 | "$ref": "#/definitions/user.MessageResponse" 355 | } 356 | } 357 | } 358 | } 359 | } 360 | }, 361 | "definitions": { 362 | "auth.AccessTokenRequest": { 363 | "type": "object", 364 | "required": [ 365 | "refreshToken" 366 | ], 367 | "properties": { 368 | "refreshToken": { 369 | "type": "string", 370 | "example": "badbunybabybebe" 371 | } 372 | } 373 | }, 374 | "auth.DataUserAuthenticated": { 375 | "type": "object", 376 | "properties": { 377 | "email": { 378 | "type": "string", 379 | "example": "some@mail.com" 380 | }, 381 | "firstName": { 382 | "type": "string", 383 | "example": "John" 384 | }, 385 | "id": { 386 | "type": "integer", 387 | "example": 123 388 | }, 389 | "lastName": { 390 | "type": "string", 391 | "example": "Doe" 392 | }, 393 | "role": { 394 | "type": "string", 395 | "example": "admin" 396 | }, 397 | "status": { 398 | "type": "boolean", 399 | "example": true 400 | }, 401 | "userName": { 402 | "type": "string", 403 | "example": "UserName" 404 | } 405 | } 406 | }, 407 | "auth.LoginRequest": { 408 | "type": "object", 409 | "required": [ 410 | "email", 411 | "password" 412 | ], 413 | "properties": { 414 | "email": { 415 | "type": "string", 416 | "example": "gbrayhan@gmail.com" 417 | }, 418 | "password": { 419 | "type": "string", 420 | "example": "Password123" 421 | } 422 | } 423 | }, 424 | "controllers.MessageResponse": { 425 | "type": "object", 426 | "properties": { 427 | "message": { 428 | "type": "string" 429 | } 430 | } 431 | }, 432 | "github_com_gbrayhan_microservices-go_src_application_usecases_medicine.PaginationResultMedicine": { 433 | "type": "object", 434 | "properties": { 435 | "current": { 436 | "type": "integer" 437 | }, 438 | "data": { 439 | "type": "array", 440 | "items": { 441 | "$ref": "#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine" 442 | } 443 | }, 444 | "limit": { 445 | "type": "integer" 446 | }, 447 | "nextCursor": { 448 | "type": "integer" 449 | }, 450 | "numPages": { 451 | "type": "integer" 452 | }, 453 | "prevCursor": { 454 | "type": "integer" 455 | }, 456 | "total": { 457 | "type": "integer" 458 | } 459 | } 460 | }, 461 | "github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine": { 462 | "type": "object", 463 | "properties": { 464 | "createdAt": { 465 | "type": "string" 466 | }, 467 | "description": { 468 | "type": "string" 469 | }, 470 | "eanCode": { 471 | "type": "string" 472 | }, 473 | "id": { 474 | "type": "integer" 475 | }, 476 | "laboratory": { 477 | "type": "string" 478 | }, 479 | "name": { 480 | "type": "string" 481 | }, 482 | "updatedAt": { 483 | "type": "string" 484 | } 485 | } 486 | }, 487 | "medicine.MessageResponse": { 488 | "type": "object", 489 | "properties": { 490 | "message": { 491 | "type": "string" 492 | } 493 | } 494 | }, 495 | "medicine.NewMedicineRequest": { 496 | "type": "object", 497 | "required": [ 498 | "description", 499 | "eanCode", 500 | "laboratory", 501 | "name" 502 | ], 503 | "properties": { 504 | "description": { 505 | "type": "string", 506 | "example": "Something" 507 | }, 508 | "eanCode": { 509 | "type": "string", 510 | "example": "122000000021" 511 | }, 512 | "laboratory": { 513 | "type": "string", 514 | "example": "Roche" 515 | }, 516 | "name": { 517 | "type": "string", 518 | "example": "Paracetamol" 519 | } 520 | } 521 | }, 522 | "medicine.ResponseMedicine": { 523 | "type": "object", 524 | "properties": { 525 | "createdAt": { 526 | "type": "string", 527 | "example": "2021-02-24 20:19:39" 528 | }, 529 | "description": { 530 | "type": "string", 531 | "example": "Some Description" 532 | }, 533 | "eanCode": { 534 | "type": "string", 535 | "example": "Some EanCode" 536 | }, 537 | "id": { 538 | "type": "integer", 539 | "example": 1099 540 | }, 541 | "laboratory": { 542 | "type": "string", 543 | "example": "Some Laboratory" 544 | }, 545 | "name": { 546 | "type": "string", 547 | "example": "Aspirina" 548 | }, 549 | "updatedAt": { 550 | "type": "string", 551 | "example": "2021-02-24 20:19:39" 552 | } 553 | } 554 | }, 555 | "user.MessageResponse": { 556 | "type": "object", 557 | "properties": { 558 | "message": { 559 | "type": "string" 560 | } 561 | } 562 | }, 563 | "user.NewUserRequest": { 564 | "type": "object", 565 | "required": [ 566 | "email", 567 | "firstName", 568 | "lastName", 569 | "password", 570 | "role", 571 | "user" 572 | ], 573 | "properties": { 574 | "email": { 575 | "type": "string", 576 | "example": "mail@mail.com" 577 | }, 578 | "firstName": { 579 | "type": "string", 580 | "example": "John" 581 | }, 582 | "lastName": { 583 | "type": "string", 584 | "example": "Doe" 585 | }, 586 | "password": { 587 | "type": "string", 588 | "example": "Password123" 589 | }, 590 | "role": { 591 | "type": "string", 592 | "example": "admin" 593 | }, 594 | "user": { 595 | "type": "string", 596 | "example": "someUser" 597 | } 598 | } 599 | }, 600 | "user.ResponseUser": { 601 | "type": "object", 602 | "properties": { 603 | "createdAt": { 604 | "type": "string", 605 | "example": "2021-02-24 20:19:39" 606 | }, 607 | "email": { 608 | "type": "string", 609 | "example": "some@mail.com" 610 | }, 611 | "firstName": { 612 | "type": "string", 613 | "example": "John" 614 | }, 615 | "id": { 616 | "type": "integer", 617 | "example": 1099 618 | }, 619 | "lastName": { 620 | "type": "string", 621 | "example": "Doe" 622 | }, 623 | "status": { 624 | "type": "boolean", 625 | "example": false 626 | }, 627 | "updatedAt": { 628 | "type": "string", 629 | "example": "2021-02-24 20:19:39" 630 | }, 631 | "user": { 632 | "type": "string", 633 | "example": "BossonH" 634 | } 635 | } 636 | } 637 | }, 638 | "securityDefinitions": { 639 | "ApiKeyAuth": { 640 | "type": "apiKey", 641 | "name": "Authorization", 642 | "in": "header" 643 | } 644 | } 645 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /v1 2 | definitions: 3 | auth.AccessTokenRequest: 4 | properties: 5 | refreshToken: 6 | example: badbunybabybebe 7 | type: string 8 | required: 9 | - refreshToken 10 | type: object 11 | auth.DataUserAuthenticated: 12 | properties: 13 | email: 14 | example: some@mail.com 15 | type: string 16 | firstName: 17 | example: John 18 | type: string 19 | id: 20 | example: 123 21 | type: integer 22 | lastName: 23 | example: Doe 24 | type: string 25 | role: 26 | example: admin 27 | type: string 28 | status: 29 | example: true 30 | type: boolean 31 | userName: 32 | example: UserName 33 | type: string 34 | type: object 35 | auth.LoginRequest: 36 | properties: 37 | email: 38 | example: gbrayhan@gmail.com 39 | type: string 40 | password: 41 | example: Password123 42 | type: string 43 | required: 44 | - email 45 | - password 46 | type: object 47 | controllers.MessageResponse: 48 | properties: 49 | message: 50 | type: string 51 | type: object 52 | github_com_gbrayhan_microservices-go_src_application_usecases_medicine.PaginationResultMedicine: 53 | properties: 54 | current: 55 | type: integer 56 | data: 57 | items: 58 | $ref: '#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine' 59 | type: array 60 | limit: 61 | type: integer 62 | nextCursor: 63 | type: integer 64 | numPages: 65 | type: integer 66 | prevCursor: 67 | type: integer 68 | total: 69 | type: integer 70 | type: object 71 | github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine: 72 | properties: 73 | createdAt: 74 | type: string 75 | description: 76 | type: string 77 | eanCode: 78 | type: string 79 | id: 80 | type: integer 81 | laboratory: 82 | type: string 83 | name: 84 | type: string 85 | updatedAt: 86 | type: string 87 | type: object 88 | medicine.MessageResponse: 89 | properties: 90 | message: 91 | type: string 92 | type: object 93 | medicine.NewMedicineRequest: 94 | properties: 95 | description: 96 | example: Something 97 | type: string 98 | eanCode: 99 | example: "122000000021" 100 | type: string 101 | laboratory: 102 | example: Roche 103 | type: string 104 | name: 105 | example: Paracetamol 106 | type: string 107 | required: 108 | - description 109 | - eanCode 110 | - laboratory 111 | - name 112 | type: object 113 | medicine.ResponseMedicine: 114 | properties: 115 | createdAt: 116 | example: "2021-02-24 20:19:39" 117 | type: string 118 | description: 119 | example: Some Description 120 | type: string 121 | eanCode: 122 | example: Some EanCode 123 | type: string 124 | id: 125 | example: 1099 126 | type: integer 127 | laboratory: 128 | example: Some Laboratory 129 | type: string 130 | name: 131 | example: Aspirina 132 | type: string 133 | updatedAt: 134 | example: "2021-02-24 20:19:39" 135 | type: string 136 | type: object 137 | user.MessageResponse: 138 | properties: 139 | message: 140 | type: string 141 | type: object 142 | user.NewUserRequest: 143 | properties: 144 | email: 145 | example: mail@mail.com 146 | type: string 147 | firstName: 148 | example: John 149 | type: string 150 | lastName: 151 | example: Doe 152 | type: string 153 | password: 154 | example: Password123 155 | type: string 156 | role: 157 | example: admin 158 | type: string 159 | user: 160 | example: someUser 161 | type: string 162 | required: 163 | - email 164 | - firstName 165 | - lastName 166 | - password 167 | - role 168 | - user 169 | type: object 170 | user.ResponseUser: 171 | properties: 172 | createdAt: 173 | example: "2021-02-24 20:19:39" 174 | type: string 175 | email: 176 | example: some@mail.com 177 | type: string 178 | firstName: 179 | example: John 180 | type: string 181 | id: 182 | example: 1099 183 | type: integer 184 | lastName: 185 | example: Doe 186 | type: string 187 | status: 188 | example: false 189 | type: boolean 190 | updatedAt: 191 | example: "2021-02-24 20:19:39" 192 | type: string 193 | user: 194 | example: BossonH 195 | type: string 196 | type: object 197 | host: localhost:8080 198 | info: 199 | contact: 200 | email: gbrayhan@gmail.com 201 | name: Alejandro Gabriel Guerrero 202 | url: http://github.com/gbrayhan 203 | description: Documentation's Boilerplate Golang 204 | license: 205 | name: Apache 2.0 206 | url: http://www.apache.org/licenses/LICENSE-2.0.html 207 | termsOfService: http://swagger.io/terms/ 208 | title: Boilerplate Golang 209 | version: "1.2" 210 | paths: 211 | /auth/access-token: 212 | post: 213 | description: Auth user by email and password 214 | parameters: 215 | - description: body data 216 | in: body 217 | name: data 218 | required: true 219 | schema: 220 | $ref: '#/definitions/auth.AccessTokenRequest' 221 | responses: 222 | "200": 223 | description: OK 224 | schema: 225 | $ref: '#/definitions/auth.DataUserAuthenticated' 226 | "400": 227 | description: Bad Request 228 | schema: 229 | $ref: '#/definitions/controllers.MessageResponse' 230 | "500": 231 | description: Internal Server Error 232 | schema: 233 | $ref: '#/definitions/controllers.MessageResponse' 234 | summary: GetAccessTokenByRefreshToken UserName 235 | tags: 236 | - auth 237 | /auth/login: 238 | post: 239 | description: Auth user by email and password 240 | parameters: 241 | - description: body data 242 | in: body 243 | name: data 244 | required: true 245 | schema: 246 | $ref: '#/definitions/auth.LoginRequest' 247 | responses: 248 | "200": 249 | description: OK 250 | schema: 251 | $ref: '#/definitions/auth.DataUserAuthenticated' 252 | "400": 253 | description: Bad Request 254 | schema: 255 | $ref: '#/definitions/controllers.MessageResponse' 256 | "500": 257 | description: Internal Server Error 258 | schema: 259 | $ref: '#/definitions/controllers.MessageResponse' 260 | summary: Login UserName 261 | tags: 262 | - auth 263 | /medicine: 264 | get: 265 | description: Get all Medicines on the system 266 | parameters: 267 | - description: limit 268 | in: query 269 | name: limit 270 | required: true 271 | type: string 272 | - description: page 273 | in: query 274 | name: page 275 | required: true 276 | type: string 277 | responses: 278 | "200": 279 | description: OK 280 | schema: 281 | items: 282 | $ref: '#/definitions/github_com_gbrayhan_microservices-go_src_application_usecases_medicine.PaginationResultMedicine' 283 | type: array 284 | "400": 285 | description: Bad Request 286 | schema: 287 | $ref: '#/definitions/medicine.MessageResponse' 288 | "500": 289 | description: Internal Server Error 290 | schema: 291 | $ref: '#/definitions/medicine.MessageResponse' 292 | summary: Get all Medicines 293 | tags: 294 | - medicine 295 | post: 296 | consumes: 297 | - application/json 298 | description: Create new medicine on the system 299 | parameters: 300 | - description: body data 301 | in: body 302 | name: data 303 | required: true 304 | schema: 305 | $ref: '#/definitions/medicine.NewMedicineRequest' 306 | produces: 307 | - application/json 308 | responses: 309 | "200": 310 | description: OK 311 | schema: 312 | $ref: '#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine' 313 | "400": 314 | description: Bad Request 315 | schema: 316 | $ref: '#/definitions/medicine.MessageResponse' 317 | "500": 318 | description: Internal Server Error 319 | schema: 320 | $ref: '#/definitions/medicine.MessageResponse' 321 | summary: Create New Medicine 322 | tags: 323 | - medicine 324 | /medicine/{medicine_id}: 325 | get: 326 | description: Get Medicines by ID on the system 327 | parameters: 328 | - description: id of medicine 329 | in: path 330 | name: medicine_id 331 | required: true 332 | type: integer 333 | responses: 334 | "200": 335 | description: OK 336 | schema: 337 | $ref: '#/definitions/github_com_gbrayhan_microservices-go_src_domain_medicine.Medicine' 338 | "400": 339 | description: Bad Request 340 | schema: 341 | $ref: '#/definitions/medicine.MessageResponse' 342 | "500": 343 | description: Internal Server Error 344 | schema: 345 | $ref: '#/definitions/medicine.MessageResponse' 346 | summary: Get medicines by ID 347 | tags: 348 | - medicine 349 | /user: 350 | get: 351 | description: Get all Users on the system 352 | responses: 353 | "200": 354 | description: OK 355 | schema: 356 | items: 357 | $ref: '#/definitions/user.ResponseUser' 358 | type: array 359 | "400": 360 | description: Bad Request 361 | schema: 362 | $ref: '#/definitions/user.MessageResponse' 363 | "500": 364 | description: Internal Server Error 365 | schema: 366 | $ref: '#/definitions/user.MessageResponse' 367 | security: 368 | - ApiKeyAuth: [] 369 | summary: Get all Users 370 | tags: 371 | - user 372 | post: 373 | consumes: 374 | - application/json 375 | description: Create new user on the system 376 | parameters: 377 | - description: body data 378 | in: body 379 | name: data 380 | required: true 381 | schema: 382 | $ref: '#/definitions/user.NewUserRequest' 383 | produces: 384 | - application/json 385 | responses: 386 | "200": 387 | description: OK 388 | schema: 389 | $ref: '#/definitions/user.ResponseUser' 390 | "400": 391 | description: Bad Request 392 | schema: 393 | $ref: '#/definitions/user.MessageResponse' 394 | "500": 395 | description: Internal Server Error 396 | schema: 397 | $ref: '#/definitions/user.MessageResponse' 398 | security: 399 | - ApiKeyAuth: [] 400 | summary: Create New UserName 401 | tags: 402 | - user 403 | /user/{user_id}: 404 | get: 405 | description: Get Users by ID on the system 406 | parameters: 407 | - description: id of user 408 | in: path 409 | name: user_id 410 | required: true 411 | type: integer 412 | responses: 413 | "200": 414 | description: OK 415 | schema: 416 | $ref: '#/definitions/user.MessageResponse' 417 | "400": 418 | description: Bad Request 419 | schema: 420 | $ref: '#/definitions/user.MessageResponse' 421 | "500": 422 | description: Internal Server Error 423 | schema: 424 | $ref: '#/definitions/user.MessageResponse' 425 | security: 426 | - ApiKeyAuth: [] 427 | summary: Get users by ID 428 | tags: 429 | - user 430 | securityDefinitions: 431 | ApiKeyAuth: 432 | in: header 433 | name: Authorization 434 | type: apiKey 435 | swagger: "2.0" 436 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gbrayhan/microservices-go 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.7.5 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/go-playground/validator/v10 v10.26.0 9 | github.com/golang-jwt/jwt/v4 v4.5.2 10 | github.com/swaggo/files v1.0.1 11 | github.com/swaggo/gin-swagger v1.6.0 12 | github.com/swaggo/swag v1.16.4 13 | golang.org/x/crypto v0.37.0 14 | gorm.io/driver/postgres v1.5.11 15 | gorm.io/gorm v1.26.0 16 | ) 17 | 18 | require ( 19 | github.com/KyleBanks/depth v1.2.1 // indirect 20 | github.com/bytedance/sonic v1.13.2 // indirect 21 | github.com/bytedance/sonic/loader v0.2.4 // indirect 22 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 23 | github.com/chenzhuoyu/iasm v0.9.1 // indirect 24 | github.com/cloudwego/base64x v0.1.5 // indirect 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 26 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 27 | github.com/gin-contrib/sse v1.1.0 // indirect 28 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 29 | github.com/go-openapi/jsonreference v0.21.0 // indirect 30 | github.com/go-openapi/spec v0.21.0 // indirect 31 | github.com/go-openapi/swag v0.23.1 // indirect 32 | github.com/go-playground/locales v0.14.1 // indirect 33 | github.com/go-playground/universal-translator v0.18.1 // indirect 34 | github.com/goccy/go-json v0.10.5 // indirect 35 | github.com/jackc/pgpassfile v1.0.0 // indirect 36 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 37 | github.com/jackc/pgx/v5 v5.7.4 // indirect 38 | github.com/jackc/puddle/v2 v2.2.2 // indirect 39 | github.com/jinzhu/inflection v1.0.0 // indirect 40 | github.com/jinzhu/now v1.1.5 // indirect 41 | github.com/josharian/intern v1.0.0 // indirect 42 | github.com/json-iterator/go v1.1.12 // indirect 43 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 44 | github.com/leodido/go-urn v1.4.0 // indirect 45 | github.com/mailru/easyjson v0.9.0 // indirect 46 | github.com/mattn/go-isatty v0.0.20 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 50 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 51 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 52 | github.com/ugorji/go/codec v1.2.12 // indirect 53 | golang.org/x/arch v0.16.0 // indirect 54 | golang.org/x/net v0.39.0 // indirect 55 | golang.org/x/sync v0.13.0 // indirect 56 | golang.org/x/sys v0.32.0 // indirect 57 | golang.org/x/text v0.24.0 // indirect 58 | golang.org/x/tools v0.32.0 // indirect 59 | google.golang.org/protobuf v1.36.6 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 4 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 5 | github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A= 6 | github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= 7 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 8 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 9 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 10 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 11 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 12 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 13 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 14 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 15 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= 16 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 17 | github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= 18 | github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 19 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 20 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 21 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 27 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 28 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 29 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 30 | github.com/gin-contrib/cors v1.6.0 h1:0Z7D/bVhE6ja07lI8CTjTonp6SB07o8bNuFyRbsBUQg= 31 | github.com/gin-contrib/cors v1.6.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro= 32 | github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk= 33 | github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0= 34 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 35 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 36 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 37 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 38 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 39 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 40 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 41 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 42 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 43 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 44 | github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= 45 | github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 46 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 47 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 48 | github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= 49 | github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= 50 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 51 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 52 | github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= 53 | github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= 54 | github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= 55 | github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= 56 | github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= 57 | github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= 58 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 59 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 60 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 61 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 62 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 63 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 64 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 65 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 66 | github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= 67 | github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 68 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 69 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 70 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 71 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 72 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 73 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 74 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 75 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 76 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 77 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 78 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 79 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 80 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 81 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 82 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 83 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 84 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 85 | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 86 | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 87 | github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= 88 | github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 89 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 90 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 91 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 92 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 93 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 94 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 95 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 96 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 97 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 98 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 99 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 100 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 101 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 102 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 103 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 104 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 105 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 106 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 107 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 108 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 109 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 110 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 111 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 112 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 113 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 114 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 115 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 116 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 117 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 118 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 119 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 120 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 121 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 122 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 123 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 124 | github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= 125 | github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 126 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 127 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 128 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 129 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 130 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 131 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 132 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 133 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 134 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 135 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 136 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 137 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 138 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 139 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 140 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 141 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 142 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 143 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 144 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 145 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 146 | github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= 147 | github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= 148 | github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= 149 | github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= 150 | github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= 151 | github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= 152 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 153 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 154 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 155 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 156 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 157 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 158 | golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= 159 | golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 160 | golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= 161 | golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 162 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 163 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 164 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 165 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 166 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 167 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 168 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 169 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 170 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 171 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 172 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 174 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 175 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 176 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 177 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 178 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 179 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 180 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 183 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 184 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 185 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 186 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 187 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 194 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 195 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 196 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 197 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 198 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 199 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 200 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 201 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 202 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 203 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 204 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 205 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 206 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 207 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 208 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 209 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 210 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 211 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 212 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 213 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 214 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 215 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 216 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 217 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 218 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 219 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 220 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 221 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 222 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 223 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 224 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 225 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 226 | gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= 227 | gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= 228 | gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= 229 | gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 230 | gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs= 231 | gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= 232 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 233 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 234 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gbrayhan/microservices-go/src/infrastructure/repository" 6 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/middlewares" 7 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/routes" 8 | "github.com/gin-contrib/cors" 9 | "github.com/gin-gonic/gin" 10 | "net/http" 11 | "os" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | router := gin.Default() 18 | router.Use(cors.Default()) 19 | DB, err := repository.InitDB() 20 | if err != nil { 21 | panic(fmt.Errorf("error initializing the database: %w", err)) 22 | } 23 | 24 | router.Use(middlewares.ErrorHandler()) 25 | router.Use(middlewares.GinBodyLogMiddleware) 26 | router.Use(middlewares.CommonHeaders) 27 | routes.ApplicationRouter(router, DB) 28 | 29 | port := os.Getenv("SERVER_PORT") 30 | if port == "" { 31 | port = "8080" 32 | } 33 | s := &http.Server{ 34 | Addr: ":" + port, 35 | Handler: router, 36 | ReadTimeout: 18000 * time.Second, 37 | WriteTimeout: 18000 * time.Second, 38 | MaxHeaderBytes: 1 << 20, 39 | } 40 | fmt.Printf("Server running at http://localhost:%s\n", port) 41 | if err := s.ListenAndServe(); err != nil { 42 | panic(strings.ToLower(err.Error())) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /show-content.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | exec > project-content.txt 2>&1 3 | 4 | BLACKLIST_DIRS=( 5 | ".github" 6 | "docs" 7 | "postman" 8 | "path/another_directory" 9 | ) 10 | 11 | BLACKLIST_FILES=( 12 | ".dockerignore" 13 | "CODE_OF_CONDUCT.md" 14 | "README.md" 15 | "SECURITY.md" 16 | "CONTRIBUTING.md" 17 | "LICENSE" 18 | "show-content.bash" 19 | "go.mod" 20 | "go.sum" 21 | ".gitignore" 22 | ) 23 | 24 | BLACKLIST_EXTENSIONS=( 25 | "md" 26 | "txt" 27 | "pdf" 28 | "log" 29 | ) 30 | 31 | is_in_blacklisted_dir() { 32 | local f="$1" 33 | for d in "${BLACKLIST_DIRS[@]}"; do 34 | [[ "$f" == "$d/"* ]] && return 0 35 | done 36 | return 1 37 | } 38 | 39 | is_blacklisted_file() { 40 | local f="$1" 41 | for b in "${BLACKLIST_FILES[@]}"; do 42 | [[ "$f" == "$b" ]] && return 0 43 | done 44 | return 1 45 | } 46 | 47 | is_blacklisted_extension() { 48 | local f="$1" 49 | [[ "$f" != *.* ]] && return 1 50 | local ext="${f##*.}" 51 | ext="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')" 52 | for b in "${BLACKLIST_EXTENSIONS[@]}"; do 53 | [[ "$ext" == "$b" ]] && return 0 54 | done 55 | return 1 56 | } 57 | 58 | if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then 59 | echo "This script must be run inside a Git repository." 60 | exit 1 61 | fi 62 | 63 | git ls-files --cached --others --exclude-standard -z | 64 | while IFS= read -r -d '' file; do 65 | if is_in_blacklisted_dir "$file" || is_blacklisted_file "$file" || is_blacklisted_extension "$file"; then 66 | continue 67 | fi 68 | 69 | echo "File: $file" 70 | echo "Content:" 71 | echo "---" 72 | cat "$file" 73 | echo "---" 74 | echo 75 | done 76 | -------------------------------------------------------------------------------- /src/application/usecases/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | errorsDomain "github.com/gbrayhan/microservices-go/src/domain/errors" 6 | userDomain "github.com/gbrayhan/microservices-go/src/domain/user" 7 | jwtInfrastructure "github.com/gbrayhan/microservices-go/src/infrastructure/security/jwt" 8 | "golang.org/x/crypto/bcrypt" 9 | "time" 10 | ) 11 | 12 | type IAuthUseCase interface { 13 | Login(user LoginUser) (*SecurityAuthenticatedUser, error) 14 | AccessTokenByRefreshToken(refreshToken string) (*SecurityAuthenticatedUser, error) 15 | } 16 | 17 | type AuthUseCase struct { 18 | userRepository userDomain.IUserService 19 | } 20 | 21 | func NewAuthUseCase(userRepository userDomain.IUserService) IAuthUseCase { 22 | return &AuthUseCase{ 23 | userRepository: userRepository, 24 | } 25 | } 26 | 27 | type Auth struct { 28 | AccessToken string 29 | RefreshToken string 30 | ExpirationAccessDateTime time.Time 31 | ExpirationRefreshDateTime time.Time 32 | } 33 | 34 | func (s *AuthUseCase) Login(user LoginUser) (*SecurityAuthenticatedUser, error) { 35 | userMap := map[string]interface{}{"email": user.Email} 36 | domainUser, err := s.userRepository.GetOneByMap(userMap) 37 | if err != nil { 38 | return &SecurityAuthenticatedUser{}, err 39 | } 40 | if domainUser.ID == 0 { 41 | return &SecurityAuthenticatedUser{}, errorsDomain.NewAppError(errors.New("email or password does not match"), errorsDomain.NotAuthorized) 42 | } 43 | 44 | isAuthenticated := checkPasswordHash(user.Password, domainUser.HashPassword) 45 | if !isAuthenticated { 46 | return &SecurityAuthenticatedUser{}, errorsDomain.NewAppError(errors.New("email or password does not match"), errorsDomain.NotAuthorized) 47 | } 48 | 49 | accessTokenClaims, err := jwtInfrastructure.GenerateJWTToken(domainUser.ID, "access") 50 | if err != nil { 51 | return &SecurityAuthenticatedUser{}, err 52 | } 53 | refreshTokenClaims, err := jwtInfrastructure.GenerateJWTToken(domainUser.ID, "refresh") 54 | if err != nil { 55 | return &SecurityAuthenticatedUser{}, err 56 | } 57 | 58 | return secAuthUserMapper(domainUser, &Auth{ 59 | AccessToken: accessTokenClaims.Token, 60 | RefreshToken: refreshTokenClaims.Token, 61 | ExpirationAccessDateTime: accessTokenClaims.ExpirationTime, 62 | ExpirationRefreshDateTime: refreshTokenClaims.ExpirationTime, 63 | }), nil 64 | } 65 | 66 | func (s *AuthUseCase) AccessTokenByRefreshToken(refreshToken string) (*SecurityAuthenticatedUser, error) { 67 | claimsMap, err := jwtInfrastructure.GetClaimsAndVerifyToken(refreshToken, "refresh") 68 | if err != nil { 69 | return nil, err 70 | } 71 | userMap := map[string]interface{}{"id": claimsMap["id"]} 72 | domainUser, err := s.userRepository.GetOneByMap(userMap) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | accessTokenClaims, err := jwtInfrastructure.GenerateJWTToken(domainUser.ID, "access") 78 | if err != nil { 79 | return &SecurityAuthenticatedUser{}, err 80 | } 81 | 82 | var expTime = int64(claimsMap["exp"].(float64)) 83 | 84 | return secAuthUserMapper(domainUser, &Auth{ 85 | AccessToken: accessTokenClaims.Token, 86 | ExpirationAccessDateTime: accessTokenClaims.ExpirationTime, 87 | RefreshToken: refreshToken, 88 | ExpirationRefreshDateTime: time.Unix(expTime, 0), 89 | }), nil 90 | } 91 | 92 | func checkPasswordHash(password, hash string) bool { 93 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 94 | return err == nil 95 | } 96 | -------------------------------------------------------------------------------- /src/application/usecases/auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | errorsDomain "github.com/gbrayhan/microservices-go/src/domain/errors" 6 | userDomain "github.com/gbrayhan/microservices-go/src/domain/user" 7 | jwtInfrastructure "github.com/gbrayhan/microservices-go/src/infrastructure/security/jwt" 8 | "golang.org/x/crypto/bcrypt" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | type mockUserService struct { 14 | getOneByMapFn func(map[string]interface{}) (*userDomain.User, error) 15 | callGetOneByMapCalled bool 16 | } 17 | 18 | func (m *mockUserService) GetAll() (*[]userDomain.User, error) { 19 | return nil, nil 20 | } 21 | func (m *mockUserService) GetByID(id int) (*userDomain.User, error) { 22 | return nil, nil 23 | } 24 | func (m *mockUserService) Create(newUser *userDomain.User) (*userDomain.User, error) { 25 | return nil, nil 26 | } 27 | func (m *mockUserService) GetOneByMap(userMap map[string]interface{}) (*userDomain.User, error) { 28 | m.callGetOneByMapCalled = true 29 | return m.getOneByMapFn(userMap) 30 | } 31 | func (m *mockUserService) Delete(id int) error { 32 | return nil 33 | } 34 | func (m *mockUserService) Update(id int, userMap map[string]interface{}) (*userDomain.User, error) { 35 | return nil, nil 36 | } 37 | 38 | func HashPasswordForTest(plain string) (string, error) { 39 | hashedBytes, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) 40 | if err != nil { 41 | return "", err 42 | } 43 | return string(hashedBytes), nil 44 | } 45 | 46 | func TestCheckPasswordHash(t *testing.T) { 47 | password := "mySecretPass" 48 | hashed, err := HashPasswordForTest(password) 49 | if err != nil { 50 | t.Fatalf("failed to generate hash for test: %v", err) 51 | } 52 | 53 | if ok := checkPasswordHash(password, hashed); !ok { 54 | t.Errorf("checkPasswordHash() = false, want true") 55 | } 56 | 57 | if ok := checkPasswordHash("wrongPassword", hashed); ok { 58 | t.Errorf("checkPasswordHash() = true, want false") 59 | } 60 | } 61 | 62 | func TestAuthUseCase_Login(t *testing.T) { 63 | os.Setenv("JWT_ACCESS_SECRET", "test_access_secret") 64 | os.Setenv("JWT_ACCESS_TIME_MINUTE", "60") 65 | os.Setenv("JWT_REFRESH_SECRET", "test_refresh_secret") 66 | os.Setenv("JWT_REFRESH_TIME_HOUR", "24") 67 | 68 | tests := []struct { 69 | name string 70 | mockGetOneByMapFn func(map[string]interface{}) (*userDomain.User, error) 71 | inputLogin LoginUser 72 | wantErr bool 73 | wantErrType string 74 | wantEmptySecurity bool 75 | wantSuccessAccessToken bool 76 | }{ 77 | { 78 | name: "Error fetching user from DB", 79 | mockGetOneByMapFn: func(m map[string]interface{}) (*userDomain.User, error) { 80 | return nil, errors.New("db error") 81 | }, 82 | inputLogin: LoginUser{Email: "test@example.com", Password: "123456"}, 83 | wantErr: true, 84 | }, 85 | { 86 | name: "User not found (ID=0)", 87 | mockGetOneByMapFn: func(m map[string]interface{}) (*userDomain.User, error) { 88 | return &userDomain.User{ID: 0}, nil 89 | }, 90 | inputLogin: LoginUser{Email: "test@example.com", Password: "123456"}, 91 | wantErr: true, 92 | wantErrType: errorsDomain.NotAuthorized, 93 | }, 94 | { 95 | name: "Incorrect password", 96 | mockGetOneByMapFn: func(m map[string]interface{}) (*userDomain.User, error) { 97 | hashed, _ := HashPasswordForTest("someOtherPass") 98 | return &userDomain.User{ID: 10, HashPassword: hashed}, nil 99 | }, 100 | inputLogin: LoginUser{Email: "test@example.com", Password: "wrong"}, 101 | wantErr: true, 102 | wantErrType: errorsDomain.NotAuthorized, 103 | wantEmptySecurity: true, 104 | }, 105 | { 106 | name: "Access token generation fails", 107 | mockGetOneByMapFn: func(m map[string]interface{}) (*userDomain.User, error) { 108 | hashed, _ := HashPasswordForTest("somePass") 109 | return &userDomain.User{ID: 10, HashPassword: hashed}, nil 110 | }, 111 | inputLogin: LoginUser{Email: "test@example.com", Password: "somePass"}, 112 | wantErr: true, 113 | }, 114 | { 115 | name: "OK - everything correct", 116 | mockGetOneByMapFn: func(m map[string]interface{}) (*userDomain.User, error) { 117 | hashed, _ := HashPasswordForTest("mySecretPass") 118 | return &userDomain.User{ 119 | ID: 10, 120 | Email: "test@example.com", 121 | HashPassword: hashed, 122 | }, nil 123 | }, 124 | inputLogin: LoginUser{Email: "test@example.com", Password: "mySecretPass"}, 125 | wantErr: false, 126 | wantSuccessAccessToken: true, 127 | }, 128 | } 129 | 130 | for _, tt := range tests { 131 | t.Run(tt.name, func(t *testing.T) { 132 | userRepoMock := &mockUserService{ 133 | getOneByMapFn: tt.mockGetOneByMapFn, 134 | } 135 | uc := NewAuthUseCase(userRepoMock) 136 | 137 | if tt.name == "Access token generation fails" { 138 | os.Setenv("JWT_ACCESS_TIME_MINUTE", "") 139 | } 140 | 141 | result, err := uc.Login(tt.inputLogin) 142 | if (err != nil) != tt.wantErr { 143 | t.Fatalf("[%s] got err = %v, wantErr = %v", tt.name, err, tt.wantErr) 144 | } 145 | 146 | if tt.wantErrType != "" && err != nil { 147 | appErr, ok := err.(*errorsDomain.AppError) 148 | if !ok || appErr.Type != tt.wantErrType { 149 | t.Errorf("[%s] expected error type = %s, got = %v", tt.name, tt.wantErrType, err) 150 | } 151 | } 152 | 153 | if !tt.wantErr && tt.wantSuccessAccessToken { 154 | if result.Security.JWTAccessToken == "" { 155 | t.Errorf("[%s] expected a non-empty AccessToken, got empty", tt.name) 156 | } 157 | } else if tt.wantErr && tt.wantEmptySecurity { 158 | if result.Security.JWTAccessToken != "" { 159 | t.Errorf("[%s] expected empty AccessToken, but got a non-empty one", tt.name) 160 | } 161 | } 162 | 163 | if tt.name == "Access token generation fails" { 164 | os.Setenv("JWT_ACCESS_TIME_MINUTE", "60") 165 | } 166 | }) 167 | } 168 | } 169 | 170 | func TestAuthUseCase_AccessTokenByRefreshToken(t *testing.T) { 171 | os.Setenv("JWT_REFRESH_SECRET", "test_refresh_secret") 172 | os.Setenv("JWT_REFRESH_TIME_HOUR", "24") 173 | os.Setenv("JWT_ACCESS_SECRET", "test_access_secret") 174 | os.Setenv("JWT_ACCESS_TIME_MINUTE", "60") 175 | 176 | validRefresh, _ := jwtInfrastructure.GenerateJWTToken(123, "refresh") 177 | 178 | tests := []struct { 179 | name string 180 | refreshToken string 181 | mockGetOneByMap func(map[string]interface{}) (*userDomain.User, error) 182 | modifySecretEnv bool 183 | wantErr bool 184 | }{ 185 | { 186 | name: "Invalid token string -> claims error", 187 | refreshToken: "some.invalid.token", 188 | mockGetOneByMap: func(map[string]interface{}) (*userDomain.User, error) { 189 | return &userDomain.User{}, nil 190 | }, 191 | wantErr: true, 192 | }, 193 | { 194 | name: "DB error retrieving user", 195 | refreshToken: validRefresh.Token, 196 | mockGetOneByMap: func(map[string]interface{}) (*userDomain.User, error) { 197 | return nil, errors.New("db error") 198 | }, 199 | wantErr: true, 200 | }, 201 | { 202 | name: "Access token generation error", 203 | refreshToken: validRefresh.Token, 204 | mockGetOneByMap: func(map[string]interface{}) (*userDomain.User, error) { 205 | return &userDomain.User{ID: 999}, nil 206 | }, 207 | modifySecretEnv: true, 208 | wantErr: true, 209 | }, 210 | { 211 | name: "OK - valid refresh token and user found", 212 | refreshToken: validRefresh.Token, 213 | mockGetOneByMap: func(map[string]interface{}) (*userDomain.User, error) { 214 | return &userDomain.User{ID: 999}, nil 215 | }, 216 | wantErr: false, 217 | }, 218 | } 219 | 220 | for _, tt := range tests { 221 | t.Run(tt.name, func(t *testing.T) { 222 | userRepoMock := &mockUserService{ 223 | getOneByMapFn: tt.mockGetOneByMap, 224 | } 225 | uc := NewAuthUseCase(userRepoMock) 226 | 227 | if tt.modifySecretEnv { 228 | os.Setenv("JWT_ACCESS_SECRET", "") 229 | } 230 | 231 | resp, err := uc.AccessTokenByRefreshToken(tt.refreshToken) 232 | if (err != nil) != tt.wantErr { 233 | t.Errorf("[%s] got err = %v, wantErr = %v", tt.name, err, tt.wantErr) 234 | } 235 | if !tt.wantErr && resp.Security.JWTAccessToken == "" { 236 | t.Errorf("[%s] expected a new AccessToken, got empty", tt.name) 237 | } 238 | 239 | if tt.modifySecretEnv { 240 | os.Setenv("JWT_ACCESS_SECRET", "test_access_secret") 241 | } 242 | }) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/application/usecases/auth/mappers.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | userDomain "github.com/gbrayhan/microservices-go/src/domain/user" 5 | ) 6 | 7 | func secAuthUserMapper(domainUser *userDomain.User, authInfo *Auth) *SecurityAuthenticatedUser { 8 | return &SecurityAuthenticatedUser{ 9 | Data: DataUserAuthenticated{ 10 | UserName: domainUser.UserName, 11 | Email: domainUser.Email, 12 | FirstName: domainUser.FirstName, 13 | LastName: domainUser.LastName, 14 | ID: domainUser.ID, 15 | Status: domainUser.Status, 16 | }, 17 | Security: DataSecurityAuthenticated{ 18 | JWTAccessToken: authInfo.AccessToken, 19 | JWTRefreshToken: authInfo.RefreshToken, 20 | ExpirationAccessDateTime: authInfo.ExpirationAccessDateTime, 21 | ExpirationRefreshDateTime: authInfo.ExpirationRefreshDateTime, 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/application/usecases/auth/structures.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "time" 4 | 5 | type LoginUser struct { 6 | Email string 7 | Password string 8 | } 9 | 10 | type DataUserAuthenticated struct { 11 | UserName string `json:"userName"` 12 | Email string `json:"email"` 13 | FirstName string `json:"firstName"` 14 | LastName string `json:"lastName"` 15 | Status bool `json:"status"` 16 | ID int `json:"id"` 17 | } 18 | 19 | type DataSecurityAuthenticated struct { 20 | JWTAccessToken string `json:"jwtAccessToken"` 21 | JWTRefreshToken string `json:"jwtRefreshToken"` 22 | ExpirationAccessDateTime time.Time `json:"expirationAccessDateTime"` 23 | ExpirationRefreshDateTime time.Time `json:"expirationRefreshDateTime"` 24 | } 25 | 26 | type SecurityAuthenticatedUser struct { 27 | Data DataUserAuthenticated `json:"data"` 28 | Security DataSecurityAuthenticated `json:"security"` 29 | } 30 | -------------------------------------------------------------------------------- /src/application/usecases/medicine/medicine.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | "github.com/gbrayhan/microservices-go/src/domain" 5 | domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 6 | ) 7 | 8 | type IMedicineUseCase interface { 9 | GetData(page int64, limit int64, sortBy string, sortDirection string, 10 | filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*domainMedicine.DataMedicine, error) 11 | GetByID(id int) (*domainMedicine.Medicine, error) 12 | Create(medicine *domainMedicine.Medicine) (*domainMedicine.Medicine, error) 13 | GetByMap(medicineMap map[string]any) (*domainMedicine.Medicine, error) 14 | Delete(id int) error 15 | Update(id int, medicineMap map[string]any) (*domainMedicine.Medicine, error) 16 | GetAll() (*[]domainMedicine.Medicine, error) 17 | } 18 | 19 | type MedicineUseCase struct { 20 | MedicineRepository domainMedicine.IMedicineService 21 | } 22 | 23 | func NewMedicineUseCase(medicineRepository domainMedicine.IMedicineService) IMedicineUseCase { 24 | return &MedicineUseCase{ 25 | MedicineRepository: medicineRepository, 26 | } 27 | } 28 | 29 | func (s *MedicineUseCase) GetData(page int64, limit int64, sortBy string, sortDirection string, 30 | filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*domainMedicine.DataMedicine, error) { 31 | return s.MedicineRepository.GetData(page, limit, sortBy, sortDirection, filters, searchText, dateRangeFilters) 32 | } 33 | 34 | func (s *MedicineUseCase) GetByID(id int) (*domainMedicine.Medicine, error) { 35 | return s.MedicineRepository.GetByID(id) 36 | } 37 | 38 | func (s *MedicineUseCase) Create(medicine *domainMedicine.Medicine) (*domainMedicine.Medicine, error) { 39 | return s.MedicineRepository.Create(medicine) 40 | } 41 | 42 | func (s *MedicineUseCase) GetByMap(medicineMap map[string]any) (*domainMedicine.Medicine, error) { 43 | return s.MedicineRepository.GetByMap(medicineMap) 44 | } 45 | 46 | func (s *MedicineUseCase) Delete(id int) error { 47 | return s.MedicineRepository.Delete(id) 48 | } 49 | 50 | func (s *MedicineUseCase) Update(id int, medicineMap map[string]any) (*domainMedicine.Medicine, error) { 51 | return s.MedicineRepository.Update(id, medicineMap) 52 | } 53 | 54 | func (s *MedicineUseCase) GetAll() (*[]domainMedicine.Medicine, error) { 55 | return s.MedicineRepository.GetAll() 56 | } 57 | -------------------------------------------------------------------------------- /src/application/usecases/medicine/medicine_test.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/gbrayhan/microservices-go/src/domain" 9 | domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 10 | ) 11 | 12 | type mockMedicineService struct { 13 | getDataFn func(page int64, limit int64, sortBy string, sortDirection string, 14 | filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*domainMedicine.DataMedicine, error) 15 | getByIDFn func(id int) (*domainMedicine.Medicine, error) 16 | createFn func(m *domainMedicine.Medicine) (*domainMedicine.Medicine, error) 17 | getByMapFn func(m map[string]any) (*domainMedicine.Medicine, error) 18 | deleteFn func(id int) error 19 | updateFn func(id int, m map[string]any) (*domainMedicine.Medicine, error) 20 | getAllFn func() (*[]domainMedicine.Medicine, error) 21 | } 22 | 23 | func (m *mockMedicineService) GetData(page int64, limit int64, sortBy string, sortDirection string, 24 | filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*domainMedicine.DataMedicine, error) { 25 | return m.getDataFn(page, limit, sortBy, sortDirection, filters, searchText, dateRangeFilters) 26 | } 27 | 28 | func (m *mockMedicineService) GetByID(id int) (*domainMedicine.Medicine, error) { 29 | return m.getByIDFn(id) 30 | } 31 | 32 | func (m *mockMedicineService) Create(med *domainMedicine.Medicine) (*domainMedicine.Medicine, error) { 33 | return m.createFn(med) 34 | } 35 | 36 | func (m *mockMedicineService) GetByMap(med map[string]any) (*domainMedicine.Medicine, error) { 37 | return m.getByMapFn(med) 38 | } 39 | 40 | func (m *mockMedicineService) Delete(id int) error { 41 | return m.deleteFn(id) 42 | } 43 | 44 | func (m *mockMedicineService) Update(id int, med map[string]any) (*domainMedicine.Medicine, error) { 45 | return m.updateFn(id, med) 46 | } 47 | 48 | func (m *mockMedicineService) GetAll() (*[]domainMedicine.Medicine, error) { 49 | return m.getAllFn() 50 | } 51 | 52 | func TestMedicineUseCase(t *testing.T) { 53 | mockRepo := &mockMedicineService{} 54 | useCase := NewMedicineUseCase(mockRepo) 55 | 56 | mockRepo.getDataFn = func(page int64, limit int64, sortBy string, sortDirection string, 57 | filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*domainMedicine.DataMedicine, error) { 58 | if page == 0 { 59 | return nil, errors.New("error in repository") 60 | } 61 | return &domainMedicine.DataMedicine{ 62 | Data: &[]domainMedicine.Medicine{{ID: 1, Name: "Med1"}}, 63 | Total: 1, 64 | }, nil 65 | } 66 | _, err := useCase.GetData(0, 10, "", "", nil, "", nil) 67 | if err == nil { 68 | t.Error("expected error, got nil") 69 | } 70 | medData, err := useCase.GetData(1, 10, "", "", nil, "", nil) 71 | if err != nil { 72 | t.Errorf("unexpected error: %v", err) 73 | } 74 | if medData == nil || len(*medData.Data) != 1 { 75 | t.Error("expected one medicine in data") 76 | } 77 | 78 | mockRepo.getByIDFn = func(id int) (*domainMedicine.Medicine, error) { 79 | if id == 123 { 80 | return &domainMedicine.Medicine{ID: 123}, nil 81 | } 82 | return nil, errors.New("not found") 83 | } 84 | _, err = useCase.GetByID(999) 85 | if err == nil { 86 | t.Error("expected error for not found, got nil") 87 | } 88 | med, err := useCase.GetByID(123) 89 | if err != nil { 90 | t.Errorf("unexpected error: %v", err) 91 | } 92 | if med.ID != 123 { 93 | t.Error("expected medicine ID=123") 94 | } 95 | 96 | mockRepo.createFn = func(m *domainMedicine.Medicine) (*domainMedicine.Medicine, error) { 97 | if m.Name == "" { 98 | return nil, errors.New("validation error") 99 | } 100 | m.ID = 999 101 | return m, nil 102 | } 103 | _, err = useCase.Create(&domainMedicine.Medicine{Name: ""}) 104 | if err == nil { 105 | t.Error("expected create error on empty name") 106 | } 107 | newMed, err := useCase.Create(&domainMedicine.Medicine{Name: "Aspirin"}) 108 | if err != nil { 109 | t.Errorf("unexpected error: %v", err) 110 | } 111 | if newMed.ID != 999 { 112 | t.Error("expected created medicine ID=999") 113 | } 114 | 115 | mockRepo.getByMapFn = func(mm map[string]any) (*domainMedicine.Medicine, error) { 116 | if mm["ean"] == "not_found" { 117 | return nil, errors.New("not found") 118 | } 119 | return &domainMedicine.Medicine{ID: 55}, nil 120 | } 121 | _, err = useCase.GetByMap(map[string]any{"ean": "not_found"}) 122 | if err == nil { 123 | t.Error("expected error, got nil") 124 | } 125 | mg, err := useCase.GetByMap(map[string]any{"ean": "valid"}) 126 | if err != nil { 127 | t.Errorf("unexpected error: %v", err) 128 | } 129 | if mg.ID != 55 { 130 | t.Error("expected ID=55") 131 | } 132 | 133 | mockRepo.deleteFn = func(id int) error { 134 | if id == 1010 { 135 | return nil 136 | } 137 | return errors.New("cannot delete") 138 | } 139 | err = useCase.Delete(100) 140 | if err == nil { 141 | t.Error("expected error, got nil") 142 | } 143 | err = useCase.Delete(1010) 144 | if err != nil { 145 | t.Errorf("unexpected error: %v", err) 146 | } 147 | 148 | mockRepo.updateFn = func(id int, mm map[string]any) (*domainMedicine.Medicine, error) { 149 | if id != 1000 { 150 | return nil, errors.New("not found for update") 151 | } 152 | return &domainMedicine.Medicine{ID: 1000, Name: "UpdatedName"}, nil 153 | } 154 | _, err = useCase.Update(999, map[string]any{"name": "whatever"}) 155 | if err == nil { 156 | t.Error("expected error, got nil") 157 | } 158 | updated, err := useCase.Update(1000, map[string]any{"name": "NewName"}) 159 | if err != nil { 160 | t.Errorf("unexpected error: %v", err) 161 | } 162 | if updated.Name != "UpdatedName" { 163 | t.Error("expected updated name to be UpdatedName") 164 | } 165 | 166 | mockRepo.getAllFn = func() (*[]domainMedicine.Medicine, error) { 167 | return &[]domainMedicine.Medicine{ 168 | {ID: 1, Name: "M1"}, {ID: 2, Name: "M2"}, 169 | }, nil 170 | } 171 | meds, err := useCase.GetAll() 172 | if err != nil { 173 | t.Errorf("unexpected error: %v", err) 174 | } 175 | if meds == nil || len(*meds) != 2 { 176 | t.Error("expected 2 medicines from GetAll()") 177 | } 178 | } 179 | 180 | func TestNewMedicineUseCase(t *testing.T) { 181 | mockRepo := &mockMedicineService{} 182 | uc := NewMedicineUseCase(mockRepo) 183 | if reflect.TypeOf(uc).String() != "*medicine.MedicineUseCase" { 184 | t.Error("expected *medicine.MedicineUseCase type") 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/application/usecases/medicine/structures.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 4 | 5 | type NewMedicine struct { 6 | Name string `json:"name" example:"Paracetamol"` 7 | Description string `json:"description" example:"Some Description"` 8 | EANCode string `json:"ean_code" example:"9900000124"` 9 | Laboratory string `json:"laboratory" example:"Roche"` 10 | } 11 | 12 | type PaginationResultMedicine struct { 13 | Data *[]domainMedicine.Medicine 14 | Total int64 15 | Limit int64 16 | Current int64 17 | NextCursor uint 18 | PrevCursor uint 19 | NumPages int64 20 | } 21 | -------------------------------------------------------------------------------- /src/application/usecases/user/structures.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import domainUser "github.com/gbrayhan/microservices-go/src/domain/user" 4 | 5 | type PaginationResultUser struct { 6 | Data []domainUser.User 7 | Total int64 8 | Limit int64 9 | Current int64 10 | NextCursor uint 11 | PrevCursor uint 12 | NumPages int64 13 | } 14 | -------------------------------------------------------------------------------- /src/application/usecases/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | userDomain "github.com/gbrayhan/microservices-go/src/domain/user" 5 | "golang.org/x/crypto/bcrypt" 6 | ) 7 | 8 | type IUserUseCase interface { 9 | GetAll() (*[]userDomain.User, error) 10 | GetByID(id int) (*userDomain.User, error) 11 | Create(newUser *userDomain.User) (*userDomain.User, error) 12 | GetOneByMap(userMap map[string]interface{}) (*userDomain.User, error) 13 | Delete(id int) error 14 | Update(id int, userMap map[string]interface{}) (*userDomain.User, error) 15 | } 16 | 17 | type UserUseCase struct { 18 | userRepository userDomain.IUserService 19 | } 20 | 21 | func NewUserUseCase(userRepository userDomain.IUserService) IUserUseCase { 22 | return &UserUseCase{ 23 | userRepository: userRepository, 24 | } 25 | } 26 | 27 | func (s *UserUseCase) GetAll() (*[]userDomain.User, error) { 28 | return s.userRepository.GetAll() 29 | } 30 | 31 | func (s *UserUseCase) GetByID(id int) (*userDomain.User, error) { 32 | return s.userRepository.GetByID(id) 33 | } 34 | 35 | func (s *UserUseCase) Create(newUser *userDomain.User) (*userDomain.User, error) { 36 | 37 | hash, err := bcrypt.GenerateFromPassword([]byte(newUser.Password), bcrypt.DefaultCost) 38 | if err != nil { 39 | return &userDomain.User{}, err 40 | } 41 | newUser.HashPassword = string(hash) 42 | newUser.Status = true 43 | 44 | return s.userRepository.Create(newUser) 45 | } 46 | 47 | func (s *UserUseCase) GetOneByMap(userMap map[string]interface{}) (*userDomain.User, error) { 48 | return s.userRepository.GetOneByMap(userMap) 49 | } 50 | 51 | func (s *UserUseCase) Delete(id int) error { 52 | return s.userRepository.Delete(id) 53 | } 54 | 55 | func (s *UserUseCase) Update(id int, userMap map[string]interface{}) (*userDomain.User, error) { 56 | return s.userRepository.Update(id, userMap) 57 | } 58 | -------------------------------------------------------------------------------- /src/application/usecases/user/user_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | userDomain "github.com/gbrayhan/microservices-go/src/domain/user" 9 | ) 10 | 11 | type mockUserService struct { 12 | getAllFn func() (*[]userDomain.User, error) 13 | getByIDFn func(id int) (*userDomain.User, error) 14 | createFn func(u *userDomain.User) (*userDomain.User, error) 15 | getOneByMapFn func(m map[string]interface{}) (*userDomain.User, error) 16 | deleteFn func(id int) error 17 | updateFn func(id int, m map[string]interface{}) (*userDomain.User, error) 18 | } 19 | 20 | func (m *mockUserService) GetAll() (*[]userDomain.User, error) { 21 | return m.getAllFn() 22 | } 23 | func (m *mockUserService) GetByID(id int) (*userDomain.User, error) { 24 | return m.getByIDFn(id) 25 | } 26 | func (m *mockUserService) Create(newUser *userDomain.User) (*userDomain.User, error) { 27 | return m.createFn(newUser) 28 | } 29 | func (m *mockUserService) GetOneByMap(userMap map[string]interface{}) (*userDomain.User, error) { 30 | return m.getOneByMapFn(userMap) 31 | } 32 | func (m *mockUserService) Delete(id int) error { 33 | return m.deleteFn(id) 34 | } 35 | func (m *mockUserService) Update(id int, userMap map[string]interface{}) (*userDomain.User, error) { 36 | return m.updateFn(id, userMap) 37 | } 38 | 39 | func TestUserUseCase(t *testing.T) { 40 | 41 | mockRepo := &mockUserService{} 42 | useCase := NewUserUseCase(mockRepo) 43 | 44 | t.Run("Test GetAll", func(t *testing.T) { 45 | mockRepo.getAllFn = func() (*[]userDomain.User, error) { 46 | return &[]userDomain.User{{ID: 1}}, nil 47 | } 48 | us, err := useCase.GetAll() 49 | if err != nil { 50 | t.Errorf("unexpected error: %v", err) 51 | } 52 | if len(*us) != 1 { 53 | t.Error("expected 1 user from GetAll") 54 | } 55 | }) 56 | 57 | t.Run("Test GetByID", func(t *testing.T) { 58 | mockRepo.getByIDFn = func(id int) (*userDomain.User, error) { 59 | if id == 999 { 60 | return nil, errors.New("not found") 61 | } 62 | return &userDomain.User{ID: id}, nil 63 | } 64 | _, err := useCase.GetByID(999) 65 | if err == nil { 66 | t.Error("expected error, got nil") 67 | } 68 | u, err := useCase.GetByID(10) 69 | if err != nil { 70 | t.Errorf("unexpected error: %v", err) 71 | } 72 | if u.ID != 10 { 73 | t.Errorf("expected user ID=10, got %d", u.ID) 74 | } 75 | }) 76 | 77 | t.Run("Test Create (OK)", func(t *testing.T) { 78 | mockRepo.createFn = func(newU *userDomain.User) (*userDomain.User, error) { 79 | if !newU.Status { 80 | t.Error("expected user.Status to be true") 81 | } 82 | if newU.HashPassword == "" { 83 | t.Error("expected user.HashPassword to be set") 84 | } 85 | if newU.Email == "" { 86 | return nil, errors.New("bad data") 87 | } 88 | newU.ID = 555 89 | return newU, nil 90 | } 91 | created, err := useCase.Create(&userDomain.User{Email: "test@mail.com", Password: "abc"}) 92 | if err != nil { 93 | t.Errorf("unexpected error: %v", err) 94 | } 95 | if created.ID != 555 { 96 | t.Error("expected ID=555 after create") 97 | } 98 | }) 99 | 100 | t.Run("Test Create (Error por email vacío)", func(t *testing.T) { 101 | _, err := useCase.Create(&userDomain.User{Email: "", Password: "abc"}) 102 | if err == nil { 103 | t.Error("expected error on create user with empty email") 104 | } 105 | }) 106 | 107 | t.Run("Test GetOneByMap", func(t *testing.T) { 108 | mockRepo.getOneByMapFn = func(m map[string]interface{}) (*userDomain.User, error) { 109 | if m["email"] == "no_exist" { 110 | return nil, errors.New("not found") 111 | } 112 | return &userDomain.User{ID: 777}, nil 113 | } 114 | _, err := useCase.GetOneByMap(map[string]interface{}{"email": "no_exist"}) 115 | if err == nil { 116 | t.Error("expected error for not found") 117 | } 118 | single, err := useCase.GetOneByMap(map[string]interface{}{"email": "yes_exist"}) 119 | if err != nil { 120 | t.Errorf("unexpected error: %v", err) 121 | } 122 | if single.ID != 777 { 123 | t.Error("expected ID=777") 124 | } 125 | }) 126 | 127 | t.Run("Test Delete", func(t *testing.T) { 128 | mockRepo.deleteFn = func(id int) error { 129 | if id == 101 { 130 | return nil 131 | } 132 | return errors.New("cannot delete") 133 | } 134 | err := useCase.Delete(999) 135 | if err == nil { 136 | t.Error("expected error for cannot delete") 137 | } 138 | err = useCase.Delete(101) 139 | if err != nil { 140 | t.Errorf("unexpected error: %v", err) 141 | } 142 | }) 143 | 144 | t.Run("Test Update", func(t *testing.T) { 145 | mockRepo.updateFn = func(id int, m map[string]interface{}) (*userDomain.User, error) { 146 | if id != 1001 { 147 | return nil, errors.New("not found") 148 | } 149 | return &userDomain.User{ID: id, UserName: "Updated"}, nil 150 | } 151 | _, err := useCase.Update(999, map[string]interface{}{"userName": "any"}) 152 | if err == nil { 153 | t.Error("expected error, got nil") 154 | } 155 | updated, err := useCase.Update(1001, map[string]interface{}{"userName": "whatever"}) 156 | if err != nil { 157 | t.Errorf("unexpected error: %v", err) 158 | } 159 | if updated.UserName != "Updated" { 160 | t.Error("expected userName=Updated") 161 | } 162 | }) 163 | } 164 | 165 | func TestNewUserUseCase(t *testing.T) { 166 | mockRepo := &mockUserService{} 167 | uc := NewUserUseCase(mockRepo) 168 | if reflect.TypeOf(uc).String() != "*user.UserUseCase" { 169 | t.Error("expected *user.UserUseCase type") 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/domain/Types.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type DateRangeFilter struct { 4 | Field string 5 | Start string 6 | End string 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/errors/Errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | const ( 6 | NotFound = "NotFound" 7 | notFoundMessage = "record not found" 8 | 9 | ValidationError = "ValidationError" 10 | validationErrorMessage = "validation error" 11 | 12 | ResourceAlreadyExists = "ResourceAlreadyExists" 13 | alreadyExistsErrorMessage = "resource already exists" 14 | 15 | RepositoryError = "RepositoryError" 16 | repositoryErrorMessage = "error in repository operation" 17 | 18 | NotAuthenticated = "NotAuthenticated" 19 | notAuthenticatedErrorMessage = "not Authenticated" 20 | 21 | TokenGeneratorError = "TokenGeneratorError" 22 | tokenGeneratorErrorMessage = "error in token generation" 23 | 24 | NotAuthorized = "NotAuthorized" 25 | notAuthorizedErrorMessage = "not authorized" 26 | 27 | UnknownError = "UnknownError" 28 | unknownErrorMessage = "something went wrong" 29 | ) 30 | 31 | type AppError struct { 32 | Err error 33 | Type string 34 | } 35 | 36 | func NewAppError(err error, errType string) *AppError { 37 | return &AppError{ 38 | Err: err, 39 | Type: errType, 40 | } 41 | } 42 | 43 | func NewAppErrorWithType(errType string) *AppError { 44 | var err error 45 | 46 | switch errType { 47 | case NotFound: 48 | err = errors.New(notFoundMessage) 49 | case ValidationError: 50 | err = errors.New(validationErrorMessage) 51 | case ResourceAlreadyExists: 52 | err = errors.New(alreadyExistsErrorMessage) 53 | case RepositoryError: 54 | err = errors.New(repositoryErrorMessage) 55 | case NotAuthenticated: 56 | err = errors.New(notAuthenticatedErrorMessage) 57 | case NotAuthorized: 58 | err = errors.New(notAuthorizedErrorMessage) 59 | case TokenGeneratorError: 60 | err = errors.New(tokenGeneratorErrorMessage) 61 | default: 62 | err = errors.New(unknownErrorMessage) 63 | } 64 | 65 | return &AppError{ 66 | Err: err, 67 | Type: errType, 68 | } 69 | } 70 | 71 | func (appErr *AppError) Error() string { 72 | return appErr.Err.Error() 73 | } 74 | -------------------------------------------------------------------------------- /src/domain/errors/Gorm.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | type GormErr struct { 4 | Number int `json:"Number"` 5 | Message string `json:"Message"` 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/medicine/Medicine.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gbrayhan/microservices-go/src/domain" 7 | ) 8 | 9 | type Medicine struct { 10 | ID int 11 | Name string 12 | Description string 13 | EanCode string 14 | Laboratory string 15 | CreatedAt time.Time 16 | UpdatedAt time.Time 17 | } 18 | 19 | type DataMedicine struct { 20 | Data *[]Medicine 21 | Total int64 22 | } 23 | 24 | type IMedicineService interface { 25 | GetAll() (*[]Medicine, error) 26 | GetData(page int64, limit int64, sortBy string, sortDirection string, filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*DataMedicine, error) 27 | GetByID(id int) (*Medicine, error) 28 | Create(medicine *Medicine) (*Medicine, error) 29 | GetByMap(medicineMap map[string]any) (*Medicine, error) 30 | Delete(id int) error 31 | Update(id int, medicineMap map[string]any) (*Medicine, error) 32 | } 33 | -------------------------------------------------------------------------------- /src/domain/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "time" 4 | 5 | type User struct { 6 | ID int 7 | UserName string 8 | Email string 9 | FirstName string 10 | LastName string 11 | Status bool 12 | HashPassword string 13 | Password string 14 | CreatedAt time.Time 15 | UpdatedAt time.Time 16 | } 17 | 18 | type IUserService interface { 19 | GetAll() (*[]User, error) 20 | GetByID(id int) (*User, error) 21 | Create(newUser *User) (*User, error) 22 | GetOneByMap(userMap map[string]interface{}) (*User, error) 23 | Delete(id int) error 24 | Update(id int, userMap map[string]interface{}) (*User, error) 25 | } 26 | -------------------------------------------------------------------------------- /src/infrastructure/repository/initDB.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gbrayhan/microservices-go/src/infrastructure/repository/medicine" 6 | "github.com/gbrayhan/microservices-go/src/infrastructure/repository/user" 7 | "gorm.io/driver/postgres" 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/logger" 10 | "log" 11 | "os" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | func InitDB() (*gorm.DB, error) { 17 | dbHost := getEnv("DB_HOST", "localhost") 18 | dbPort := getEnv("DB_PORT", "5432") 19 | dbUser := getEnv("DB_USER", "postgres") 20 | dbPass := getEnv("DB_PASS", "") 21 | dbName := getEnv("DB_NAME", "postgres") 22 | 23 | dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=America/Mexico_City", dbHost, dbUser, dbPass, dbName, dbPort) 24 | 25 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ 26 | Logger: logger.Default.LogMode(logger.Info), 27 | }) 28 | if err != nil { 29 | log.Printf("Error connecting to database: %v", err) 30 | return nil, err 31 | } 32 | 33 | sqlDB, err := db.DB() 34 | if err != nil { 35 | log.Printf("Error retrieving sql.DB from gorm.DB: %v", err) 36 | return nil, err 37 | } 38 | 39 | maxIdleConns := getEnvAsInt("DB_MAX_IDLE_CONNS", 10) 40 | maxOpenConns := getEnvAsInt("DB_MAX_OPEN_CONNS", 50) 41 | connMaxLifetime := getEnvAsInt("DB_CONN_MAX_LIFETIME", 300) 42 | 43 | sqlDB.SetMaxIdleConns(maxIdleConns) 44 | sqlDB.SetMaxOpenConns(maxOpenConns) 45 | sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Second) 46 | 47 | if err = sqlDB.Ping(); err != nil { 48 | log.Printf("Error pinging database: %v", err) 49 | return nil, err 50 | } 51 | 52 | if err = db.AutoMigrate(&user.User{}, &medicine.Medicine{}); err != nil { 53 | log.Printf("Error auto-migrating database schema: %v", err) 54 | return nil, err 55 | } 56 | 57 | return db, nil 58 | } 59 | 60 | func getEnv(key string, defaultVal string) string { 61 | if val, ok := os.LookupEnv(key); ok { 62 | return val 63 | } 64 | return defaultVal 65 | } 66 | 67 | func getEnvAsInt(key string, defaultVal int) int { 68 | if valStr, ok := os.LookupEnv(key); ok { 69 | if val, err := strconv.Atoi(valStr); err == nil { 70 | return val 71 | } 72 | } 73 | return defaultVal 74 | } 75 | -------------------------------------------------------------------------------- /src/infrastructure/repository/medicine/mappers.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 5 | ) 6 | 7 | func (m *Medicine) toDomainMapper() *domainMedicine.Medicine { 8 | return &domainMedicine.Medicine{ 9 | ID: m.ID, 10 | Name: m.Name, 11 | Description: m.Description, 12 | EanCode: m.EANCode, 13 | Laboratory: m.Laboratory, 14 | CreatedAt: m.CreatedAt, 15 | UpdatedAt: m.UpdatedAt, 16 | } 17 | } 18 | 19 | func fromDomainMapper(m *domainMedicine.Medicine) *Medicine { 20 | return &Medicine{ 21 | ID: m.ID, 22 | Name: m.Name, 23 | Description: m.Description, 24 | EANCode: m.EanCode, 25 | Laboratory: m.Laboratory, 26 | CreatedAt: m.CreatedAt, 27 | UpdatedAt: m.UpdatedAt, 28 | } 29 | } 30 | 31 | func arrayToDomainMapper(medicines *[]Medicine) *[]domainMedicine.Medicine { 32 | medicinesDomain := make([]domainMedicine.Medicine, len(*medicines)) 33 | for i, medicine := range *medicines { 34 | medicinesDomain[i] = *medicine.toDomainMapper() 35 | } 36 | return &medicinesDomain 37 | } 38 | -------------------------------------------------------------------------------- /src/infrastructure/repository/medicine/medicine.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gbrayhan/microservices-go/src/domain" 7 | 8 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 9 | domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 10 | "github.com/gbrayhan/microservices-go/src/infrastructure/repository/utils" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type IMedicineRepository interface { 15 | GetData(page int64, limit int64, sortBy string, sortDirection string, filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*domainMedicine.DataMedicine, error) 16 | Create(newMedicine *domainMedicine.Medicine) (*domainMedicine.Medicine, error) 17 | GetByID(id int) (*domainMedicine.Medicine, error) 18 | GetByMap(medicineMap map[string]any) (*domainMedicine.Medicine, error) 19 | Update(id int, medicineMap map[string]any) (*domainMedicine.Medicine, error) 20 | Delete(id int) error 21 | GetAll() (*[]domainMedicine.Medicine, error) 22 | } 23 | 24 | func (I Repository) GetByMap(medicineMap map[string]any) (*domainMedicine.Medicine, error) { 25 | panic("implement me") 26 | } 27 | 28 | func NewMedicineRepository(DB *gorm.DB) IMedicineRepository { 29 | return &Repository{ 30 | DB: DB} 31 | } 32 | 33 | func (*Medicine) TableName() string { 34 | return "medicines" 35 | } 36 | 37 | type Repository struct { 38 | DB *gorm.DB 39 | } 40 | 41 | var ColumnsMedicineMapping = map[string]string{ 42 | "id": "id", 43 | "name": "name", 44 | "description": "description", 45 | "eanCode": "ean_code", 46 | "laboratory": "laboratory", 47 | "createdAt": "created_at", 48 | "updatedAt": "updated_at", 49 | } 50 | 51 | func (r *Repository) GetData(page int64, limit int64, sortBy string, sortDirection string, filters map[string][]string, searchText string, dateRangeFilters []domain.DateRangeFilter) (*domainMedicine.DataMedicine, error) { 52 | var medicines []Medicine 53 | var total int64 54 | offset := (page - 1) * limit 55 | 56 | var searchColumns = []string{"name", "description", "ean_code", "laboratory"} 57 | 58 | countResult := make(chan error) 59 | go func() { 60 | err := r.DB.Model(&Medicine{}). 61 | Scopes(utils.ApplyFilters(ColumnsMedicineMapping, filters, dateRangeFilters, searchText, searchColumns)). 62 | Count(&total).Error 63 | countResult <- err 64 | }() 65 | 66 | queryResult := make(chan error) 67 | go func() { 68 | query, err := utils.ComplementSearch((*gorm.DB)(nil), sortBy, sortDirection, limit, offset, filters, dateRangeFilters, searchText, searchColumns, ColumnsMedicineMapping) 69 | if err != nil { 70 | queryResult <- err 71 | return 72 | } 73 | if query == nil { 74 | query = r.DB 75 | } else { 76 | query = r.DB.Scopes(utils.ApplyFilters(ColumnsMedicineMapping, filters, dateRangeFilters, searchText, searchColumns)) 77 | } 78 | 79 | err = query.Find(&medicines).Error 80 | queryResult <- err 81 | }() 82 | 83 | var countErr, queryErr error 84 | for i := 0; i < 2; i++ { 85 | select { 86 | case err := <-countResult: 87 | countErr = err 88 | case err := <-queryResult: 89 | queryErr = err 90 | } 91 | } 92 | 93 | if countErr != nil { 94 | return &domainMedicine.DataMedicine{}, countErr 95 | } 96 | if queryErr != nil { 97 | return &domainMedicine.DataMedicine{}, queryErr 98 | } 99 | 100 | return &domainMedicine.DataMedicine{ 101 | Data: arrayToDomainMapper(&medicines), 102 | Total: total, 103 | }, nil 104 | } 105 | 106 | func (r *Repository) Create(newMedicine *domainMedicine.Medicine) (*domainMedicine.Medicine, error) { 107 | medicine := &Medicine{ 108 | Name: newMedicine.Name, 109 | Description: newMedicine.Description, 110 | EANCode: newMedicine.EanCode, 111 | Laboratory: newMedicine.Laboratory, 112 | } 113 | 114 | tx := r.DB.Create(medicine) 115 | if tx.Error != nil { 116 | byteErr, _ := json.Marshal(tx.Error) 117 | var newError domainErrors.GormErr 118 | err := json.Unmarshal(byteErr, &newError) 119 | if err != nil { 120 | return nil, err 121 | } 122 | switch newError.Number { 123 | case 1062: 124 | return nil, domainErrors.NewAppErrorWithType(domainErrors.ResourceAlreadyExists) 125 | default: 126 | return nil, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 127 | } 128 | } 129 | return medicine.toDomainMapper(), nil 130 | } 131 | 132 | func (r *Repository) GetByID(id int) (*domainMedicine.Medicine, error) { 133 | var medicine Medicine 134 | err := r.DB.Where("id = ?", id).First(&medicine).Error 135 | if err != nil { 136 | if err == gorm.ErrRecordNotFound { 137 | return nil, domainErrors.NewAppErrorWithType(domainErrors.NotFound) 138 | } 139 | return nil, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 140 | } 141 | return medicine.toDomainMapper(), nil 142 | } 143 | 144 | func (r *Repository) GetOneByMap(medicineMap map[string]any) (*domainMedicine.Medicine, error) { 145 | var medicine Medicine 146 | tx := r.DB.Limit(1) 147 | for key, value := range medicineMap { 148 | if !utils.IsZeroValue(value) { 149 | tx = tx.Where(fmt.Sprintf("%s = ?", key), value) 150 | } 151 | } 152 | err := tx.Find(&medicine).Error 153 | if err != nil { 154 | return nil, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 155 | } 156 | return medicine.toDomainMapper(), nil 157 | } 158 | 159 | func (r *Repository) Update(id int, medicineMap map[string]any) (*domainMedicine.Medicine, error) { 160 | var med Medicine 161 | med.ID = id 162 | err := r.DB.Model(&med). 163 | Select("name", "description", "ean_code", "laboratory"). 164 | Updates(medicineMap).Error 165 | if err != nil { 166 | byteErr, _ := json.Marshal(err) 167 | var newError domainErrors.GormErr 168 | errUnmarshal := json.Unmarshal(byteErr, &newError) 169 | if errUnmarshal != nil { 170 | return nil, errUnmarshal 171 | } 172 | switch newError.Number { 173 | case 1062: 174 | return nil, domainErrors.NewAppErrorWithType(domainErrors.ResourceAlreadyExists) 175 | default: 176 | return nil, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 177 | } 178 | } 179 | err = r.DB.Where("id = ?", id).First(&med).Error 180 | return med.toDomainMapper(), err 181 | } 182 | 183 | func (r *Repository) Delete(id int) error { 184 | tx := r.DB.Delete(&Medicine{}, id) 185 | if tx.Error != nil { 186 | return domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 187 | } 188 | if tx.RowsAffected == 0 { 189 | return domainErrors.NewAppErrorWithType(domainErrors.NotFound) 190 | } 191 | return nil 192 | } 193 | 194 | func (r *Repository) GetAll() (*[]domainMedicine.Medicine, error) { 195 | var medicines []Medicine 196 | err := r.DB.Find(&medicines).Error 197 | if err != nil { 198 | return nil, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 199 | } 200 | return arrayToDomainMapper(&medicines), nil 201 | } 202 | -------------------------------------------------------------------------------- /src/infrastructure/repository/medicine/structures.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | "time" 5 | 6 | domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 7 | ) 8 | 9 | type Medicine struct { 10 | ID int `gorm:"primaryKey"` 11 | Name string `gorm:"unique"` 12 | Description string 13 | EANCode string `gorm:"unique"` 14 | Laboratory string 15 | CreatedAt time.Time `gorm:"autoCreateTime:milli"` 16 | UpdatedAt time.Time `gorm:"autoUpdateTime:milli"` 17 | } 18 | 19 | type PaginationResultMedicine struct { 20 | Data *[]domainMedicine.Medicine 21 | Total int64 22 | Limit int64 23 | Current int64 24 | NextCursor uint 25 | PrevCursor uint 26 | NumPages int64 27 | } 28 | -------------------------------------------------------------------------------- /src/infrastructure/repository/user/mappers.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | domainUser "github.com/gbrayhan/microservices-go/src/domain/user" 5 | ) 6 | 7 | func (u *User) toDomainMapper() *domainUser.User { 8 | return &domainUser.User{ 9 | ID: u.ID, 10 | UserName: u.UserName, 11 | Email: u.Email, 12 | FirstName: u.FirstName, 13 | LastName: u.LastName, 14 | Status: u.Status, 15 | HashPassword: u.HashPassword, 16 | CreatedAt: u.CreatedAt, 17 | UpdatedAt: u.UpdatedAt, 18 | } 19 | } 20 | 21 | func fromDomainMapper(u *domainUser.User) *User { 22 | return &User{ 23 | ID: u.ID, 24 | UserName: u.UserName, 25 | Email: u.Email, 26 | FirstName: u.FirstName, 27 | LastName: u.LastName, 28 | Status: u.Status, 29 | HashPassword: u.HashPassword, 30 | CreatedAt: u.CreatedAt, 31 | UpdatedAt: u.UpdatedAt, 32 | } 33 | } 34 | 35 | func arrayToDomainMapper(users *[]User) *[]domainUser.User { 36 | usersDomain := make([]domainUser.User, len(*users)) 37 | for i, user := range *users { 38 | usersDomain[i] = *user.toDomainMapper() 39 | } 40 | return &usersDomain 41 | } 42 | -------------------------------------------------------------------------------- /src/infrastructure/repository/user/structures.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type User struct { 8 | ID int `gorm:"primaryKey"` 9 | UserName string `gorm:"column:user_name;unique"` 10 | Email string `gorm:"unique"` 11 | FirstName string `gorm:"column:first_name"` 12 | LastName string `gorm:"column:last_name"` 13 | Status bool `gorm:"column:status"` 14 | HashPassword string `gorm:"column:hash_password"` 15 | CreatedAt time.Time `gorm:"autoCreateTime:mili"` 16 | UpdatedAt time.Time `gorm:"autoUpdateTime:mili"` 17 | } 18 | -------------------------------------------------------------------------------- /src/infrastructure/repository/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 7 | domainUser "github.com/gbrayhan/microservices-go/src/domain/user" 8 | "github.com/gbrayhan/microservices-go/src/infrastructure/repository/utils" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func (*User) TableName() string { 13 | return "users" 14 | } 15 | 16 | type IUserRepository interface { 17 | GetAll() (*[]domainUser.User, error) 18 | Create(userDomain *domainUser.User) (*domainUser.User, error) 19 | GetOneByMap(userMap map[string]interface{}) (*domainUser.User, error) 20 | GetByID(id int) (*domainUser.User, error) 21 | Update(id int, userMap map[string]interface{}) (*domainUser.User, error) 22 | Delete(id int) error 23 | } 24 | 25 | type Repository struct { 26 | DB *gorm.DB 27 | } 28 | 29 | func NewUserRepository(db *gorm.DB) IUserRepository { 30 | return &Repository{DB: db} 31 | } 32 | 33 | func (r *Repository) GetAll() (*[]domainUser.User, error) { 34 | var users []User 35 | if err := r.DB.Find(&users).Error; err != nil { 36 | return nil, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 37 | } 38 | return arrayToDomainMapper(&users), nil 39 | } 40 | 41 | func (r *Repository) Create(userDomain *domainUser.User) (*domainUser.User, error) { 42 | userRepository := fromDomainMapper(userDomain) 43 | txDb := r.DB.Create(userRepository) 44 | err := txDb.Error 45 | if err != nil { 46 | byteErr, _ := json.Marshal(err) 47 | var newError domainErrors.GormErr 48 | errUnmarshal := json.Unmarshal(byteErr, &newError) 49 | if errUnmarshal != nil { 50 | return &domainUser.User{}, errUnmarshal 51 | } 52 | switch newError.Number { 53 | case 1062: 54 | err = domainErrors.NewAppErrorWithType(domainErrors.ResourceAlreadyExists) 55 | return &domainUser.User{}, err 56 | default: 57 | err = domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 58 | } 59 | } 60 | return userRepository.toDomainMapper(), err 61 | } 62 | 63 | func (r *Repository) GetOneByMap(userMap map[string]interface{}) (*domainUser.User, error) { 64 | var userRepository User 65 | tx := r.DB.Limit(1) 66 | for key, value := range userMap { 67 | if !utils.IsZeroValue(value) { 68 | tx = tx.Where(fmt.Sprintf("%s = ?", key), value) 69 | } 70 | } 71 | if err := tx.Find(&userRepository).Error; err != nil { 72 | return &domainUser.User{}, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 73 | } 74 | return userRepository.toDomainMapper(), nil 75 | } 76 | 77 | func (r *Repository) GetByID(id int) (*domainUser.User, error) { 78 | var user User 79 | err := r.DB.Where("id = ?", id).First(&user).Error 80 | if err != nil { 81 | if err == gorm.ErrRecordNotFound { 82 | err = domainErrors.NewAppErrorWithType(domainErrors.NotFound) 83 | } else { 84 | err = domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 85 | } 86 | return &domainUser.User{}, err 87 | } 88 | return user.toDomainMapper(), nil 89 | } 90 | 91 | func (r *Repository) Update(id int, userMap map[string]interface{}) (*domainUser.User, error) { 92 | var userObj User 93 | userObj.ID = id 94 | err := r.DB.Model(&userObj). 95 | Select("user_name", "email", "first_name", "last_name", "status", "role"). 96 | Updates(userMap).Error 97 | if err != nil { 98 | byteErr, _ := json.Marshal(err) 99 | var newError domainErrors.GormErr 100 | errUnmarshal := json.Unmarshal(byteErr, &newError) 101 | if errUnmarshal != nil { 102 | return &domainUser.User{}, errUnmarshal 103 | } 104 | switch newError.Number { 105 | case 1062: 106 | return &domainUser.User{}, domainErrors.NewAppErrorWithType(domainErrors.ResourceAlreadyExists) 107 | default: 108 | return &domainUser.User{}, domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 109 | } 110 | } 111 | if err := r.DB.Where("id = ?", id).First(&userObj).Error; err != nil { 112 | return &domainUser.User{}, err 113 | } 114 | return userObj.toDomainMapper(), nil 115 | } 116 | 117 | func (r *Repository) Delete(id int) error { 118 | tx := r.DB.Delete(&User{}, id) 119 | if tx.Error != nil { 120 | return domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 121 | } 122 | if tx.RowsAffected == 0 { 123 | return domainErrors.NewAppErrorWithType(domainErrors.NotFound) 124 | } 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /src/infrastructure/repository/utils/Utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/gbrayhan/microservices-go/src/domain" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func ComplementSearch(r *gorm.DB, sortBy string, sortDirection string, limit int64, offset int64, filters map[string][]string, dateRangeFilters []domain.DateRangeFilter, searchText string, searchColumns []string, columnMapping map[string]string) (query *gorm.DB, err error) { 13 | if r == nil { 14 | return nil, nil 15 | } 16 | 17 | query = r 18 | if sortBy != "" { 19 | orderClause := fmt.Sprintf("%s %s", columnMapping[sortBy], sortDirection) 20 | query = query.Order(orderClause).Limit(int(limit)).Offset(int(offset)) 21 | } else { 22 | query = query.Limit(int(limit)).Offset(int(offset)) 23 | } 24 | 25 | if len(filters) > 0 { 26 | filters = UpdateFilterKeys(filters, columnMapping) 27 | for key, values := range filters { 28 | query = query.Where(fmt.Sprintf("%s IN (?)", key), values) 29 | } 30 | } 31 | 32 | if len(dateRangeFilters) > 0 { 33 | for i := range dateRangeFilters { 34 | if newFieldName, ok := columnMapping[dateRangeFilters[i].Field]; ok { 35 | dateRangeFilters[i].Field = newFieldName 36 | } 37 | } 38 | for _, filter := range dateRangeFilters { 39 | query = query.Where(fmt.Sprintf("%s BETWEEN ? AND ?", filter.Field), filter.Start, filter.End) 40 | } 41 | } 42 | 43 | if searchText != "" { 44 | var orConditions []string 45 | for _, column := range searchColumns { 46 | orConditions = append(orConditions, fmt.Sprintf("%s LIKE '%%%s%%'", column, searchText)) 47 | } 48 | searchQuery := fmt.Sprintf("AND (%s)", strings.Join(orConditions, " OR ")) 49 | query = query.Where(fmt.Sprintf("1=1 %s", searchQuery)) 50 | } 51 | return 52 | } 53 | 54 | func UpdateFilterKeys(filters map[string][]string, columnMapping map[string]string) map[string][]string { 55 | updatedFilters := make(map[string][]string) 56 | for key, value := range filters { 57 | if updatedKey, ok := columnMapping[key]; ok { 58 | updatedFilters[updatedKey] = value 59 | } else { 60 | updatedFilters[key] = value 61 | } 62 | } 63 | return updatedFilters 64 | } 65 | 66 | func ApplyFilters(columnMapping map[string]string, filters map[string][]string, dateRangeFilters []domain.DateRangeFilter, searchText string, searchColumns []string) func(db *gorm.DB) *gorm.DB { 67 | return func(db *gorm.DB) *gorm.DB { 68 | query := db 69 | if len(filters) > 0 { 70 | filters = UpdateFilterKeys(filters, columnMapping) 71 | for key, values := range filters { 72 | query = query.Where(fmt.Sprintf("%s IN (?)", key), values) 73 | } 74 | } 75 | if len(dateRangeFilters) > 0 { 76 | for _, filter := range dateRangeFilters { 77 | if newFieldName, ok := columnMapping[filter.Field]; ok { 78 | filter.Field = newFieldName 79 | } 80 | query = query.Where(fmt.Sprintf("%s BETWEEN ? AND ?", filter.Field), filter.Start, filter.End) 81 | } 82 | } 83 | if searchText != "" && len(searchColumns) > 0 { 84 | var orConditions []string 85 | var args []interface{} 86 | for _, column := range searchColumns { 87 | orConditions = append(orConditions, fmt.Sprintf("%s LIKE ?", column)) 88 | args = append(args, "%"+searchText+"%") 89 | } 90 | searchQuery := fmt.Sprintf("(%s)", strings.Join(orConditions, " OR ")) 91 | query = query.Where(searchQuery, args...) 92 | } 93 | return query 94 | } 95 | } 96 | 97 | func IsZeroValue(value any) bool { 98 | return reflect.DeepEqual(value, reflect.Zero(reflect.TypeOf(value)).Interface()) 99 | } 100 | -------------------------------------------------------------------------------- /src/infrastructure/rest/adapter/auth.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | authUseCase "github.com/gbrayhan/microservices-go/src/application/usecases/auth" 5 | userRepository "github.com/gbrayhan/microservices-go/src/infrastructure/repository/user" 6 | authController "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers/auth" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func AuthAdapter(db *gorm.DB) authController.IAuthController { 11 | userRepository := userRepository.NewUserRepository(db) 12 | authUseCase := authUseCase.NewAuthUseCase(userRepository) 13 | authController := authController.NewAuthController(authUseCase) 14 | return authController 15 | } 16 | -------------------------------------------------------------------------------- /src/infrastructure/rest/adapter/medicine.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | medicineUseCase "github.com/gbrayhan/microservices-go/src/application/usecases/medicine" 5 | medicineRepository "github.com/gbrayhan/microservices-go/src/infrastructure/repository/medicine" 6 | medicineController "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers/medicine" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func MedicineAdapter(db *gorm.DB) medicineController.IMedicineController { 11 | repository := medicineRepository.NewMedicineRepository(db) 12 | service := medicineUseCase.NewMedicineUseCase(repository) 13 | controller := medicineController.NewMedicineController(service) 14 | return controller 15 | } 16 | -------------------------------------------------------------------------------- /src/infrastructure/rest/adapter/user.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | userUseCase "github.com/gbrayhan/microservices-go/src/application/usecases/user" 5 | userRepository "github.com/gbrayhan/microservices-go/src/infrastructure/repository/user" 6 | userController "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers/user" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func UserAdapter(db *gorm.DB) userController.IUserController { 11 | repository := userRepository.NewUserRepository(db) 12 | service := userUseCase.NewUserUseCase(repository) 13 | controller := userController.NewUserController(service) 14 | return controller 15 | } 16 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/BindTools.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func BindJSON(c *gin.Context, request any) error { 12 | buf := make([]byte, 5120) 13 | num, _ := c.Request.Body.Read(buf) 14 | reqBody := string(buf[0:num]) 15 | c.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(reqBody))) 16 | err := c.ShouldBindJSON(request) 17 | c.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(reqBody))) 18 | return err 19 | } 20 | 21 | func BindJSONMap(c *gin.Context, request *map[string]any) error { 22 | buf := make([]byte, 5120) 23 | num, _ := c.Request.Body.Read(buf) 24 | reqBody := buf[0:num] 25 | c.Request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) 26 | err := json.Unmarshal(reqBody, &request) 27 | c.Request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) 28 | return err 29 | } 30 | 31 | type MessageResponse struct { 32 | Message string `json:"message"` 33 | } 34 | 35 | type SortByDataRequest struct { 36 | Field string `json:"field"` 37 | Direction string `json:"direction"` 38 | } 39 | 40 | type FieldDateRangeDataRequest struct { 41 | Field string `json:"field"` 42 | StartDate string `json:"startDate"` 43 | EndDate string `json:"endDate"` 44 | } 45 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/Utils.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | func PaginationValues(limit int64, page int64, total int64) (numPages int64, nextCursor int64, prevCursor int64) { 4 | numPages = (total + limit - 1) / limit 5 | if page < numPages { 6 | nextCursor = page + 1 7 | } 8 | if page > 1 { 9 | prevCursor = page - 1 10 | } 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/auth/Auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | useCaseAuth "github.com/gbrayhan/microservices-go/src/application/usecases/auth" 5 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 6 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | type IAuthController interface { 12 | Login(ctx *gin.Context) 13 | GetAccessTokenByRefreshToken(ctx *gin.Context) 14 | } 15 | 16 | type AuthController struct { 17 | authUsecase useCaseAuth.IAuthUseCase 18 | } 19 | 20 | func NewAuthController(authUsecase useCaseAuth.IAuthUseCase) IAuthController { 21 | return &AuthController{ 22 | authUsecase: authUsecase, 23 | } 24 | } 25 | 26 | func (c *AuthController) Login(ctx *gin.Context) { 27 | var request LoginRequest 28 | if err := controllers.BindJSON(ctx, &request); err != nil { 29 | appError := domainErrors.NewAppError(err, domainErrors.ValidationError) 30 | _ = ctx.Error(appError) 31 | return 32 | } 33 | userLogin := useCaseAuth.LoginUser{ 34 | Email: request.Email, 35 | Password: request.Password, 36 | } 37 | authDataUser, err := c.authUsecase.Login(userLogin) 38 | if err != nil { 39 | _ = ctx.Error(err) 40 | return 41 | } 42 | ctx.JSON(http.StatusOK, authDataUser) 43 | } 44 | 45 | func (c *AuthController) GetAccessTokenByRefreshToken(ctx *gin.Context) { 46 | var request AccessTokenRequest 47 | if err := controllers.BindJSON(ctx, &request); err != nil { 48 | appError := domainErrors.NewAppError(err, domainErrors.ValidationError) 49 | _ = ctx.Error(appError) 50 | return 51 | } 52 | authDataUser, err := c.authUsecase.AccessTokenByRefreshToken(request.RefreshToken) 53 | if err != nil { 54 | _ = ctx.Error(err) 55 | return 56 | } 57 | ctx.JSON(http.StatusOK, authDataUser) 58 | } 59 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/auth/Structures.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | type LoginRequest struct { 4 | Email string `json:"email" binding:"required"` 5 | Password string `json:"password" binding:"required"` 6 | } 7 | 8 | type AccessTokenRequest struct { 9 | RefreshToken string `json:"refreshToken" binding:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/medicine/Mapper.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 4 | 5 | func domainToResponseMapper(m *domainMedicine.Medicine) *ResponseMedicine { 6 | return &ResponseMedicine{ 7 | ID: m.ID, 8 | Name: m.Name, 9 | Description: m.Description, 10 | EanCode: m.EanCode, 11 | Laboratory: m.Laboratory, 12 | CreatedAt: m.CreatedAt, 13 | UpdatedAt: m.UpdatedAt, 14 | } 15 | } 16 | 17 | func arrayDomainToResponseMapper(m *[]domainMedicine.Medicine) *[]ResponseMedicine { 18 | res := make([]ResponseMedicine, len(*m)) 19 | for i, med := range *m { 20 | res[i] = *domainToResponseMapper(&med) 21 | } 22 | return &res 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/medicine/Medicines.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gbrayhan/microservices-go/src/domain" 9 | domainError "github.com/gbrayhan/microservices-go/src/domain/errors" 10 | domainMedicine "github.com/gbrayhan/microservices-go/src/domain/medicine" 11 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type IMedicineController interface { 16 | NewMedicine(ctx *gin.Context) 17 | GetAllMedicines(ctx *gin.Context) 18 | GetDataMedicines(ctx *gin.Context) 19 | GetMedicinesByID(ctx *gin.Context) 20 | UpdateMedicine(ctx *gin.Context) 21 | DeleteMedicine(ctx *gin.Context) 22 | } 23 | 24 | type Controller struct { 25 | MedicineService domainMedicine.IMedicineService 26 | } 27 | 28 | func NewMedicineController(medicineService domainMedicine.IMedicineService) IMedicineController { 29 | return &Controller{MedicineService: medicineService} 30 | } 31 | 32 | func (c *Controller) NewMedicine(ctx *gin.Context) { 33 | var request NewMedicineRequest 34 | if err := controllers.BindJSON(ctx, &request); err != nil { 35 | appError := domainError.NewAppError(err, domainError.ValidationError) 36 | _ = ctx.Error(appError) 37 | return 38 | } 39 | newMed := domainMedicine.Medicine{ 40 | Name: request.Name, 41 | Description: request.Description, 42 | Laboratory: request.Laboratory, 43 | EanCode: request.EanCode, 44 | } 45 | dMed, err := c.MedicineService.Create(&newMed) 46 | if err != nil { 47 | _ = ctx.Error(err) 48 | return 49 | } 50 | resp := domainToResponseMapper(dMed) 51 | ctx.JSON(http.StatusOK, resp) 52 | } 53 | 54 | func (c *Controller) GetAllMedicines(ctx *gin.Context) { 55 | medicines, err := c.MedicineService.GetAll() 56 | if err != nil { 57 | appError := domainError.NewAppErrorWithType(domainError.UnknownError) 58 | _ = ctx.Error(appError) 59 | return 60 | } 61 | ctx.JSON(http.StatusOK, arrayDomainToResponseMapper(medicines)) 62 | } 63 | 64 | func (c *Controller) GetDataMedicines(ctx *gin.Context) { 65 | var request DataMedicineRequest 66 | if err := controllers.BindJSON(ctx, &request); err != nil { 67 | appError := domainError.NewAppError(err, domainError.ValidationError) 68 | _ = ctx.Error(appError) 69 | return 70 | } 71 | var dateRangeFiltersDomain []domain.DateRangeFilter 72 | for _, f := range request.FieldsDateRange { 73 | dateRangeFiltersDomain = append(dateRangeFiltersDomain, domain.DateRangeFilter{ 74 | Field: f.Field, Start: f.StartDate, End: f.EndDate, 75 | }) 76 | } 77 | result, err := c.MedicineService.GetData( 78 | request.Page, request.Limit, 79 | request.SorBy.Field, request.SorBy.Direction, 80 | request.Filters, request.GlobalSearch, 81 | dateRangeFiltersDomain, 82 | ) 83 | if err != nil { 84 | appError := domainError.NewAppErrorWithType(domainError.UnknownError) 85 | _ = ctx.Error(appError) 86 | return 87 | } 88 | numPages, nextCursor, prevCursor := controllers.PaginationValues(request.Limit, request.Page, result.Total) 89 | resp := PaginationResultMedicine{ 90 | Data: arrayDomainToResponseMapper(result.Data), 91 | Total: result.Total, 92 | Limit: request.Limit, 93 | Current: request.Page, 94 | NextCursor: nextCursor, 95 | PrevCursor: prevCursor, 96 | NumPages: numPages, 97 | } 98 | ctx.JSON(http.StatusOK, resp) 99 | } 100 | 101 | func (c *Controller) GetMedicinesByID(ctx *gin.Context) { 102 | medicineID, err := strconv.Atoi(ctx.Param("id")) 103 | if err != nil { 104 | appError := domainError.NewAppError(errors.New("medicine id is invalid"), domainError.ValidationError) 105 | _ = ctx.Error(appError) 106 | return 107 | } 108 | dMed, err := c.MedicineService.GetByID(medicineID) 109 | if err != nil { 110 | _ = ctx.Error(err) 111 | return 112 | } 113 | ctx.JSON(http.StatusOK, dMed) 114 | } 115 | 116 | func (c *Controller) UpdateMedicine(ctx *gin.Context) { 117 | medicineID, err := strconv.Atoi(ctx.Param("id")) 118 | if err != nil { 119 | appError := domainError.NewAppError(errors.New("param id is necessary"), domainError.ValidationError) 120 | _ = ctx.Error(appError) 121 | return 122 | } 123 | var requestMap map[string]any 124 | if err := controllers.BindJSONMap(ctx, &requestMap); err != nil { 125 | appError := domainError.NewAppError(err, domainError.ValidationError) 126 | _ = ctx.Error(appError) 127 | return 128 | } 129 | err = updateValidation(requestMap) 130 | if err != nil { 131 | _ = ctx.Error(err) 132 | return 133 | } 134 | updated, err := c.MedicineService.Update(medicineID, requestMap) 135 | if err != nil { 136 | _ = ctx.Error(err) 137 | return 138 | } 139 | ctx.JSON(http.StatusOK, updated) 140 | } 141 | 142 | func (c *Controller) DeleteMedicine(ctx *gin.Context) { 143 | medicineID, err := strconv.Atoi(ctx.Param("id")) 144 | if err != nil { 145 | appError := domainError.NewAppError(errors.New("param id is necessary"), domainError.ValidationError) 146 | _ = ctx.Error(appError) 147 | return 148 | } 149 | if err = c.MedicineService.Delete(medicineID); err != nil { 150 | _ = ctx.Error(err) 151 | return 152 | } 153 | ctx.JSON(http.StatusOK, gin.H{"message": "resource deleted successfully"}) 154 | } 155 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/medicine/Requests.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers" 4 | 5 | type NewMedicineRequest struct { 6 | Name string `json:"name" binding:"required"` 7 | Description string `json:"description" binding:"required"` 8 | Laboratory string `json:"laboratory" binding:"required"` 9 | EanCode string `json:"eanCode" binding:"required"` 10 | } 11 | 12 | type DataMedicineRequest struct { 13 | Limit int64 `json:"limit" example:"10"` 14 | Page int64 `json:"page" example:"1"` 15 | GlobalSearch string `json:"globalSearch" example:"John"` 16 | Filters map[string][]string `json:"filters"` 17 | SorBy controllers.SortByDataRequest `json:"sortBy"` 18 | FieldsDateRange []controllers.FieldDateRangeDataRequest `json:"fieldsDateRange"` 19 | } 20 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/medicine/Responses.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import "time" 4 | 5 | type ResponseMedicine struct { 6 | ID int `json:"id"` 7 | Name string `json:"name"` 8 | Description string `json:"description"` 9 | EanCode string `json:"eanCode"` 10 | Laboratory string `json:"laboratory"` 11 | CreatedAt time.Time `json:"createdAt,omitempty"` 12 | UpdatedAt time.Time `json:"updatedAt,omitempty"` 13 | } 14 | 15 | type PaginationResultMedicine struct { 16 | Data *[]ResponseMedicine `json:"data"` 17 | Total int64 `json:"total"` 18 | Limit int64 `json:"limit"` 19 | Current int64 `json:"current"` 20 | NextCursor int64 `json:"nextCursor"` 21 | PrevCursor int64 `json:"prevCursor"` 22 | NumPages int64 `json:"numPages"` 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/medicine/Validation.go: -------------------------------------------------------------------------------- 1 | package medicine 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 9 | "github.com/go-playground/validator/v10" 10 | ) 11 | 12 | func updateValidation(request map[string]any) error { 13 | var errorsValidation []string 14 | for k, v := range request { 15 | if v == "" { 16 | errorsValidation = append(errorsValidation, fmt.Sprintf("%s cannot be empty", k)) 17 | } 18 | } 19 | 20 | validationMap := map[string]string{ 21 | "name": "omitempty,gt=3,lt=100", 22 | "description": "omitempty,gt=3,lt=100", 23 | "ean_code": "omitempty,gt=3,lt=100", 24 | "laboratory": "omitempty,gt=3,lt=100", 25 | } 26 | 27 | validate := validator.New() 28 | err := validate.RegisterValidation("update_validation", func(fl validator.FieldLevel) bool { 29 | m, ok := fl.Field().Interface().(map[string]any) 30 | if !ok { 31 | return false 32 | } 33 | for k, rule := range validationMap { 34 | if val, exists := m[k]; exists { 35 | errValidate := validate.Var(val, rule) 36 | if errValidate != nil { 37 | validatorErr := errValidate.(validator.ValidationErrors) 38 | errorsValidation = append( 39 | errorsValidation, 40 | fmt.Sprintf("%s do not satisfy condition %v=%v", k, validatorErr[0].Tag(), validatorErr[0].Param()), 41 | ) 42 | } 43 | } 44 | } 45 | return true 46 | }) 47 | if err != nil { 48 | return domainErrors.NewAppError(err, domainErrors.UnknownError) 49 | } 50 | 51 | err = validate.Var(request, "update_validation") 52 | if err != nil { 53 | return domainErrors.NewAppError(err, domainErrors.UnknownError) 54 | } 55 | if len(errorsValidation) > 0 { 56 | return domainErrors.NewAppError(errors.New(strings.Join(errorsValidation, ", ")), domainErrors.ValidationError) 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/user/Mapper.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | userDomain "github.com/gbrayhan/microservices-go/src/domain/user" 5 | ) 6 | 7 | func domainToResponseMapper(domainUser *userDomain.User) *ResponseUser { 8 | return &ResponseUser{ 9 | ID: domainUser.ID, 10 | UserName: domainUser.UserName, 11 | Email: domainUser.Email, 12 | FirstName: domainUser.FirstName, 13 | LastName: domainUser.LastName, 14 | Status: domainUser.Status, 15 | CreatedAt: domainUser.CreatedAt, 16 | UpdatedAt: domainUser.UpdatedAt, 17 | } 18 | } 19 | 20 | func arrayDomainToResponseMapper(users *[]userDomain.User) *[]ResponseUser { 21 | res := make([]ResponseUser, len(*users)) 22 | for i, u := range *users { 23 | res[i] = *domainToResponseMapper(&u) 24 | } 25 | return &res 26 | } 27 | 28 | func toUsecaseMapper(req *NewUserRequest) *userDomain.User { 29 | return &userDomain.User{ 30 | UserName: req.UserName, 31 | Email: req.Email, 32 | FirstName: req.FirstName, 33 | LastName: req.LastName, 34 | Password: req.Password, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/user/Requests.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | type NewUserRequest struct { 4 | UserName string `json:"user" binding:"required"` 5 | Email string `json:"email" binding:"required"` 6 | FirstName string `json:"firstName" binding:"required"` 7 | LastName string `json:"lastName" binding:"required"` 8 | Password string `json:"password" binding:"required"` 9 | Role string `json:"role" binding:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/user/Responses.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "time" 4 | 5 | type ResponseUser struct { 6 | ID int `json:"id"` 7 | UserName string `json:"user"` 8 | Email string `json:"email"` 9 | FirstName string `json:"firstName"` 10 | LastName string `json:"lastName"` 11 | Status bool `json:"status"` 12 | CreatedAt time.Time `json:"createdAt,omitempty"` 13 | UpdatedAt time.Time `json:"updatedAt,omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/user/User.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 6 | domainUser "github.com/gbrayhan/microservices-go/src/domain/user" 7 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers" 8 | "github.com/gin-gonic/gin" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | type IUserController interface { 14 | NewUser(ctx *gin.Context) 15 | GetAllUsers(ctx *gin.Context) 16 | GetUsersByID(ctx *gin.Context) 17 | UpdateUser(ctx *gin.Context) 18 | DeleteUser(ctx *gin.Context) 19 | } 20 | 21 | type UserController struct { 22 | userService domainUser.IUserService 23 | } 24 | 25 | func NewUserController(userService domainUser.IUserService) IUserController { 26 | return &UserController{userService} 27 | } 28 | 29 | func (c *UserController) NewUser(ctx *gin.Context) { 30 | var request NewUserRequest 31 | if err := controllers.BindJSON(ctx, &request); err != nil { 32 | appError := domainErrors.NewAppError(err, domainErrors.ValidationError) 33 | _ = ctx.Error(appError) 34 | return 35 | } 36 | userModel, err := c.userService.Create(toUsecaseMapper(&request)) 37 | if err != nil { 38 | _ = ctx.Error(err) 39 | return 40 | } 41 | userResponse := domainToResponseMapper(userModel) 42 | ctx.JSON(http.StatusOK, userResponse) 43 | } 44 | 45 | func (c *UserController) GetAllUsers(ctx *gin.Context) { 46 | users, err := c.userService.GetAll() 47 | if err != nil { 48 | appError := domainErrors.NewAppErrorWithType(domainErrors.UnknownError) 49 | _ = ctx.Error(appError) 50 | return 51 | } 52 | ctx.JSON(http.StatusOK, arrayDomainToResponseMapper(users)) 53 | } 54 | 55 | func (c *UserController) GetUsersByID(ctx *gin.Context) { 56 | userID, err := strconv.Atoi(ctx.Param("id")) 57 | if err != nil { 58 | appError := domainErrors.NewAppError(errors.New("user id is invalid"), domainErrors.ValidationError) 59 | _ = ctx.Error(appError) 60 | return 61 | } 62 | user, err := c.userService.GetByID(userID) 63 | if err != nil { 64 | _ = ctx.Error(err) 65 | return 66 | } 67 | ctx.JSON(http.StatusOK, domainToResponseMapper(user)) 68 | } 69 | 70 | func (c *UserController) UpdateUser(ctx *gin.Context) { 71 | userID, err := strconv.Atoi(ctx.Param("id")) 72 | if err != nil { 73 | appError := domainErrors.NewAppError(errors.New("param id is necessary"), domainErrors.ValidationError) 74 | _ = ctx.Error(appError) 75 | return 76 | } 77 | var requestMap map[string]any 78 | err = controllers.BindJSONMap(ctx, &requestMap) 79 | if err != nil { 80 | appError := domainErrors.NewAppError(err, domainErrors.ValidationError) 81 | _ = ctx.Error(appError) 82 | return 83 | } 84 | err = updateValidation(requestMap) 85 | if err != nil { 86 | _ = ctx.Error(err) 87 | return 88 | } 89 | userUpdated, err := c.userService.Update(userID, requestMap) 90 | if err != nil { 91 | _ = ctx.Error(err) 92 | return 93 | } 94 | ctx.JSON(http.StatusOK, domainToResponseMapper(userUpdated)) 95 | } 96 | 97 | func (c *UserController) DeleteUser(ctx *gin.Context) { 98 | userID, err := strconv.Atoi(ctx.Param("id")) 99 | if err != nil { 100 | appError := domainErrors.NewAppError(errors.New("param id is necessary"), domainErrors.ValidationError) 101 | _ = ctx.Error(appError) 102 | return 103 | } 104 | err = c.userService.Delete(userID) 105 | if err != nil { 106 | _ = ctx.Error(err) 107 | return 108 | } 109 | ctx.JSON(http.StatusOK, gin.H{"message": "resource deleted successfully"}) 110 | } 111 | -------------------------------------------------------------------------------- /src/infrastructure/rest/controllers/user/Validation.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 9 | "github.com/go-playground/validator/v10" 10 | ) 11 | 12 | func updateValidation(request map[string]any) error { 13 | var errorsValidation []string 14 | for k, v := range request { 15 | if v == "" { 16 | errorsValidation = append(errorsValidation, fmt.Sprintf("%s cannot be empty", k)) 17 | } 18 | } 19 | 20 | validationMap := map[string]string{ 21 | "user_name": "omitempty,gt=3,lt=100", 22 | "email": "omitempty,email", 23 | "firstName": "omitempty,gt=1,lt=100", 24 | "lastName": "omitempty,gt=1,lt=100", 25 | } 26 | 27 | validate := validator.New() 28 | err := validate.RegisterValidation("update_validation", func(fl validator.FieldLevel) bool { 29 | m, ok := fl.Field().Interface().(map[string]any) 30 | if !ok { 31 | return false 32 | } 33 | for k, rule := range validationMap { 34 | if val, exists := m[k]; exists { 35 | errValidate := validate.Var(val, rule) 36 | if errValidate != nil { 37 | validatorErr := errValidate.(validator.ValidationErrors) 38 | errorsValidation = append( 39 | errorsValidation, 40 | fmt.Sprintf("%s does not satisfy condition %v=%v", k, validatorErr[0].Tag(), validatorErr[0].Param()), 41 | ) 42 | } 43 | } 44 | } 45 | return true 46 | }) 47 | if err != nil { 48 | return domainErrors.NewAppError(err, domainErrors.UnknownError) 49 | } 50 | 51 | err = validate.Var(request, "update_validation") 52 | if err != nil { 53 | return domainErrors.NewAppError(err, domainErrors.UnknownError) 54 | } 55 | if len(errorsValidation) > 0 { 56 | return domainErrors.NewAppError(errors.New(strings.Join(errorsValidation, ", ")), domainErrors.ValidationError) 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /src/infrastructure/rest/middlewares/Headers.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func CommonHeaders(c *gin.Context) { 6 | c.Header("Access-Control-Allow-Origin", "*") 7 | c.Header("Access-Control-Allow-Credentials", "true") 8 | c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, DELETE, GET, PUT") 9 | c.Header("Access-Control-Allow-Headers", 10 | "Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-CompanyName, Cache-Control") 11 | c.Header("X-Frame-Options", "SAMEORIGIN") 12 | c.Header("Cache-Control", "no-cache, no-store") 13 | c.Header("Pragma", "no-cache") 14 | c.Header("Expires", "0") 15 | 16 | c.Next() 17 | } 18 | -------------------------------------------------------------------------------- /src/infrastructure/rest/middlewares/Interceptor.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type bodyLogWriter struct { 13 | gin.ResponseWriter 14 | body *bytes.Buffer 15 | } 16 | 17 | func (w bodyLogWriter) Write(b []byte) (int, error) { 18 | w.body.Write(b) 19 | return w.ResponseWriter.Write(b) 20 | } 21 | 22 | func GinBodyLogMiddleware(c *gin.Context) { 23 | blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} 24 | c.Writer = blw 25 | 26 | buf := make([]byte, 4096) 27 | num, err := c.Request.Body.Read(buf) 28 | if err != nil && err.Error() != "EOF" { 29 | _ = fmt.Errorf("error reading buffer: %s", err.Error()) 30 | } 31 | reqBody := string(buf[0:num]) 32 | c.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(reqBody))) 33 | 34 | c.Next() 35 | 36 | loc, _ := time.LoadLocation("America/Mexico_City") 37 | allDataIO := map[string]any{ 38 | "ruta": c.FullPath(), 39 | "request_uri": c.Request.RequestURI, 40 | "raw_request": reqBody, 41 | "status_code": c.Writer.Status(), 42 | "body_response": blw.body.String(), 43 | "errors": c.Errors.Errors(), 44 | "created_at": time.Now().In(loc).Format("2006-01-02T15:04:05"), 45 | } 46 | _ = fmt.Sprintf("%v", allDataIO) 47 | } 48 | -------------------------------------------------------------------------------- /src/infrastructure/rest/middlewares/RequiresLogin.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/golang-jwt/jwt/v4" 10 | ) 11 | 12 | func AuthJWTMiddleware() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | tokenString := c.GetHeader("Authorization") 15 | if tokenString == "" { 16 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Token not provided"}) 17 | c.Abort() 18 | return 19 | } 20 | accessSecret := os.Getenv("JWT_ACCESS_SECRET") 21 | if accessSecret == "" { 22 | c.JSON(http.StatusUnauthorized, gin.H{"error": "JWT_ACCESS_SECRET not configured"}) 23 | c.Abort() 24 | return 25 | } 26 | if strings.HasPrefix(tokenString, "Bearer ") { 27 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") 28 | } 29 | claims := jwt.MapClaims{} 30 | _, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (any, error) { 31 | return []byte(accessSecret), nil 32 | }) 33 | if err != nil { 34 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 35 | c.Abort() 36 | return 37 | } 38 | if exp, ok := claims["exp"].(float64); ok { 39 | if int64(exp) < (jwt.TimeFunc().Unix()) { 40 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Token expired"}) 41 | c.Abort() 42 | return 43 | } 44 | } else { 45 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"}) 46 | c.Abort() 47 | return 48 | } 49 | if t, ok := claims["type"].(string); ok { 50 | if t != "access" { 51 | c.JSON(http.StatusForbidden, gin.H{"error": "Token type mismatch"}) 52 | c.Abort() 53 | return 54 | } 55 | } else { 56 | c.JSON(http.StatusForbidden, gin.H{"error": "Missing token type"}) 57 | c.Abort() 58 | return 59 | } 60 | c.Next() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/infrastructure/rest/middlewares/errorHandler.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func ErrorHandler() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | c.Next() 13 | 14 | if len(c.Errors) > 0 { 15 | err := c.Errors.Last().Err 16 | var appErr *domainErrors.AppError 17 | if errors.As(err, &appErr) { 18 | switch appErr.Type { 19 | case domainErrors.NotFound: 20 | c.JSON(http.StatusNotFound, gin.H{"error": appErr.Error()}) 21 | case domainErrors.ValidationError: 22 | c.JSON(http.StatusBadRequest, gin.H{"error": appErr.Error()}) 23 | case domainErrors.RepositoryError: 24 | c.JSON(http.StatusInternalServerError, gin.H{"error": appErr.Error()}) 25 | case domainErrors.NotAuthenticated: 26 | c.JSON(http.StatusUnauthorized, gin.H{"error": appErr.Error()}) 27 | case domainErrors.NotAuthorized: 28 | c.JSON(http.StatusForbidden, gin.H{"error": appErr.Error()}) 29 | default: 30 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) 31 | } 32 | } else { 33 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/infrastructure/rest/routes/auth.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | authController "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers/auth" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func AuthRoutes(router *gin.RouterGroup, controller authController.IAuthController) { 9 | routerAuth := router.Group("/auth") 10 | { 11 | routerAuth.POST("/login", controller.Login) 12 | routerAuth.POST("/access-token", controller.GetAccessTokenByRefreshToken) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/infrastructure/rest/routes/medicine.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers/medicine" 5 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/middlewares" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func MedicineRoutes(router *gin.RouterGroup, controller medicine.IMedicineController) { 10 | med := router.Group("/medicine") 11 | med.Use(middlewares.AuthJWTMiddleware()) 12 | { 13 | med.GET("/", controller.GetAllMedicines) 14 | med.POST("/", controller.NewMedicine) 15 | med.GET("/:id", controller.GetMedicinesByID) 16 | med.POST("/data", controller.GetDataMedicines) 17 | med.PUT("/:id", controller.UpdateMedicine) 18 | med.DELETE("/:id", controller.DeleteMedicine) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/infrastructure/rest/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/adapter" 5 | "github.com/gin-gonic/gin" 6 | swaggerFiles "github.com/swaggo/files" 7 | ginSwagger "github.com/swaggo/gin-swagger" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | func ApplicationRouter(router *gin.Engine, db *gorm.DB) { 12 | v1 := router.Group("/v1") 13 | 14 | v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 15 | 16 | AuthRoutes(v1, adapter.AuthAdapter(db)) 17 | UserRoutes(v1, adapter.UserAdapter(db)) 18 | MedicineRoutes(v1, adapter.MedicineAdapter(db)) 19 | } 20 | -------------------------------------------------------------------------------- /src/infrastructure/rest/routes/user.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/controllers/user" 5 | "github.com/gbrayhan/microservices-go/src/infrastructure/rest/middlewares" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func UserRoutes(router *gin.RouterGroup, controller user.IUserController) { 10 | u := router.Group("/user") 11 | u.Use(middlewares.AuthJWTMiddleware()) 12 | { 13 | u.POST("/", controller.NewUser) 14 | u.GET("/", controller.GetAllUsers) 15 | u.GET("/:id", controller.GetUsersByID) 16 | u.PUT("/:id", controller.UpdateUser) 17 | u.DELETE("/:id", controller.DeleteUser) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/infrastructure/security/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | domainErrors "github.com/gbrayhan/microservices-go/src/domain/errors" 7 | "github.com/golang-jwt/jwt/v4" 8 | "os" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const ( 14 | Access = "access" 15 | Refresh = "refresh" 16 | ) 17 | 18 | type AppToken struct { 19 | Token string `json:"token"` 20 | TokenType string `json:"type"` 21 | ExpirationTime time.Time `json:"expirationTime"` 22 | } 23 | 24 | type Claims struct { 25 | ID int `json:"id"` 26 | Type string `json:"type"` 27 | jwt.RegisteredClaims 28 | } 29 | 30 | func GenerateJWTToken(userID int, tokenType string) (*AppToken, error) { 31 | var secretKey string 32 | var expStr string 33 | 34 | switch tokenType { 35 | case Access: 36 | secretKey = os.Getenv("JWT_ACCESS_SECRET") 37 | expStr = os.Getenv("JWT_ACCESS_TIME_MINUTE") 38 | case Refresh: 39 | secretKey = os.Getenv("JWT_REFRESH_SECRET") 40 | expStr = os.Getenv("JWT_REFRESH_TIME_HOUR") 41 | default: 42 | return nil, errors.New("invalid token type") 43 | } 44 | 45 | if secretKey == "" || expStr == "" { 46 | return nil, errors.New("missing token environment variables") 47 | } 48 | 49 | tokenTimeConverted, err := strconv.ParseInt(expStr, 10, 64) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | var duration time.Duration 55 | switch tokenType { 56 | case Refresh: 57 | duration = time.Duration(tokenTimeConverted) * time.Hour 58 | case Access: 59 | duration = time.Duration(tokenTimeConverted) * time.Minute 60 | } 61 | 62 | nowTime := time.Now() 63 | expirationTokenTime := nowTime.Add(duration) 64 | 65 | tokenClaims := &Claims{ 66 | ID: userID, 67 | Type: tokenType, 68 | RegisteredClaims: jwt.RegisteredClaims{ 69 | ExpiresAt: jwt.NewNumericDate(expirationTokenTime), 70 | }, 71 | } 72 | tokenWithClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenClaims) 73 | 74 | tokenStr, err := tokenWithClaims.SignedString([]byte(secretKey)) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return &AppToken{ 80 | Token: tokenStr, 81 | TokenType: tokenType, 82 | ExpirationTime: expirationTokenTime, 83 | }, nil 84 | } 85 | 86 | func GetClaimsAndVerifyToken(tokenString string, tokenType string) (jwt.MapClaims, error) { 87 | var secretKey string 88 | if tokenType == Refresh { 89 | secretKey = os.Getenv("JWT_REFRESH_SECRET") 90 | } else { 91 | secretKey = os.Getenv("JWT_ACCESS_SECRET") 92 | } 93 | if secretKey == "" { 94 | return nil, domainErrors.NewAppError(errors.New("missing token environment variable"), domainErrors.NotAuthenticated) 95 | } 96 | 97 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { 98 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 99 | return nil, domainErrors.NewAppError(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), domainErrors.NotAuthenticated) 100 | } 101 | return []byte(secretKey), nil 102 | }) 103 | 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 109 | if claims["type"] != tokenType { 110 | return nil, domainErrors.NewAppError(errors.New("invalid token type"), domainErrors.NotAuthenticated) 111 | } 112 | var timeExpire = claims["exp"].(float64) 113 | if time.Now().Unix() > int64(timeExpire) { 114 | return nil, domainErrors.NewAppError(errors.New("token expired"), domainErrors.NotAuthenticated) 115 | } 116 | return claims, nil 117 | } 118 | return nil, err 119 | } 120 | --------------------------------------------------------------------------------