├── .air.toml ├── .env.example ├── .github └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .gitlab-ci.yml ├── .golangci.yml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── configs ├── config.go ├── constants.go └── pagination.go ├── docker-compose.yml ├── docs ├── docs.go ├── go-gin-api.postman_collection.json ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── handlers ├── article_handler.go ├── requests │ ├── article.go │ ├── user.go │ └── validator.go ├── responses │ ├── pagination.go │ └── user.go └── user_handler.go ├── infras ├── di.go ├── wire.go └── wire_gen.go ├── main.go ├── middleware ├── cors.go ├── jwt.go └── response.go ├── migrations ├── migration.go └── seed.go ├── models ├── article.go └── user.go ├── pkgs └── jwt-token │ └── token.go ├── router └── router.go ├── services ├── article.go └── user.go ├── testing.env ├── tests └── unit_test.go └── utils ├── array.go ├── array_test.go ├── bytes.go ├── bytes_test.go ├── datetime.go ├── datetime_test.go ├── file.go ├── file_test.go ├── map.go ├── password.go ├── password_test.go └── response.go /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "go build -o ./tmp/main ." 9 | delay = 0 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | rerun = false 22 | rerun_delay = 500 23 | send_interrupt = false 24 | stop_on_error = false 25 | 26 | [color] 27 | app = "" 28 | build = "yellow" 29 | main = "magenta" 30 | runner = "green" 31 | watcher = "cyan" 32 | 33 | [log] 34 | main_only = false 35 | time = false 36 | 37 | [misc] 38 | clean_on_exit = false 39 | 40 | [screen] 41 | clear_on_rebuild = false 42 | keep_scroll = true 43 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY=1234567890 2 | # -- API --- 3 | APP_MODE=debug# debug, release, test 4 | APP_TIMEZONE= 5 | PORT=8081 6 | DOMAIN=localhost 7 | HTTP_SCHEMA= 8 | 9 | # --- Database --- 10 | DATABASE_USER_NAME= 11 | DATABASE_PASSWORD= 12 | DATABASE_HOST=127.0.0.1 13 | DATABASE_PORT=5432 14 | DATABASE_NAME=api_db 15 | DATABASE_TIMEZONE=Europe/London 16 | 17 | # --- Secutiry --- 18 | CLIENT_ORIGIN=http://localhost:3000 19 | ALLOW_ORIGIN=http://localhost:8080,http://localhost:3000,https://xxx.com 20 | TOKEN_EXPIRED_IN=60m 21 | TOKEN_MAX_AGE=60 22 | TOKEN_SECRET= 23 | SENTRY_DSN= 24 | 25 | 26 | # --- Redis for Jobs & Queue --- 27 | REDIS_ENDPOINT=xxx.ec2.cloud.redislabs.com:13999 28 | REDIS_PASSWORD=xxx 29 | 30 | # --- GG API --- 31 | MAPS_API_URL= 32 | MAPS_API_KEY= 33 | -------------------------------------------------------------------------------- /.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.19 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | permissions: 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | # pull-requests: read 13 | jobs: 14 | golangci: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/setup-go@v4 19 | with: 20 | go-version: '1.17' 21 | cache: false 22 | - uses: actions/checkout@v3 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v3 25 | with: 26 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 27 | version: v1.52.2 28 | 29 | # Optional: working directory, useful for monorepos 30 | # working-directory: somedir 31 | 32 | # Optional: golangci-lint command line arguments. 33 | # args: --issues-exit-code=0 34 | 35 | # Optional: show only new issues if it's a pull request. The default value is `false`. 36 | # only-new-issues: true 37 | 38 | # Optional: if set to true then the all caching functionality will be complete disabled, 39 | # takes precedence over all other caching options. 40 | # skip-cache: true 41 | 42 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 43 | # skip-pkg-cache: true 44 | 45 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 46 | # skip-build-cache: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | tmp 3 | .idea/ 4 | uploads 5 | .storages/ -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # This file is a template, and might need editing before it works on your project. 2 | # You can copy and paste this template into a new `.gitlab-ci.yml` file. 3 | # You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. 4 | # 5 | # To contribute improvements to CI/CD templates, please follow the Development guide at: 6 | # https://docs.gitlab.com/ee/development/cicd/templates.html 7 | # This specific template is located at: 8 | # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Go.gitlab-ci.yml 9 | 10 | image: golang:1.19.5 11 | 12 | stages: 13 | - test 14 | - build 15 | - deploy 16 | 17 | lint: 18 | image: golangci/golangci-lint:v1.51.0 19 | stage: test 20 | script: 21 | - golangci-lint run 22 | 23 | test: 24 | stage: test 25 | script: 26 | - go fmt $(go list ./... | grep -v /vendor/) 27 | - go vet $(go list ./... | grep -v /vendor/) 28 | - go test -race $(go list ./... | grep -v /vendor/) 29 | 30 | build: 31 | stage: build 32 | script: 33 | - mkdir -p mybinaries 34 | - go build -o mybinaries ./... 35 | artifacts: 36 | paths: 37 | - mybinaries 38 | 39 | deploy: 40 | stage: deploy 41 | script: echo "Define your deployment script!" 42 | environment: production 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | image: golang:latest 58 | 59 | stages: 60 | - test 61 | - build 62 | - deploy 63 | 64 | lint: 65 | image: golangci/golangci-lint:v1.51.0 66 | stage: test 67 | script: 68 | - golangci-lint run 69 | 70 | compile: 71 | stage: build 72 | script: 73 | - mkdir -p mybinaries 74 | - go build -o mybinaries ./... 75 | artifacts: 76 | paths: 77 | - mybinaries 78 | 79 | test: 80 | image: golang:1.13.3-alpine3.10 81 | tags: 82 | - dind 83 | - docker 84 | stage: test 85 | script: 86 | - go get -v ./... 87 | - go test -v ./... 88 | 89 | deploy: 90 | stage: deploy 91 | script: echo "Define your deployment script!" 92 | environment: production 93 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | errcheck: 3 | check-type-assertions: true 4 | goconst: 5 | min-len: 2 6 | min-occurrences: 3 7 | gocritic: 8 | enabled-tags: 9 | - diagnostic 10 | - experimental 11 | - opinionated 12 | - performance 13 | - style 14 | nolintlint: 15 | require-explanation: true 16 | require-specific: true 17 | 18 | linters: 19 | disable-all: true 20 | enable: 21 | - bodyclose 22 | - depguard 23 | - dogsled 24 | - dupl 25 | - errcheck 26 | - exportloopref 27 | - exhaustive 28 | - goconst 29 | - gocritic 30 | - gofmt 31 | - goimports 32 | - gomnd 33 | - gocyclo 34 | - gosec 35 | - gosimple 36 | - govet 37 | - ineffassign 38 | - misspell 39 | - nolintlint 40 | - nakedret 41 | - prealloc 42 | - predeclared 43 | - revive 44 | - staticcheck 45 | - stylecheck 46 | - thelper 47 | - tparallel 48 | - typecheck 49 | - unconvert 50 | - unparam 51 | - unused 52 | - whitespace 53 | - wsl 54 | 55 | run: 56 | issues-exit-code: 1 57 | skip-dirs: 58 | - tests 59 | - utils 60 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/tekwizely/pre-commit-golang 3 | rev: v0.8.3 4 | hooks: 5 | - id: golangci-lint 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Citizen Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Golang Api Template is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Golang Api Template to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open [Source/Culture/Tech] Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people's personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone's consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Weapons Policy 47 | 48 | No weapons will be allowed at Golang Api Template events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. 49 | 50 | ## 6. Consequences of Unacceptable Behavior 51 | 52 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 53 | 54 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 55 | 56 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 57 | 58 | ## 7. Reporting Guidelines 59 | 60 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. . 61 | 62 | 63 | 64 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 65 | 66 | ## 8. Addressing Grievances 67 | 68 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify GoldenOwlAsia with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 69 | 70 | 71 | 72 | ## 9. Scope 73 | 74 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. 75 | 76 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 77 | 78 | ## 10. Contact info 79 | 80 | 81 | 82 | ## 11. License and attribution 83 | 84 | The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 85 | 86 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 87 | 88 | _Revision 2.3. Posted 6 March 2017._ 89 | 90 | _Revision 2.2. Posted 4 February 2016._ 91 | 92 | _Revision 2.1. Posted 23 June 2014._ 93 | 94 | _Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ 95 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.20 2 | ARG ALPINE_VERSION=3.17.2 3 | 4 | FROM golang:1.20-alpine AS builder 5 | RUN apk update && apk add alpine-sdk git && rm -rf /var/cache/apk/* 6 | WORKDIR /go/src/project/ 7 | COPY . /go/src/project 8 | RUN go mod download 9 | RUN go build -o /bin/project 10 | 11 | #FROM alpine:3.17.2 12 | #RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 13 | #WORKDIR /go/src/project/ 14 | #COPY --from=build /bin/project /bin/project 15 | #COPY --from=build /go/src/project/.env . 16 | 17 | EXPOSE 8080 18 | ENTRYPOINT [ "/bin/project" ] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GoldenOwlAsia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go run main.go 3 | start: 4 | go run main.go 5 | watch: 6 | air -d 7 | build: 8 | go build -o api . 9 | tidy: 10 | go mod download && go mod tidy 11 | test: 12 | go test ./... -v -cover 13 | lint: 14 | golangci-lint run -v 15 | di: 16 | cd infras && wire && cd ../ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golden Owl Golang Gin API Template 2 | 3 |

4 | accessibility text 5 |

6 | 7 | ### Why Using Gin For Golang Backend? 8 | 9 | Gin allows you to build web applications and microservices in Go. It contains a set of commonly used functionalities (e.g., routing, middleware support, rendering, etc.) that reduce boilerplate code and make it simpler to build web applications. 10 | 11 | 12 | ### Prerequisites 13 | 14 | 1. Install go. You can download the Golang in this [page](https://go.dev/doc/install). You should install version 1.20 15 | 2. Install Postgres database. You can download the Postgres in this [page](https://www.postgresql.org/download/). You should install version 14.1 16 | 3. Make an `.env` file from `.env.example` 17 | 4. Go to `pgadmin` create a database. Note the name of it and add to `.env` 18 | 5. Install **Air - live reload fo Go apps**. You can visit this [page](https://github.com/cosmtrek/air). 19 | 20 | ### 💿 Installation 21 | 22 | #### Via `go` 23 | 24 | 1. You run this command to install packages 25 | ```sh 26 | go mod download && go mod tidy 27 | ``` 28 | 2. Create `.env` file from `.env.example` file. 29 | 3. run this command to start (hot reload): 30 | ```sh 31 | make watch 32 | ``` 33 | run without hot reload 34 | ```sh 35 | make run 36 | ``` 37 | 4. Visit: http://localhost:8080/swagger/index.html to access the API interface. 38 | #### Via `docker` 39 | Run by docker 40 | ```sh 41 | docker-compose up 42 | ``` 43 | 44 | ### Build go executables 45 | build api executable file 46 | ```sh 47 | make build 48 | ``` 49 | 50 | 51 | 52 | [Golang]: https://img.shields.io/badge/go-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white 53 | [Golang-url]: https://go.dev/doc/ 54 | 55 | 56 | #### 📌 Database Diagram 57 | [![](https://mermaid.ink/img/pako:eNqtkk1PwzAMhv9K5HM3pZ90uU0bBw5IiLEL6iVqvRGpTabEFYyu_53QgTq2cUDCJ_uxI7-x3UFpKgQBaJdKbq1sCs28rR1ad-Ky7hh8Wqs0sbslO7Ux68gqvR1eadngb3n2IJ17Nba6zNw2UtWX-NHUeElXJKl1Iyfley4sSsJqTmd8vauu8iXWeML7Qh-duSVV1uh-Rn-eBXtSdE37wmhCTf8uftza4TCZHLpRuWCl7ymVdhBAg9ZPuvLLHz5UAL1ggwUI71a4kW1NBRS696WyJbPa6xIE2RYDaActXxcDYiNr5-lO6mdjmu8iH4Lo4A1EnOXTWZImYRjFPL7hWQB7EJMkn0ZpksY5z_Mw4rM-gPfhfTjlaZ7FnKchD7M8ytIAsFJk7P3xWoej7T8A9wvK7w?type=png)](https://mermaid.live/edit#pako:eNqtkk1PwzAMhv9K5HM3pZ90uU0bBw5IiLEL6iVqvRGpTabEFYyu_53QgTq2cUDCJ_uxI7-x3UFpKgQBaJdKbq1sCs28rR1ad-Ky7hh8Wqs0sbslO7Ux68gqvR1eadngb3n2IJ17Nba6zNw2UtWX-NHUeElXJKl1Iyfley4sSsJqTmd8vauu8iXWeML7Qh-duSVV1uh-Rn-eBXtSdE37wmhCTf8uftza4TCZHLpRuWCl7ymVdhBAg9ZPuvLLHz5UAL1ggwUI71a4kW1NBRS696WyJbPa6xIE2RYDaActXxcDYiNr5-lO6mdjmu8iH4Lo4A1EnOXTWZImYRjFPL7hWQB7EJMkn0ZpksY5z_Mw4rM-gPfhfTjlaZ7FnKchD7M8ytIAsFJk7P3xWoej7T8A9wvK7w) 58 | 59 | #### 🔗 Converts Go annotations to Swagger Documentation 2.0 60 | Run `swag init` in the project's root folder which contains the `main.go` file. This will parse your comments and generate the required files (`docs` folder and `docs/docs.go`). 61 | ```sh 62 | swag init 63 | ``` 64 | 65 | 66 | #### 💉 Dependency injection with Wire 67 | 1. change configuration in file ```wire.go ``` 68 | 2. run the following command to automatically generate the code 69 | ```sh 70 | make di 71 | ``` 72 | 73 | #### 🧪 testing 74 | ```sh 75 | make test 76 | # or 77 | go test ./... -cover 78 | # with cover 79 | go test ./... -cover 80 | # with verbose 81 | go test -v ./... -cover 82 | # specific folder 83 | go test -v ./utils -cover 84 | # specific test file 85 | go test ./utils/array_test.go ./utils/array.go 86 | # one unit test 87 | # - api/utils is a package name 88 | # - TestChunkSlice is a testcase 89 | go test api/utils -run TestChunkSlice 90 | ``` 91 | 92 | #### 🧪 Improve code with lint checks 93 | ```sh 94 | make lint 95 | ``` 96 | 97 | ### Demo 98 | #### Login 99 | ```sh 100 | curl -L 'localhost:8080/api/v1/user/login' \ 101 | -H 'Content-Type: application/json' \ 102 | -d '{ 103 | "username": "admin", 104 | "password": "1234" 105 | }' 106 | ``` 107 | response: 108 | ```json 109 | { 110 | "status": "success", 111 | "message": "welcome back", 112 | "data": { 113 | "user": { 114 | "id": 1, 115 | "created_at": "2023-04-19T14:32:21.978531Z", 116 | "updated_at": "2023-04-19T14:32:21.978531Z", 117 | "username": "admin", 118 | "email": "admin@example.com", 119 | "role": "Admin", 120 | "status": "" 121 | }, 122 | "access_token": "xxx.xxx.xxx", 123 | "refresh_token": "yyy.yyy.yyy" 124 | } 125 | } 126 | ``` 127 | #### Articles 128 | ```shell 129 | curl -L 'localhost:8080/api/v1/articles' -H 'Authorization: Bearer xxx.xxx.xxx-xxx' 130 | ``` 131 | response: 132 | ```json 133 | { 134 | "_metadata": { 135 | "limit": 10, 136 | "total": 54, 137 | "total_pages": 6, 138 | "per_page": 10, 139 | "page": 1, 140 | "sort": "created_at DESC" 141 | }, 142 | "records": [ 143 | { 144 | "id": 59, 145 | "created_at": "2023-04-23T10:38:29.80017Z", 146 | "updated_at": "2023-04-23T10:38:29.80017Z", 147 | "user": { 148 | "id": 1, 149 | "created_at": "2023-04-19T14:32:21.978531Z", 150 | "updated_at": "2023-04-19T14:32:21.978531Z", 151 | "username": "admin", 152 | "email": "admin@example.com", 153 | "role": "Admin", 154 | "status": "" 155 | }, 156 | "title": "test", 157 | "content": "test content" 158 | } 159 | ] 160 | } 161 | ``` 162 | -------------------------------------------------------------------------------- /configs/config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | var ( 10 | ConfApp Config 11 | ) 12 | 13 | type Config struct { 14 | AppMode string `mapstructure:"APP_MODE"` 15 | SecretKey string `mapstructure:"SECRET_KEY"` 16 | AppTimezone string `mapstructure:"APP_TIMEZONE"` 17 | Port string `mapstructure:"PORT"` 18 | HTTPSchema string `mapstructure:"HTTP_SCHEMA"` 19 | Domain string `mapstructure:"DOMAIN"` 20 | DatabaseUserName string `mapstructure:"DATABASE_USER_NAME"` 21 | DatabasePassword string `mapstructure:"DATABASE_PASSWORD"` 22 | DatabaseHost string `mapstructure:"DATABASE_HOST"` 23 | DatabaseName string `mapstructure:"DATABASE_NAME"` 24 | DatabasePort int `mapstructure:"DATABASE_PORT"` 25 | DatabaseTimezone string `mapstructure:"DATABASE_TIMEZONE"` 26 | SentryDNS string `mapstructure:"SENTRY_DSN"` 27 | AllowOrigin string `mapstructure:"ALLOW_ORIGIN"` 28 | TokenSecret string `mapstructure:"TOKEN_SECRET"` 29 | TokenExpiresIn time.Duration `mapstructure:"TOKEN_EXPIRED_IN"` 30 | TokenMaxAge int `mapstructure:"TOKEN_MAX_AGE"` 31 | } 32 | 33 | func LoadConfig(path, nvFile string) (config Config, err error) { 34 | viper.AddConfigPath(path) 35 | viper.SetConfigFile(nvFile) 36 | viper.AutomaticEnv() 37 | err = viper.ReadInConfig() 38 | 39 | if err != nil { 40 | return 41 | } 42 | 43 | err = viper.Unmarshal(&config) 44 | ConfApp = config 45 | 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /configs/constants.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import "time" 4 | 5 | const ( 6 | DefaultDateCsvLayoutFormat = "02/01/2006" 7 | AccessTokenExpireTime = 15 * time.Minute 8 | RefreshTokenExpireTime = 24 * time.Hour 9 | ReadHeaderTimeout = 5 * time.Second 10 | ) 11 | -------------------------------------------------------------------------------- /configs/pagination.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | const ( 4 | DefaultPage = 1 5 | DefaultItemPerPage = 10 6 | DefaultSorting = "created_at DESC" 7 | ) 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | db: 5 | image: postgres:15 6 | restart: always 7 | container_name: web-postgres 8 | env_file: .env 9 | ports: 10 | - "5433:5432" 11 | volumes: 12 | - data:/var/lib/postgresql/data 13 | environment: 14 | POSTGRES_USER: ${DATABASE_USER_NAME} 15 | POSTGRES_PASSWORD: ${DATABASE_PASSWORD} 16 | 17 | api: 18 | build: 19 | context: . 20 | dockerfile: Dockerfile 21 | env_file: .env 22 | container_name: web 23 | ports: 24 | - "8080:8080" 25 | depends_on: 26 | - db 27 | networks: 28 | - default 29 | restart: always 30 | 31 | volumes: 32 | data: 33 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // 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 | "contact": { 13 | "name": "goldenowl.asia", 14 | "email": "hello@goldenowl.asia" 15 | }, 16 | "license": { 17 | "name": "Apache 2.0", 18 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 19 | }, 20 | "version": "{{.Version}}" 21 | }, 22 | "host": "{{.Host}}", 23 | "basePath": "{{.BasePath}}", 24 | "paths": { 25 | "/api/v1/user/login": { 26 | "post": { 27 | "description": "login by username \u0026 password", 28 | "consumes": [ 29 | "application/json" 30 | ], 31 | "produces": [ 32 | "application/json" 33 | ], 34 | "tags": [ 35 | "users" 36 | ], 37 | "summary": "Login user to system", 38 | "parameters": [ 39 | { 40 | "description": "body params", 41 | "name": "json", 42 | "in": "body", 43 | "required": true, 44 | "schema": { 45 | "$ref": "#/definitions/requests.UserLoginRequest" 46 | } 47 | } 48 | ], 49 | "responses": { 50 | "200": { 51 | "description": "OK", 52 | "schema": { 53 | "allOf": [ 54 | { 55 | "$ref": "#/definitions/utils.ResponseSuccess" 56 | }, 57 | { 58 | "type": "object", 59 | "properties": { 60 | "Data": { 61 | "$ref": "#/definitions/responses.UserLoginResponse" 62 | } 63 | } 64 | } 65 | ] 66 | } 67 | }, 68 | "401": { 69 | "description": "Unauthorized", 70 | "schema": { 71 | "$ref": "#/definitions/utils.ResponseFailed" 72 | } 73 | }, 74 | "422": { 75 | "description": "Unprocessable Entity", 76 | "schema": { 77 | "$ref": "#/definitions/utils.ResponseFailed" 78 | } 79 | } 80 | } 81 | } 82 | } 83 | }, 84 | "definitions": { 85 | "requests.UserLoginRequest": { 86 | "type": "object", 87 | "properties": { 88 | "password": { 89 | "type": "string" 90 | }, 91 | "username": { 92 | "type": "string" 93 | } 94 | } 95 | }, 96 | "responses.UserLoginResponse": { 97 | "type": "object", 98 | "properties": { 99 | "access_token": { 100 | "type": "string" 101 | }, 102 | "id": { 103 | "type": "string" 104 | }, 105 | "refresh_token": { 106 | "type": "string" 107 | } 108 | } 109 | }, 110 | "utils.ResponseFailed": { 111 | "type": "object", 112 | "properties": { 113 | "code": { 114 | "type": "integer" 115 | }, 116 | "message": { 117 | "type": "string" 118 | }, 119 | "status_code": { 120 | "type": "string" 121 | } 122 | } 123 | }, 124 | "utils.ResponseSuccess": { 125 | "type": "object", 126 | "properties": { 127 | "code": { 128 | "type": "integer" 129 | }, 130 | "data": {}, 131 | "message": { 132 | "type": "string" 133 | }, 134 | "status_code": { 135 | "type": "string" 136 | } 137 | } 138 | } 139 | }, 140 | "securityDefinitions": { 141 | "BasicAuth": { 142 | "type": "basic" 143 | } 144 | } 145 | }` 146 | 147 | // SwaggerInfo holds exported Swagger Info so clients can modify it 148 | var SwaggerInfo = &swag.Spec{ 149 | Version: "1.0.0", 150 | Host: "", 151 | BasePath: "/", 152 | Schemes: []string{}, 153 | Title: "GoldenOwl Gin API", 154 | Description: "This API is for GoldenOwl API application", 155 | InfoInstanceName: "swagger", 156 | SwaggerTemplate: docTemplate, 157 | } 158 | 159 | func init() { 160 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 161 | } 162 | -------------------------------------------------------------------------------- /docs/go-gin-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "dbdf92b0-0afa-49eb-9ec5-81566795d14e", 4 | "name": "go-gin-api", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "11864262" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Login", 11 | "request": { 12 | "auth": { 13 | "type": "noauth" 14 | }, 15 | "method": "POST", 16 | "header": [], 17 | "body": { 18 | "mode": "raw", 19 | "raw": "{\n \"username\": \"admin\",\n \"password\": \"1234\"\n}", 20 | "options": { 21 | "raw": { 22 | "language": "json" 23 | } 24 | } 25 | }, 26 | "url": { 27 | "raw": "{{url}}/user/login", 28 | "host": [ 29 | "{{url}}" 30 | ], 31 | "path": [ 32 | "user", 33 | "login" 34 | ] 35 | } 36 | }, 37 | "response": [] 38 | }, 39 | { 40 | "name": "articles", 41 | "protocolProfileBehavior": { 42 | "disableBodyPruning": true 43 | }, 44 | "request": { 45 | "auth": { 46 | "type": "bearer", 47 | "bearer": [ 48 | { 49 | "key": "token", 50 | "value": "{{authToken}}", 51 | "type": "string" 52 | } 53 | ] 54 | }, 55 | "method": "GET", 56 | "header": [], 57 | "body": { 58 | "mode": "raw", 59 | "raw": "", 60 | "options": { 61 | "raw": { 62 | "language": "json" 63 | } 64 | } 65 | }, 66 | "url": { 67 | "raw": "localhost:8080/api/v1/articles", 68 | "host": [ 69 | "localhost" 70 | ], 71 | "port": "8080", 72 | "path": [ 73 | "api", 74 | "v1", 75 | "articles" 76 | ] 77 | } 78 | }, 79 | "response": [] 80 | }, 81 | { 82 | "name": "article by id", 83 | "protocolProfileBehavior": { 84 | "disableBodyPruning": true 85 | }, 86 | "request": { 87 | "auth": { 88 | "type": "bearer", 89 | "bearer": [ 90 | { 91 | "key": "token", 92 | "value": "{{authToken}}", 93 | "type": "string" 94 | } 95 | ] 96 | }, 97 | "method": "GET", 98 | "header": [], 99 | "body": { 100 | "mode": "raw", 101 | "raw": "", 102 | "options": { 103 | "raw": { 104 | "language": "json" 105 | } 106 | } 107 | }, 108 | "url": { 109 | "raw": "localhost:8080/api/v1/articles/10", 110 | "host": [ 111 | "localhost" 112 | ], 113 | "port": "8080", 114 | "path": [ 115 | "api", 116 | "v1", 117 | "articles", 118 | "10" 119 | ] 120 | } 121 | }, 122 | "response": [] 123 | }, 124 | { 125 | "name": "delete article", 126 | "request": { 127 | "auth": { 128 | "type": "bearer", 129 | "bearer": [ 130 | { 131 | "key": "token", 132 | "value": "{{authToken}}", 133 | "type": "string" 134 | } 135 | ] 136 | }, 137 | "method": "DELETE", 138 | "header": [], 139 | "body": { 140 | "mode": "raw", 141 | "raw": "", 142 | "options": { 143 | "raw": { 144 | "language": "json" 145 | } 146 | } 147 | }, 148 | "url": { 149 | "raw": "localhost:8080/api/v1/articles/4", 150 | "host": [ 151 | "localhost" 152 | ], 153 | "port": "8080", 154 | "path": [ 155 | "api", 156 | "v1", 157 | "articles", 158 | "4" 159 | ] 160 | } 161 | }, 162 | "response": [] 163 | }, 164 | { 165 | "name": "create article", 166 | "request": { 167 | "auth": { 168 | "type": "bearer", 169 | "bearer": [ 170 | { 171 | "key": "token", 172 | "value": "{{authToken}}", 173 | "type": "string" 174 | } 175 | ] 176 | }, 177 | "method": "POST", 178 | "header": [], 179 | "body": { 180 | "mode": "raw", 181 | "raw": "{\n \"title\":\"test\",\n \"content\":\"test content\"\n}", 182 | "options": { 183 | "raw": { 184 | "language": "json" 185 | } 186 | } 187 | }, 188 | "url": { 189 | "raw": "localhost:8080/api/v1/articles", 190 | "host": [ 191 | "localhost" 192 | ], 193 | "port": "8080", 194 | "path": [ 195 | "api", 196 | "v1", 197 | "articles" 198 | ] 199 | } 200 | }, 201 | "response": [] 202 | }, 203 | { 204 | "name": "update article", 205 | "request": { 206 | "auth": { 207 | "type": "bearer", 208 | "bearer": [ 209 | { 210 | "key": "token", 211 | "value": "{{authToken}}", 212 | "type": "string" 213 | } 214 | ] 215 | }, 216 | "method": "PUT", 217 | "header": [], 218 | "body": { 219 | "mode": "raw", 220 | "raw": "{\n \"title\":\"test2\",\n \"content\":\"test content2\"\n}", 221 | "options": { 222 | "raw": { 223 | "language": "json" 224 | } 225 | } 226 | }, 227 | "url": { 228 | "raw": "localhost:8080/api/v1/articles/4", 229 | "host": [ 230 | "localhost" 231 | ], 232 | "port": "8080", 233 | "path": [ 234 | "api", 235 | "v1", 236 | "articles", 237 | "4" 238 | ] 239 | } 240 | }, 241 | "response": [] 242 | } 243 | ], 244 | "event": [ 245 | { 246 | "listen": "prerequest", 247 | "script": { 248 | "type": "text/javascript", 249 | "exec": [ 250 | "" 251 | ] 252 | } 253 | }, 254 | { 255 | "listen": "test", 256 | "script": { 257 | "type": "text/javascript", 258 | "exec": [ 259 | "" 260 | ] 261 | } 262 | } 263 | ], 264 | "variable": [ 265 | { 266 | "key": "url", 267 | "value": "localhost:8080/api/v1", 268 | "type": "string" 269 | }, 270 | { 271 | "key": "authToken", 272 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODE0Mjg5NzAsImlhdCI6MTY4MDcwODk3MCwibmJmIjoxNjgwNzA4OTcwLCJzdWIiOiJhZG1pbiJ9.qAqssHGM6KoiYcgrkinK8ayITJYhQMwSRJknjTB2HOo", 273 | "type": "string" 274 | } 275 | ] 276 | } -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This API is for GoldenOwl API application", 5 | "title": "GoldenOwl Gin API", 6 | "contact": { 7 | "name": "goldenowl.asia", 8 | "email": "hello@goldenowl.asia" 9 | }, 10 | "license": { 11 | "name": "Apache 2.0", 12 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | }, 14 | "version": "1.0.0" 15 | }, 16 | "basePath": "/", 17 | "paths": { 18 | "/api/v1/user/login": { 19 | "post": { 20 | "description": "login by username \u0026 password", 21 | "consumes": [ 22 | "application/json" 23 | ], 24 | "produces": [ 25 | "application/json" 26 | ], 27 | "tags": [ 28 | "users" 29 | ], 30 | "summary": "Login user to system", 31 | "parameters": [ 32 | { 33 | "description": "body params", 34 | "name": "json", 35 | "in": "body", 36 | "required": true, 37 | "schema": { 38 | "$ref": "#/definitions/requests.UserLoginRequest" 39 | } 40 | } 41 | ], 42 | "responses": { 43 | "200": { 44 | "description": "OK", 45 | "schema": { 46 | "allOf": [ 47 | { 48 | "$ref": "#/definitions/utils.ResponseSuccess" 49 | }, 50 | { 51 | "type": "object", 52 | "properties": { 53 | "Data": { 54 | "$ref": "#/definitions/responses.UserLoginResponse" 55 | } 56 | } 57 | } 58 | ] 59 | } 60 | }, 61 | "401": { 62 | "description": "Unauthorized", 63 | "schema": { 64 | "$ref": "#/definitions/utils.ResponseFailed" 65 | } 66 | }, 67 | "422": { 68 | "description": "Unprocessable Entity", 69 | "schema": { 70 | "$ref": "#/definitions/utils.ResponseFailed" 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "definitions": { 78 | "requests.UserLoginRequest": { 79 | "type": "object", 80 | "properties": { 81 | "password": { 82 | "type": "string" 83 | }, 84 | "username": { 85 | "type": "string" 86 | } 87 | } 88 | }, 89 | "responses.UserLoginResponse": { 90 | "type": "object", 91 | "properties": { 92 | "access_token": { 93 | "type": "string" 94 | }, 95 | "id": { 96 | "type": "string" 97 | }, 98 | "refresh_token": { 99 | "type": "string" 100 | } 101 | } 102 | }, 103 | "utils.ResponseFailed": { 104 | "type": "object", 105 | "properties": { 106 | "code": { 107 | "type": "integer" 108 | }, 109 | "message": { 110 | "type": "string" 111 | }, 112 | "status_code": { 113 | "type": "string" 114 | } 115 | } 116 | }, 117 | "utils.ResponseSuccess": { 118 | "type": "object", 119 | "properties": { 120 | "code": { 121 | "type": "integer" 122 | }, 123 | "data": {}, 124 | "message": { 125 | "type": "string" 126 | }, 127 | "status_code": { 128 | "type": "string" 129 | } 130 | } 131 | } 132 | }, 133 | "securityDefinitions": { 134 | "BasicAuth": { 135 | "type": "basic" 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: / 2 | definitions: 3 | requests.UserLoginRequest: 4 | properties: 5 | password: 6 | type: string 7 | username: 8 | type: string 9 | type: object 10 | responses.UserLoginResponse: 11 | properties: 12 | access_token: 13 | type: string 14 | id: 15 | type: string 16 | refresh_token: 17 | type: string 18 | type: object 19 | utils.ResponseFailed: 20 | properties: 21 | code: 22 | type: integer 23 | message: 24 | type: string 25 | status_code: 26 | type: string 27 | type: object 28 | utils.ResponseSuccess: 29 | properties: 30 | code: 31 | type: integer 32 | data: {} 33 | message: 34 | type: string 35 | status_code: 36 | type: string 37 | type: object 38 | info: 39 | contact: 40 | email: hello@goldenowl.asia 41 | name: goldenowl.asia 42 | description: This API is for GoldenOwl API application 43 | license: 44 | name: Apache 2.0 45 | url: http://www.apache.org/licenses/LICENSE-2.0.html 46 | title: GoldenOwl Gin API 47 | version: 1.0.0 48 | paths: 49 | /api/v1/user/login: 50 | post: 51 | consumes: 52 | - application/json 53 | description: login by username & password 54 | parameters: 55 | - description: body params 56 | in: body 57 | name: json 58 | required: true 59 | schema: 60 | $ref: '#/definitions/requests.UserLoginRequest' 61 | produces: 62 | - application/json 63 | responses: 64 | "200": 65 | description: OK 66 | schema: 67 | allOf: 68 | - $ref: '#/definitions/utils.ResponseSuccess' 69 | - properties: 70 | Data: 71 | $ref: '#/definitions/responses.UserLoginResponse' 72 | type: object 73 | "401": 74 | description: Unauthorized 75 | schema: 76 | $ref: '#/definitions/utils.ResponseFailed' 77 | "422": 78 | description: Unprocessable Entity 79 | schema: 80 | $ref: '#/definitions/utils.ResponseFailed' 81 | summary: Login user to system 82 | tags: 83 | - users 84 | securityDefinitions: 85 | BasicAuth: 86 | type: basic 87 | swagger: "2.0" 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoldenOwlAsia/golang-api-template 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/getsentry/sentry-go v0.20.0 7 | github.com/gin-gonic/gin v1.8.2 8 | github.com/go-playground/validator/v10 v10.11.1 9 | github.com/golang-jwt/jwt/v5 v5.0.0 10 | github.com/google/wire v0.5.0 11 | github.com/spf13/cast v1.5.0 12 | github.com/spf13/viper v1.15.0 13 | github.com/stretchr/testify v1.8.2 14 | github.com/swaggo/files v1.0.0 15 | github.com/swaggo/gin-swagger v1.5.3 16 | github.com/swaggo/swag v1.8.11 17 | golang.org/x/crypto v0.7.0 18 | gorm.io/driver/postgres v1.4.7 19 | gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 20 | ) 21 | 22 | require ( 23 | github.com/KyleBanks/depth v1.2.1 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/fsnotify/fsnotify v1.6.0 // indirect 26 | github.com/gin-contrib/sse v0.1.0 // indirect 27 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 28 | github.com/go-openapi/jsonreference v0.20.2 // indirect 29 | github.com/go-openapi/spec v0.20.8 // indirect 30 | github.com/go-openapi/swag v0.22.3 // indirect 31 | github.com/go-playground/locales v0.14.0 // indirect 32 | github.com/go-playground/universal-translator v0.18.0 // indirect 33 | github.com/goccy/go-json v0.10.0 // indirect 34 | github.com/hashicorp/hcl v1.0.0 // indirect 35 | github.com/jackc/pgpassfile v1.0.0 // indirect 36 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 37 | github.com/jackc/pgx/v5 v5.2.0 // indirect 38 | github.com/jinzhu/inflection v1.0.0 // indirect 39 | github.com/jinzhu/now v1.1.5 // indirect 40 | github.com/josharian/intern v1.0.0 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/leodido/go-urn v1.2.1 // indirect 43 | github.com/magiconair/properties v1.8.7 // indirect 44 | github.com/mailru/easyjson v0.7.7 // indirect 45 | github.com/mattn/go-isatty v0.0.17 // indirect 46 | github.com/mattn/go-sqlite3 v1.14.15 // indirect 47 | github.com/mitchellh/mapstructure v1.5.0 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.2 // indirect 50 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 51 | github.com/pmezard/go-difflib v1.0.0 // indirect 52 | github.com/spf13/afero v1.9.3 // indirect 53 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 54 | github.com/spf13/pflag v1.0.5 // indirect 55 | github.com/subosito/gotenv v1.4.2 // indirect 56 | github.com/ugorji/go/codec v1.2.7 // indirect 57 | golang.org/x/net v0.8.0 // indirect 58 | golang.org/x/sys v0.6.0 // indirect 59 | golang.org/x/text v0.8.0 // indirect 60 | golang.org/x/tools v0.7.0 // indirect 61 | google.golang.org/protobuf v1.30.0 // indirect 62 | gopkg.in/ini.v1 v1.67.0 // indirect 63 | gopkg.in/yaml.v2 v2.4.0 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | gorm.io/driver/sqlite v1.5.0 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 42 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 43 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 44 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 45 | github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= 46 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 47 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 48 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 49 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 50 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 51 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 52 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 53 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 54 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 55 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 56 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 58 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 59 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 60 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 61 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 62 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 63 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 64 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 65 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 66 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 67 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 68 | github.com/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhjHmQaQ= 69 | github.com/getsentry/sentry-go v0.20.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= 70 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 71 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 72 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 73 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 74 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 75 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 76 | github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= 77 | github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= 78 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 79 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 80 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 81 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 82 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 83 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 84 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 85 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 86 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 87 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 88 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 89 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 90 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 91 | github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= 92 | github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 93 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 94 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 95 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 96 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 97 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 98 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 99 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 100 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 101 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 102 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 103 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 104 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= 105 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 106 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 107 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 108 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 109 | github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= 110 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 111 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 112 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 113 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 114 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 115 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 116 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 117 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 118 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 119 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 120 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 121 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 122 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 123 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 124 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 125 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 126 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 127 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 128 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 129 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 130 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 131 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 132 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 133 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 134 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 135 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 136 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 137 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 138 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 139 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 140 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 141 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 142 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 144 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 145 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 146 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 147 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 148 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 149 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 150 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 151 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 152 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 153 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 154 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 155 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 156 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 157 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 158 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 159 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 160 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 161 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 162 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 163 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 164 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 165 | github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 166 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 167 | github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= 168 | github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= 169 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 170 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 171 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 172 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 173 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 174 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 175 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 176 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 177 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 178 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 179 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 180 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 181 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 182 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 183 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 184 | github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8= 185 | github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= 186 | github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= 187 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 188 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 189 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 190 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 191 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 192 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 193 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 194 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 195 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 196 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 197 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 198 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 199 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 200 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 201 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 202 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 203 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 204 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 205 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 206 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 207 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 208 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 209 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 210 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 211 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 212 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 213 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 214 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 215 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 216 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 217 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 218 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 219 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 220 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 221 | github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= 222 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 223 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 224 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 225 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 226 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 227 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 228 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 229 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 230 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 231 | github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= 232 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 233 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 234 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 235 | github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 236 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 237 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 238 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 239 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 240 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 241 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 242 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 243 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 244 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 245 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 246 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 247 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 248 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 249 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 250 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 251 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 252 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 253 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 254 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 255 | github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= 256 | github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= 257 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 258 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 259 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 260 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 261 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 262 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 263 | github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= 264 | github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= 265 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 266 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 267 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 268 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 269 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 270 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 271 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 272 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 273 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 274 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 275 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 276 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 277 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 278 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 279 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 280 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 281 | github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= 282 | github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= 283 | github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= 284 | github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= 285 | github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= 286 | github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= 287 | github.com/swaggo/swag v1.8.11 h1:Fp1dNNtDvbCf+8kvehZbHQnlF6AxHGjmw6H/xAMrZfY= 288 | github.com/swaggo/swag v1.8.11/go.mod h1:2GXgpNI9iy5OdsYWu8zXfRAGnOAPxYxTWTyM0XOTYZQ= 289 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 290 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 291 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 292 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 293 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 294 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 295 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 296 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 297 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 298 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 299 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 300 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 301 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 302 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 303 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 304 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 305 | go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 306 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 307 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 308 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 309 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 310 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 311 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 312 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 313 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 314 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 315 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 316 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 317 | golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= 318 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 319 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 320 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 321 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 322 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 323 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 324 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 325 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 326 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 327 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 328 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 329 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 330 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 331 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 332 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 333 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 334 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 335 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 336 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 337 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 338 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 339 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 340 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 341 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 342 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 343 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 344 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 345 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 346 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 347 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 348 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 349 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 350 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 351 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 352 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 353 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 354 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 355 | golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= 356 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 357 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 358 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 359 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 360 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 361 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 362 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 363 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 364 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 365 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 366 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 367 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 368 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 369 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 370 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 373 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 374 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 375 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 376 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 377 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 378 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 379 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 380 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 381 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 382 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 383 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 384 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 385 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 386 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 387 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 388 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 389 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 390 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 391 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 392 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 393 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 394 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 395 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 396 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 397 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 398 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 399 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 400 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 401 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 402 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 403 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 404 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 405 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 406 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 407 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 408 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 409 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 410 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 411 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 412 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 413 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 416 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 417 | golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 418 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 419 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 420 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 454 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 455 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 456 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 457 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 458 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 459 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 460 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 461 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 462 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 463 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 464 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 465 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 466 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 467 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 468 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 469 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 470 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 471 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 472 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 473 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 474 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 475 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 476 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 477 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 478 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 479 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 480 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 481 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 482 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 483 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 484 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 485 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 486 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 487 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 488 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 489 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 490 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 491 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 492 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 493 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 494 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 495 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 496 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 497 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 498 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 499 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 500 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 501 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 502 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 503 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 504 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 505 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 506 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 507 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 508 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 509 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 510 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 511 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 512 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 513 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 514 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 515 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 516 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 517 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 518 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 519 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 520 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 521 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 522 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 523 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 524 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 525 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 526 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 527 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 528 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 529 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 530 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 531 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 532 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 533 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 534 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 535 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 536 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 537 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 538 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 539 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 540 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 541 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 542 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 543 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 544 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 545 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 546 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 547 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 548 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 549 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 550 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 551 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 552 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 553 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 554 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 555 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 556 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 557 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 558 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 559 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 560 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 561 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 562 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 563 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 564 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 565 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 566 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 567 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 568 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 569 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 570 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 571 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 572 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 573 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 574 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 575 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 576 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 577 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 578 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 579 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 580 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 581 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 582 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 583 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 584 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 585 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 586 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 587 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 588 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 589 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 590 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 591 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 592 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 593 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 594 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 595 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 596 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 597 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 598 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 599 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 600 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 601 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 602 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 603 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 604 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 605 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 606 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 607 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 608 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 609 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 610 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 611 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 612 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 613 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 614 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 615 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 616 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 617 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 618 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 619 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 620 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 621 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 622 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 623 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 624 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 625 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 626 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 627 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 628 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 629 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 630 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 631 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 632 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 633 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 634 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 635 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 636 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 637 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 638 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 639 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 640 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 641 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 642 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 643 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 644 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 645 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 646 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 647 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 648 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 649 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 650 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 651 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 652 | gorm.io/driver/postgres v1.4.7 h1:J06jXZCNq7Pdf7LIPn8tZn9LsWjd81BRSKveKNr0ZfA= 653 | gorm.io/driver/postgres v1.4.7/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= 654 | gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= 655 | gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= 656 | gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 657 | gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE= 658 | gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 659 | gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g= 660 | gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 661 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 662 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 663 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 664 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 665 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 666 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 667 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 668 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 669 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 670 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 671 | -------------------------------------------------------------------------------- /handlers/article_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/GoldenOwlAsia/golang-api-template/handlers/requests" 5 | "github.com/GoldenOwlAsia/golang-api-template/handlers/responses" 6 | "github.com/GoldenOwlAsia/golang-api-template/services" 7 | "net/http" 8 | 9 | "github.com/GoldenOwlAsia/golang-api-template/models" 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/cast" 12 | ) 13 | 14 | type ArticleHandler struct { 15 | Service services.ArticleService 16 | } 17 | 18 | func NewArticleHandler(service services.ArticleService) ArticleHandler { 19 | return ArticleHandler{ 20 | Service: service, 21 | } 22 | } 23 | 24 | func (h *ArticleHandler) All(c *gin.Context) { 25 | pagination := responses.Pagination(c) 26 | baseQuery := h.Service.All("User") 27 | data, paginated, err := responses.Paginate(baseQuery, &models.Article{}, &pagination) 28 | if err != nil { 29 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) 30 | return 31 | } 32 | c.JSON(http.StatusOK, responses.PaginatedResponse{Metadata: paginated, Records: data}) 33 | } 34 | 35 | func (h *ArticleHandler) Get(c *gin.Context) { 36 | id := cast.ToUint(c.Param("id")) 37 | article, err := h.Service.Get(id) 38 | if err != nil { 39 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "Article not found"}) 40 | return 41 | } 42 | c.JSON(http.StatusOK, gin.H{"data": article}) 43 | } 44 | 45 | func (h *ArticleHandler) Create(c *gin.Context) { 46 | var form requests.CreateArticleForm 47 | if err := c.ShouldBindJSON(&form); err != nil { 48 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) 49 | return 50 | } 51 | user := c.MustGet("currentUser").(models.User) 52 | article, err := h.Service.Create(form, user) 53 | if err != nil { 54 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) 55 | return 56 | } 57 | c.JSON(http.StatusOK, gin.H{"message": "Article created", "data": article}) 58 | } 59 | 60 | func (h *ArticleHandler) Update(c *gin.Context) { 61 | id := cast.ToUint(c.Param("id")) 62 | var form requests.CreateArticleForm 63 | if err := c.ShouldBindJSON(&form); err != nil { 64 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) 65 | return 66 | } 67 | article, err := h.Service.Update(id, form) 68 | if err != nil { 69 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) 70 | return 71 | } 72 | c.JSON(http.StatusOK, gin.H{"message": "Article updated", "data": article}) 73 | } 74 | 75 | func (h *ArticleHandler) Delete(c *gin.Context) { 76 | id := cast.ToUint(c.Param("id")) 77 | err := h.Service.Delete(id) 78 | if err != nil { 79 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) 80 | return 81 | } 82 | c.JSON(http.StatusOK, gin.H{"data": id}) 83 | } 84 | -------------------------------------------------------------------------------- /handlers/requests/article.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/go-playground/validator/v10" 7 | ) 8 | 9 | type ArticleForm struct{} 10 | 11 | type CreateArticleForm struct { 12 | Title string `form:"title" json:"title" binding:"required,min=3,max=100"` 13 | Content string `form:"content" json:"content" binding:"required,min=3,max=1000"` 14 | } 15 | 16 | func getMessage(field, tag string, errMsg ...string) string { 17 | switch tag { 18 | case "required": 19 | if len(errMsg) == 0 { 20 | return "Please enter the " + field 21 | } 22 | return errMsg[0] 23 | case "min", "max": 24 | return field + " should be between 3 to 1000 characters" 25 | default: 26 | return "Something went wrong, please try again later" 27 | } 28 | } 29 | 30 | func (f ArticleForm) Create(err error) string { 31 | switch err.(type) { 32 | case validator.ValidationErrors: 33 | if _, ok := err.(*json.UnmarshalTypeError); ok { 34 | return "Something went wrong, please try again later" 35 | } 36 | for _, err := range err.(validator.ValidationErrors) { 37 | switch err.Field() { 38 | case "Title": 39 | return getMessage("Title", err.Tag()) 40 | case "Content": 41 | return getMessage("Content", err.Tag()) 42 | } 43 | } 44 | default: 45 | return "Invalid request" 46 | } 47 | return "Something went wrong, please try again later" 48 | } 49 | 50 | func (f ArticleForm) Update(err error) string { 51 | switch err.(type) { 52 | case validator.ValidationErrors: 53 | if _, ok := err.(*json.UnmarshalTypeError); ok { 54 | return "Something went wrong, please try again later" 55 | } 56 | for _, err := range err.(validator.ValidationErrors) { 57 | switch err.Field() { 58 | case "Title": 59 | return getMessage("Title", err.Tag()) 60 | case "Content": 61 | return getMessage("Content", err.Tag()) 62 | } 63 | } 64 | default: 65 | return "Invalid request" 66 | } 67 | return "Something went wrong, please try again later" 68 | } 69 | -------------------------------------------------------------------------------- /handlers/requests/user.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | type UserCreateRequest struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | ConfirmPassword string `json:"confirm_password"` 7 | Email string `json:"email"` 8 | } 9 | 10 | type UserLoginRequest struct { 11 | Username string `json:"username"` 12 | Password string `json:"password"` 13 | } 14 | -------------------------------------------------------------------------------- /handlers/requests/validator.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/gin-gonic/gin/binding" 10 | "github.com/go-playground/validator/v10" 11 | ) 12 | 13 | // DefaultValidator ... 14 | type DefaultValidator struct { 15 | once sync.Once 16 | validate *validator.Validate 17 | } 18 | 19 | var _ binding.StructValidator = &DefaultValidator{} 20 | 21 | func (v *DefaultValidator) ValidateStruct(obj interface{}) error { 22 | if kindOfData(obj) == reflect.Struct { 23 | v.lazyInit() 24 | if err := v.validate.Struct(obj); err != nil { 25 | return err 26 | } 27 | } 28 | return nil 29 | } 30 | 31 | func (v *DefaultValidator) Engine() interface{} { 32 | v.lazyInit() 33 | return v.validate 34 | } 35 | 36 | func (v *DefaultValidator) lazyInit() { 37 | v.once.Do(func() { 38 | v.validate = validator.New() 39 | v.validate.SetTagName("binding") 40 | 41 | // add any custom validations etc. here 42 | 43 | // Custom rule for user full name 44 | v.validate.RegisterValidation("fullName", ValidateFullName) 45 | }) 46 | } 47 | 48 | func kindOfData(data interface{}) reflect.Kind { 49 | value := reflect.ValueOf(data) 50 | valueType := value.Kind() 51 | 52 | if valueType == reflect.Ptr { 53 | valueType = value.Elem().Kind() 54 | } 55 | return valueType 56 | } 57 | 58 | func ValidateFullName(fl validator.FieldLevel) bool { 59 | // Remove the extra space 60 | space := regexp.MustCompile(`\s+`) 61 | name := space.ReplaceAllString(fl.Field().String(), " ") 62 | 63 | // Remove trailing spaces 64 | name = strings.TrimSpace(name) 65 | 66 | // To support all possible languages 67 | matched, _ := regexp.Match(`^[^±!@£$%^&*_+§¡€#¢§¶•ªº«\\/<>?:;'"|=.,0123456789]{3,20}$`, []byte(name)) 68 | return matched 69 | } 70 | -------------------------------------------------------------------------------- /handlers/responses/pagination.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "math" 6 | "reflect" 7 | 8 | "github.com/GoldenOwlAsia/golang-api-template/configs" 9 | "github.com/gin-gonic/gin" 10 | "github.com/spf13/cast" 11 | ) 12 | 13 | type PaginatedResponse struct { 14 | Metadata PaginatedData `json:"_metadata"` 15 | Records any `json:"records"` 16 | } 17 | 18 | type PaginatedData struct { 19 | Limit int `json:"limit"` 20 | Total int `json:"total"` 21 | TotalPages int `json:"total_pages"` 22 | PerPage int `json:"per_page"` 23 | Page int `json:"page"` 24 | Sort string `json:"sort"` 25 | } 26 | 27 | func Pagination(c *gin.Context) PaginatedData { 28 | limit := configs.DefaultItemPerPage 29 | page := configs.DefaultPage 30 | sort := configs.DefaultSorting 31 | query := c.Request.URL.Query() 32 | for key, value := range query { 33 | queryValue := value[len(value)-1] 34 | switch key { 35 | case "limit": 36 | limit = cast.ToInt(queryValue) 37 | case "page": 38 | page = cast.ToInt(queryValue) 39 | case "sort": 40 | sort = queryValue 41 | } 42 | } 43 | 44 | return PaginatedData{ 45 | Limit: limit, 46 | Page: page, 47 | Sort: sort, 48 | } 49 | } 50 | 51 | func Paginate(baseQuery *gorm.DB, model interface{}, pagination *PaginatedData) (interface{}, PaginatedData, error) { 52 | offset := (pagination.Page - 1) * pagination.Limit 53 | perPage := 10 54 | 55 | // Get total count of records 56 | var count int64 57 | if err := baseQuery.Count(&count).Error; err != nil { 58 | return nil, PaginatedData{}, err 59 | } 60 | 61 | // Calculate pagination values 62 | totalPages := int64(math.Ceil(float64(count) / float64(perPage))) 63 | 64 | // Build query 65 | query := baseQuery.Limit(pagination.Limit).Offset(offset).Order(pagination.Sort) 66 | 67 | // Retrieve records 68 | records := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem())).Interface() 69 | if err := query.Find(records).Error; err != nil { 70 | return nil, PaginatedData{}, err 71 | } 72 | 73 | return records, PaginatedData{ 74 | Total: int(count), 75 | TotalPages: int(totalPages), 76 | PerPage: perPage, 77 | Page: pagination.Page, 78 | Sort: pagination.Sort, 79 | Limit: pagination.Limit, 80 | }, nil 81 | } 82 | -------------------------------------------------------------------------------- /handlers/responses/user.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import "github.com/GoldenOwlAsia/golang-api-template/models" 4 | 5 | type UserLoginResponse struct { 6 | User models.User `json:"user"` 7 | AccessToken string `json:"access_token"` 8 | RefreshToken string `json:"refresh_token"` 9 | } 10 | -------------------------------------------------------------------------------- /handlers/user_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/GoldenOwlAsia/golang-api-template/handlers/requests" 7 | jwt_token "github.com/GoldenOwlAsia/golang-api-template/pkgs/jwt-token" 8 | "github.com/GoldenOwlAsia/golang-api-template/services" 9 | "github.com/GoldenOwlAsia/golang-api-template/utils" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type UserHandler struct { 14 | service services.UserService 15 | } 16 | 17 | func NewUserHandler(s services.UserService) UserHandler { 18 | return UserHandler{ 19 | service: s, 20 | } 21 | } 22 | 23 | // Login godoc 24 | // @Summary Login user to system 25 | // @Description login by username & password 26 | // @Tags users 27 | // @Accept json 28 | // @Produce json 29 | // @Param json body requests.UserLoginRequest true "body params" 30 | // @Success 200 {object} utils.ResponseSuccess{Data=responses.UserLoginResponse} 31 | // @Failure 401 {object} utils.ResponseFailed{} 32 | // @Failure 422 {object} utils.ResponseFailed{} 33 | // @Router /api/v1/user/login [post] 34 | func (h *UserHandler) Login(c *gin.Context) { 35 | var req requests.UserLoginRequest 36 | 37 | if err := c.ShouldBindJSON(&req); err != nil { 38 | c.JSON(http.StatusUnprocessableEntity, utils.GetRespError("invalid input", nil)) 39 | return 40 | } 41 | 42 | resLogin, err := h.service.Login(req) 43 | if err != nil { 44 | c.JSON(http.StatusUnauthorized, utils.GetRespError(err.Error(), nil)) 45 | return 46 | } 47 | c.JSON(http.StatusOK, utils.GetRespSuccess("welcome back", resLogin)) 48 | } 49 | 50 | func (h *UserHandler) RefreshAccessToken(c *gin.Context) { 51 | refreshTokenString, ok := c.GetPostForm("refresh_token") 52 | if !ok { 53 | c.JSON(http.StatusBadRequest, gin.H{"error": "refresh token not found"}) 54 | return 55 | } 56 | newAccessToken, newRefreshToken, err := jwt_token.RefreshAccessToken(refreshTokenString) 57 | if err != nil { 58 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 59 | return 60 | } 61 | c.JSON(http.StatusOK, jwt_token.Token{ 62 | AccessToken: newAccessToken, 63 | RefreshToken: newRefreshToken, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /infras/di.go: -------------------------------------------------------------------------------- 1 | package infras 2 | 3 | import ( 4 | "github.com/GoldenOwlAsia/golang-api-template/handlers" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type AppHandler struct { 9 | User handlers.UserHandler 10 | Article handlers.ArticleHandler 11 | } 12 | 13 | func DI(db *gorm.DB) AppHandler { 14 | return AppHandler{ 15 | User: InitUserAPI(db), 16 | Article: InitArticleAPI(db), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /infras/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | //go:generate go run github.com/google/wire/cmd/wire@latest 5 | package infras 6 | 7 | import ( 8 | "github.com/GoldenOwlAsia/golang-api-template/handlers" 9 | "github.com/GoldenOwlAsia/golang-api-template/services" 10 | "github.com/google/wire" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func InitUserAPI(db *gorm.DB) handlers.UserHandler { 15 | wire.Build( 16 | services.NewUserService, 17 | handlers.NewUserHandler, 18 | ) 19 | 20 | return handlers.UserHandler{} 21 | } 22 | 23 | func InitArticleAPI(db *gorm.DB) handlers.ArticleHandler { 24 | wire.Build( 25 | services.NewArticleService, 26 | handlers.NewArticleHandler, 27 | ) 28 | 29 | return handlers.ArticleHandler{} 30 | } 31 | -------------------------------------------------------------------------------- /infras/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package infras 8 | 9 | import ( 10 | "github.com/GoldenOwlAsia/golang-api-template/handlers" 11 | "github.com/GoldenOwlAsia/golang-api-template/services" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | // Injectors from wire.go: 16 | 17 | func InitUserAPI(db *gorm.DB) handlers.UserHandler { 18 | userService := services.NewUserService(db) 19 | userHandler := handlers.NewUserHandler(userService) 20 | return userHandler 21 | } 22 | 23 | func InitArticleAPI(db *gorm.DB) handlers.ArticleHandler { 24 | articleService := services.NewArticleService(db) 25 | articleHandler := handlers.NewArticleHandler(articleService) 26 | return articleHandler 27 | } 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/GoldenOwlAsia/golang-api-template/configs" 6 | "github.com/GoldenOwlAsia/golang-api-template/infras" 7 | "github.com/GoldenOwlAsia/golang-api-template/middleware" 8 | "github.com/GoldenOwlAsia/golang-api-template/migrations" 9 | "github.com/GoldenOwlAsia/golang-api-template/router" 10 | "github.com/getsentry/sentry-go" 11 | sentrygin "github.com/getsentry/sentry-go/gin" 12 | "github.com/gin-gonic/gin" 13 | _ "github.com/swaggo/files" 14 | _ "github.com/swaggo/gin-swagger" 15 | "gorm.io/driver/postgres" 16 | "gorm.io/gorm" 17 | "gorm.io/gorm/logger" 18 | "log" 19 | "net/http" 20 | "os" 21 | "os/signal" 22 | ) 23 | 24 | var ( 25 | DB *gorm.DB 26 | Gin *gin.Engine 27 | ) 28 | 29 | // @title GoldenOwl Gin API 30 | // @version 1.0.0 31 | // @description This API is for GoldenOwl API application 32 | // @contact.name goldenowl.asia 33 | // @contact.email hello@goldenowl.asia 34 | // @license.name Apache 2.0 35 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 36 | // @BasePath / 37 | // @securityDefinitions.basic BasicAuth 38 | func main() { 39 | InitEnv() 40 | InitDB() 41 | InitGin() 42 | InitSentry() 43 | migrations.Migrate(DB) 44 | migrations.Seed(DB) 45 | Gin.Use(middleware.Cors()) 46 | Gin.Use(middleware.HandleResponse) 47 | Gin.Use(sentrygin.New(sentrygin.Options{Repanic: true})) 48 | appHandler := infras.DI(DB) 49 | Gin = router.InitRouter(Gin, appHandler, DB) 50 | 51 | SpinUp(" 🚀 ") 52 | } 53 | 54 | func InitEnv() { 55 | var err error 56 | _, err = configs.LoadConfig(".", ".env") 57 | if err != nil { 58 | log.Fatal("cannot load configs: ", err) 59 | } 60 | } 61 | 62 | func InitDB() { 63 | dsn := getDSN() 64 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ 65 | Logger: func() logger.Interface { 66 | if configs.ConfApp.AppMode == gin.DebugMode { 67 | return logger.Default.LogMode(logger.Info) 68 | } 69 | return logger.Default.LogMode(logger.Silent) 70 | }(), 71 | }) 72 | 73 | if err != nil { 74 | log.Println("cannot connect to database: ", err) 75 | } 76 | DB = db 77 | } 78 | 79 | func getDSN() (dsn string) { 80 | username := configs.ConfApp.DatabaseUserName 81 | password := configs.ConfApp.DatabasePassword 82 | host := configs.ConfApp.DatabaseHost 83 | port := configs.ConfApp.DatabasePort 84 | databaseName := configs.ConfApp.DatabaseName 85 | timeZone := configs.ConfApp.DatabaseTimezone 86 | dsn = fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=%s", host, username, password, databaseName, port, timeZone) 87 | 88 | return 89 | } 90 | 91 | func InitGin() { 92 | g := gin.Default() 93 | Gin = g 94 | gin.SetMode(configs.ConfApp.AppMode) 95 | } 96 | 97 | func InitSentry() { 98 | var err error 99 | if err = sentry.Init(sentry.ClientOptions{ 100 | Dsn: configs.ConfApp.SentryDNS, 101 | EnableTracing: true, 102 | Environment: configs.ConfApp.AppMode, 103 | TracesSampleRate: 1.0, 104 | }); err != nil { 105 | log.Printf("Sentry initialization failed: %v\n", err) 106 | } 107 | } 108 | 109 | func SpinUp(msg string) (server *http.Server) { 110 | // -- Graceful restart or stop server -- 111 | server = &http.Server{ 112 | ReadHeaderTimeout: configs.ReadHeaderTimeout, 113 | Addr: fmt.Sprintf(":%s", configs.ConfApp.Port), // + configs.ConfApp.Port 114 | Handler: Gin, 115 | } 116 | fmt.Printf("[GoldenOwl API - %s] Start to listening the incoming requests on http address: %s", gin.Mode(), server.Addr) 117 | fmt.Println(msg) 118 | 119 | quit := make(chan os.Signal) 120 | signal.Notify(quit, os.Interrupt) 121 | 122 | go func() { 123 | <-quit 124 | log.Println("receive interrupt signal") 125 | if err := server.Close(); err != nil { 126 | log.Fatal("Server Close:", err) 127 | } 128 | }() 129 | 130 | if err := server.ListenAndServe(); err != nil { 131 | if err == http.ErrServerClosed { 132 | log.Println("Server closed under request") 133 | } else { 134 | log.Fatal("Server closed unexpect") 135 | } 136 | } 137 | 138 | log.Println("Server exiting") 139 | 140 | return server 141 | } 142 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func Cors() gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 11 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 12 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-AccessToken, Authorization, ResponseType, accept, origin, Cache-Control, X-Requested-With") 13 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE") 14 | 15 | if c.Request.Method == "OPTIONS" { 16 | c.AbortWithStatus(http.StatusNoContent) 17 | return 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/GoldenOwlAsia/golang-api-template/configs" 10 | "github.com/GoldenOwlAsia/golang-api-template/models" 11 | jwtToken "github.com/GoldenOwlAsia/golang-api-template/pkgs/jwt-token" 12 | "github.com/GoldenOwlAsia/golang-api-template/utils" 13 | "github.com/gin-gonic/gin" 14 | "github.com/spf13/cast" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | type jwtMiddleware struct { 19 | db *gorm.DB 20 | } 21 | 22 | func NewJwtAuth(db *gorm.DB) *jwtMiddleware { 23 | return &jwtMiddleware{db: db} 24 | } 25 | 26 | func (m jwtMiddleware) Authenticate() gin.HandlerFunc { 27 | return func(c *gin.Context) { 28 | var token string 29 | 30 | token, err := extractBearerToken(c.GetHeader("Authorization")) 31 | if err != nil { 32 | c.AbortWithStatusJSON(http.StatusForbidden, utils.GetRespError(err.Error(), nil)) 33 | return 34 | } 35 | 36 | if token == "" { 37 | c.AbortWithStatusJSON(http.StatusForbidden, utils.GetRespError("you are not logged in", nil)) 38 | return 39 | } 40 | 41 | sub, err := jwtToken.ValidateAccessToken(token, configs.ConfApp.SecretKey) 42 | if err != nil { 43 | c.AbortWithStatusJSON(http.StatusForbidden, utils.GetRespError("your token is invalid", nil)) 44 | return 45 | } 46 | 47 | userID := cast.ToUint(fmt.Sprint(sub)) 48 | 49 | var user models.User 50 | err = m.db.Where(&models.User{ID: userID}).First(&user).Error 51 | if err != nil { 52 | c.AbortWithStatusJSON(http.StatusUnauthorized, utils.GetRespError("the user belonging to this token no logger exists", nil)) 53 | return 54 | } 55 | 56 | c.Set("currentUser", user) 57 | c.Next() 58 | } 59 | } 60 | 61 | func extractBearerToken(header string) (string, error) { 62 | if header == "" { 63 | return "", errors.New("bad header value given") 64 | } 65 | 66 | token := strings.Split(header, " ") 67 | if len(token) != 2 { 68 | return "", errors.New("incorrectly formatted authorization header") 69 | } 70 | 71 | return token[1], nil 72 | } 73 | -------------------------------------------------------------------------------- /middleware/response.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type bodyLogWriter struct { 12 | gin.ResponseWriter 13 | body *bytes.Buffer 14 | } 15 | 16 | func (w bodyLogWriter) Write(b []byte) (int, error) { 17 | w.body.Write(b) 18 | return w.ResponseWriter.Write(b) 19 | } 20 | 21 | func HandleResponse(c *gin.Context) { 22 | blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} 23 | c.Writer = blw 24 | c.Next() 25 | statusCode := c.Writer.Status() 26 | if statusCode >= http.StatusBadRequest { 27 | // ok this is a request with error, let's make a record for it 28 | // now print body (or log in your preferred way) 29 | fmt.Println("Response body: " + blw.body.String()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/migration.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/GoldenOwlAsia/golang-api-template/models" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | func Migrate(db *gorm.DB) (err error) { 9 | return db.AutoMigrate( 10 | &models.User{}, 11 | &models.Article{}, 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /migrations/seed.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/GoldenOwlAsia/golang-api-template/configs" 7 | "github.com/GoldenOwlAsia/golang-api-template/models" 8 | "github.com/GoldenOwlAsia/golang-api-template/utils" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func Seed(db *gorm.DB) { 13 | if configs.ConfApp.AppMode == "release" { 14 | return 15 | } 16 | seedUser(db) 17 | } 18 | 19 | func seedUser(db *gorm.DB) { 20 | // Create some sample users 21 | hashPassword, err := utils.HashPassword("1234") 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | users := []models.User{ 26 | {Username: "admin", Email: "admin@example.com", Password: hashPassword, Role: "Admin"}, 27 | } 28 | 29 | // Insert the users into the database 30 | for _, user := range users { 31 | result := db.Where("username = ?", user.Username).FirstOrCreate(&user) 32 | if result.Error != nil { 33 | log.Printf("failed to create user: %v", result.Error) 34 | } 35 | log.Printf("created user with ID: %d\n", user.ID) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /models/article.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | type Article struct { 9 | ID uint `json:"id" gorm:"primaryKey;autoIncrement:true"` 10 | CreatedAt time.Time `json:"created_at"` 11 | UpdatedAt time.Time `json:"updated_at"` 12 | DeletedAt gorm.DeletedAt `json:"-"` 13 | UserID uint `json:"-"` 14 | User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` 15 | Title string `json:"title"` 16 | Content string `json:"content"` 17 | } 18 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | type User struct { 9 | ID uint `json:"id" gorm:"primaryKey;autoIncrement:true"` 10 | CreatedAt time.Time `json:"created_at"` 11 | UpdatedAt time.Time `json:"updated_at"` 12 | DeletedAt gorm.DeletedAt `json:"-"` 13 | Username string `json:"username" gorm:"type:varchar(30);uniqueIndex:uidx_username"` 14 | Password string `json:"-" gorm:"type:varchar"` // hashing 15 | Email string `json:"email" gorm:"type:varchar(50);"` 16 | Role string `json:"role" gorm:"type:varchar;"` 17 | Status string `json:"status" gorm:"type:varchar;"` 18 | } 19 | -------------------------------------------------------------------------------- /pkgs/jwt-token/token.go: -------------------------------------------------------------------------------- 1 | package jwt_token 2 | 3 | import ( 4 | "fmt" 5 | "github.com/GoldenOwlAsia/golang-api-template/configs" 6 | "github.com/golang-jwt/jwt/v5" 7 | "time" 8 | ) 9 | 10 | type Token struct { 11 | AccessToken string `json:"access_token"` 12 | RefreshToken string `json:"refresh_token"` 13 | } 14 | 15 | // GenerateAccessToken generates a new JWT access token using the given userId. 16 | func GenerateAccessToken(userId string, key string) (string, error) { 17 | // define token claims 18 | claims := jwt.MapClaims{ 19 | "user_id": userId, 20 | "exp": time.Now().Add(configs.AccessTokenExpireTime).Unix(), // set token to expire in 15 mins 21 | } 22 | 23 | // create token with claims 24 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 25 | 26 | // sign token with secret key 27 | signedToken, err := token.SignedString([]byte(key)) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | // return signed token 33 | return signedToken, nil 34 | } 35 | 36 | func GenerateRefreshToken(userId, key string) (string, error) { 37 | // define token claims 38 | claims := jwt.MapClaims{ 39 | "user_id": userId, 40 | "exp": time.Now().Add(configs.RefreshTokenExpireTime).Unix(), // set token to expire in 24 hours 41 | } 42 | 43 | // create token with claims 44 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 45 | 46 | // sign token with secret key 47 | signedToken, err := token.SignedString([]byte(key)) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | // return signed token 53 | return signedToken, nil 54 | } 55 | 56 | func RefreshAccessToken(refreshTokenString string) (accessToken string, newRefreshToken string, err error) { 57 | // parse refresh token 58 | refreshToken, err := jwt.Parse(refreshTokenString, func(token *jwt.Token) (interface{}, error) { 59 | // check signing method 60 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 61 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 62 | } 63 | 64 | // return secret key 65 | return []byte(configs.ConfApp.SecretKey), nil 66 | }) 67 | 68 | // check for errors 69 | if err != nil { 70 | return 71 | } 72 | 73 | // get user ID from token 74 | userID, ok := refreshToken.Claims.(jwt.MapClaims)["user_id"].(string) 75 | if !ok { 76 | err = fmt.Errorf("invalid token") 77 | return 78 | } 79 | 80 | // generate new access token 81 | accessToken, err = GenerateAccessToken(userID, configs.ConfApp.SecretKey) 82 | if err != nil { 83 | return 84 | } 85 | 86 | newRefreshToken, _ = GenerateRefreshToken(userID, configs.ConfApp.SecretKey) 87 | 88 | // return access token 89 | return 90 | } 91 | 92 | func ValidateAccessToken(tokenString string, key string) (*jwt.Token, error) { 93 | // parse token 94 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 95 | // check signing method 96 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 97 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 98 | } 99 | 100 | // return secret key 101 | return []byte(key), nil 102 | }) 103 | 104 | // check for errors 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | // check token is valid 110 | if !token.Valid { 111 | return nil, fmt.Errorf("invalid token") 112 | } 113 | 114 | // return token 115 | return token, nil 116 | } 117 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/GoldenOwlAsia/golang-api-template/docs" 5 | "github.com/GoldenOwlAsia/golang-api-template/infras" 6 | "github.com/GoldenOwlAsia/golang-api-template/middleware" 7 | "github.com/gin-gonic/gin" 8 | swaggerFiles "github.com/swaggo/files" 9 | ginSwagger "github.com/swaggo/gin-swagger" 10 | "gorm.io/gorm" 11 | "net/http" 12 | ) 13 | 14 | func InitRouter(app *gin.Engine, appHandler infras.AppHandler, db *gorm.DB) *gin.Engine { 15 | jwtAuthMiddleware := middleware.NewJwtAuth(db) 16 | 17 | app.GET("/health_check", func(context *gin.Context) { 18 | context.JSON(http.StatusOK, gin.H{ 19 | "service_name": "Golden Owl Gin API", 20 | "status": "ok", 21 | "data": "📺 API up and running", 22 | }) 23 | }) 24 | 25 | users := app.Group("api/v1/user") 26 | users.POST("/login", appHandler.User.Login) 27 | users.POST("/refreshAccessToken", appHandler.User.RefreshAccessToken) 28 | 29 | articles := app.Group("api/v1/articles", jwtAuthMiddleware.Authenticate()) 30 | articles.GET("/", appHandler.Article.All) 31 | articles.GET("/:id", appHandler.Article.Get) 32 | articles.POST("/", appHandler.Article.Create) 33 | articles.PUT("/:id", appHandler.Article.Update) 34 | articles.DELETE("/:id", appHandler.Article.Delete) 35 | 36 | docs.SwaggerInfo.BasePath = "" 37 | app.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 38 | app.StaticFS("/images", http.Dir("public")) 39 | 40 | return app 41 | } 42 | -------------------------------------------------------------------------------- /services/article.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/GoldenOwlAsia/golang-api-template/handlers/requests" 5 | "github.com/GoldenOwlAsia/golang-api-template/models" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type ArticleService struct { 10 | DB *gorm.DB 11 | } 12 | 13 | func NewArticleService(db *gorm.DB) ArticleService { 14 | return ArticleService{ 15 | DB: db, 16 | } 17 | } 18 | 19 | func (s *ArticleService) All(preloads ...string) (baseQuery *gorm.DB) { 20 | baseQuery = s.DB.Model(&models.Article{}) 21 | for _, preload := range preloads { 22 | baseQuery = s.DB.Model(&models.Article{}).Preload(preload) 23 | } 24 | return 25 | } 26 | 27 | func (s *ArticleService) Create(req requests.CreateArticleForm, user models.User) (article *models.Article, err error) { 28 | article = &models.Article{ 29 | Title: req.Title, 30 | Content: req.Content, 31 | UserID: user.ID, 32 | } 33 | err = s.DB.Create(article).Error 34 | return 35 | } 36 | 37 | func (s *ArticleService) Update(id uint, req requests.CreateArticleForm) (article *models.Article, err error) { 38 | article = &models.Article{} 39 | err = s.DB.First(article, id).Error 40 | if err != nil { 41 | return 42 | } 43 | article.Title = req.Title 44 | article.Content = req.Content 45 | err = s.DB.Save(&article).Error 46 | return 47 | } 48 | 49 | func (s *ArticleService) Delete(id uint) (err error) { 50 | err = s.DB.Delete(&models.Article{}, id).Error 51 | return 52 | } 53 | 54 | func (s *ArticleService) Get(id uint) (article models.Article, err error) { 55 | err = s.DB.Where("id = ?", id).First(&article).Error 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /services/user.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "github.com/GoldenOwlAsia/golang-api-template/configs" 6 | "github.com/GoldenOwlAsia/golang-api-template/handlers/requests" 7 | "github.com/GoldenOwlAsia/golang-api-template/handlers/responses" 8 | "github.com/GoldenOwlAsia/golang-api-template/models" 9 | jwt_token "github.com/GoldenOwlAsia/golang-api-template/pkgs/jwt-token" 10 | "github.com/GoldenOwlAsia/golang-api-template/utils" 11 | "github.com/spf13/cast" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type UserService struct { 16 | DB *gorm.DB 17 | } 18 | 19 | func NewUserService(db *gorm.DB) UserService { 20 | return UserService{ 21 | DB: db, 22 | } 23 | } 24 | 25 | func (s UserService) Login(req requests.UserLoginRequest) (resp responses.UserLoginResponse, err error) { 26 | var user models.User 27 | err = s.DB.Where(&models.User{Username: req.Username}).First(&user).Error 28 | 29 | if err != nil || len(user.Username) <= 0 { 30 | err = errors.New("invalid username or password") 31 | return 32 | } 33 | var userIDString = cast.ToString(user.ID) 34 | if err = utils.VerifyPassword(user.Password, req.Password); err != nil { 35 | err = errors.New("invalid username or password") 36 | return 37 | } 38 | accessToken, _ := jwt_token.GenerateAccessToken(userIDString, configs.ConfApp.SecretKey) 39 | refreshToken, _ := jwt_token.GenerateRefreshToken(userIDString, configs.ConfApp.SecretKey) 40 | resp = responses.UserLoginResponse{ 41 | User: user, 42 | AccessToken: accessToken, 43 | RefreshToken: refreshToken, 44 | } 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /testing.env: -------------------------------------------------------------------------------- 1 | SECRET_KEY=12321421421412312 2 | APP_MODE=debug 3 | PORT=8080 4 | DOMAIN=localhost 5 | # Database 6 | DATABASE_USER_NAME=postgres 7 | DATABASE_PASSWORD=postgres 8 | DATABASE_HOST=localhost 9 | DATABASE_PORT=5432 10 | DATABASE_NAME=go_api_testing 11 | 12 | 13 | DATABASE_TIMEZONE=Europe/London 14 | 15 | # 16 | CLIENT_ORIGIN=http://localhost:3000 17 | ALLOW_ORIGIN=http://localhost:8080,http://localhost:3000,https://couria-api.onrender.com 18 | TOKEN_EXPIRED_IN=12000m 19 | TOKEN_MAX_AGE=60 20 | TOKEN_SECRET=secret 21 | # Sentry 22 | SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx 23 | # Dvla 24 | 25 | #REDIS 26 | REDIS_ENDPOINT=redis-18927.c278.us-east-1-4.ec2.cloud.redislabs.com:18927 27 | REDIS_PASSWORD=LjL0kwawpCPHYgvw9nHjzM8J6ipKIn8J 28 | 29 | MAPS_API_URL=https://couria.free.beeceptor.com/ggmapsapi 30 | MAPS_API_KEY= -------------------------------------------------------------------------------- /tests/unit_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestSomething(t *testing.T) { 9 | assert.True(t, true, "True is true!") 10 | } 11 | -------------------------------------------------------------------------------- /utils/array.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func ChunkSlice[T comparable](slice []T, chunkSize int) (chunks [][]T) { 4 | if len(slice) == 0 { 5 | return [][]T{} 6 | } 7 | for i := 0; i < len(slice); i += chunkSize { 8 | end := i + chunkSize 9 | 10 | if end > len(slice) { 11 | end = len(slice) 12 | } 13 | chunks = append(chunks, slice[i:end]) 14 | } 15 | 16 | return chunks 17 | } 18 | 19 | func Intersect(a, b []string) []string { 20 | m := make(map[string]bool) 21 | for _, x := range a { 22 | m[x] = true 23 | } 24 | var res []string 25 | for _, x := range b { 26 | if m[x] { 27 | res = append(res, x) 28 | } 29 | } 30 | return res 31 | } 32 | 33 | func Unique[T comparable](slice []T) []T { 34 | keys := make(map[T]bool) 35 | list := []T{} 36 | for _, entry := range slice { 37 | if _, value := keys[entry]; !value { 38 | keys[entry] = true 39 | list = append(list, entry) 40 | } 41 | } 42 | return list 43 | } 44 | -------------------------------------------------------------------------------- /utils/array_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestChunkSlice(t *testing.T) { 11 | t.Run("Chunk size less than slice length", func(t *testing.T) { 12 | slice := []int{1, 2, 3, 4, 5, 6, 7, 8} 13 | chunkSize := 3 14 | 15 | expected := [][]int{ 16 | {1, 2, 3}, 17 | {4, 5, 6}, 18 | {7, 8}, 19 | } 20 | 21 | result := ChunkSlice(slice, chunkSize) 22 | 23 | if !reflect.DeepEqual(expected, result) { 24 | t.Errorf("expected %v but got %v", expected, result) 25 | } 26 | }) 27 | 28 | t.Run("Chunk size greater than slice length", func(t *testing.T) { 29 | slice := []string{"apple", "banana", "cherry"} 30 | chunkSize := 5 31 | 32 | expected := [][]string{ 33 | {"apple", "banana", "cherry"}, 34 | } 35 | 36 | result := ChunkSlice(slice, chunkSize) 37 | 38 | if !reflect.DeepEqual(expected, result) { 39 | t.Errorf("expected %v but got %v", expected, result) 40 | } 41 | }) 42 | 43 | t.Run("Empty slice", func(t *testing.T) { 44 | slice := []int{} 45 | chunkSize := 5 46 | 47 | expected := [][]int{} 48 | 49 | result := ChunkSlice(slice, chunkSize) 50 | 51 | if !assert.EqualValues(t, expected, result) { 52 | t.Errorf("expected %v but got %v", expected, result) 53 | } 54 | }) 55 | } 56 | 57 | func TestIntersect(t *testing.T) { 58 | a := []string{"apple", "banana", "orange", "pear"} 59 | b := []string{"orange", "pear", "kiwi", "pineapple"} 60 | 61 | expected := []string{"orange", "pear"} 62 | actual := Intersect(a, b) 63 | 64 | if len(actual) != len(expected) { 65 | t.Errorf("Intersect(%v, %v) = %v; expected %v", a, b, actual, expected) 66 | } 67 | 68 | for i, v := range expected { 69 | if actual[i] != v { 70 | t.Errorf("Intersect(%v, %v) = %v; expected %v", a, b, actual, expected) 71 | } 72 | } 73 | } 74 | 75 | func TestUnique(t *testing.T) { 76 | testCases := []struct { 77 | input []int 78 | expected []int 79 | }{ 80 | {[]int{1, 2, 2, 3, 3, 3, 4, 4, 4, 4}, []int{1, 2, 3, 4}}, 81 | {[]int{5, 4, 3, 2, 1}, []int{5, 4, 3, 2, 1}}, 82 | {[]int{}, []int{}}, 83 | {[]int{1, 1}, []int{1}}, 84 | {[]int{1, 2, 3}, []int{1, 2, 3}}, 85 | } 86 | 87 | for _, tc := range testCases { 88 | result := Unique(tc.input) 89 | if !reflect.DeepEqual(result, tc.expected) { 90 | t.Errorf("Unique(%v) = %v; expected %v", tc.input, result, tc.expected) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /utils/bytes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | var ( 9 | suffixes [5]string 10 | ) 11 | 12 | func Round(val float64, roundOn float64, places int) (newVal float64) { 13 | var round float64 14 | pow := math.Pow(10, float64(places)) 15 | digit := pow * val 16 | _, div := math.Modf(digit) 17 | if div >= roundOn { 18 | round = math.Ceil(digit) 19 | } else { 20 | round = math.Floor(digit) 21 | } 22 | newVal = round / pow 23 | return 24 | } 25 | 26 | func HumanFileSize(size float64) string { 27 | suffixes[0] = "B" 28 | suffixes[1] = "KB" 29 | suffixes[2] = "MB" 30 | suffixes[3] = "GB" 31 | suffixes[4] = "TB" 32 | 33 | base := math.Log(size) / math.Log(1024) 34 | getSize := Round(math.Pow(1024, base-math.Floor(base)), .5, 2) 35 | getSuffix := suffixes[int(math.Floor(base))] 36 | return strconv.FormatFloat(getSize, 'f', -1, 64) + " " + getSuffix 37 | } 38 | -------------------------------------------------------------------------------- /utils/bytes_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestHumanFileSize(t *testing.T) { 6 | testCases := []struct { 7 | input float64 8 | output string 9 | }{ 10 | {100, "100 B"}, 11 | {1024, "1 KB"}, 12 | {1024 * 1024, "1 MB"}, 13 | {1024 * 1024 * 1024, "1 GB"}, 14 | {1024 * 1024 * 1024 * 1024, "1 TB"}, 15 | } 16 | 17 | for _, tc := range testCases { 18 | result := HumanFileSize(tc.input) 19 | if result != tc.output { 20 | t.Errorf("Expected %v but got %v", tc.output, result) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /utils/datetime.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/GoldenOwlAsia/golang-api-template/configs" 5 | "time" 6 | ) 7 | 8 | // ParseDate value: "2023-03-20" 9 | func ParseDate(value string) (result time.Time, err error) { 10 | result, err = time.Parse("2006-01-02", value) 11 | 12 | return 13 | } 14 | 15 | // ParseDate value: "20/03/2023" 16 | func ParseDateCSV(value string) (result time.Time, err error) { 17 | result, err = time.Parse(configs.DefaultDateCsvLayoutFormat, value) 18 | 19 | return 20 | } 21 | 22 | // ParseDateTime value: "2022-03-20T12:34:56Z" 23 | func ParseDateTime(value string) (result time.Time, err error) { 24 | result, err = time.Parse(time.RFC3339, value) 25 | 26 | return 27 | } 28 | 29 | // DatetimeToString output: "2022-03-20T12:34:56Z" 30 | func DatetimeToString(value time.Time) (result string, err error) { 31 | result = value.Format(time.RFC3339) 32 | 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /utils/datetime_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestParseDate(t *testing.T) { 9 | dateStr := "2023-03-20" 10 | expectedDate := time.Date(2023, 03, 20, 0, 0, 0, 0, time.UTC) 11 | 12 | result, err := ParseDate(dateStr) 13 | 14 | if err != nil { 15 | t.Errorf("Unexpected error while parsing date: %v", err) 16 | } 17 | 18 | if result != expectedDate { 19 | t.Errorf("Unexpected date returned. Expected %v, but got %v", expectedDate, result) 20 | } 21 | } 22 | 23 | func TestParseDateTime(t *testing.T) { 24 | // Test cases with valid input strings 25 | testCases := []struct { 26 | input string 27 | expected time.Time 28 | }{ 29 | {"2022-03-20T12:34:56Z", time.Date(2022, time.March, 20, 12, 34, 56, 0, time.UTC)}, 30 | {"2022-01-01T00:00:00-08:00", time.Date(2022, time.January, 1, 0, 0, 0, 0, time.FixedZone("", -8*60*60))}, 31 | } 32 | 33 | for _, tc := range testCases { 34 | actual, err := ParseDateTime(tc.input) 35 | if err != nil { 36 | t.Errorf("Unexpected error for input %q: %v", tc.input, err) 37 | continue 38 | } 39 | if !actual.Equal(tc.expected) { 40 | t.Errorf("Unexpected result for input %q. Expected %v, but got %v", tc.input, tc.expected, actual) 41 | } 42 | } 43 | 44 | // Test cases with invalid input strings 45 | invalidInputs := []string{"", "2022-03-20", "not a valid datetime string"} 46 | for _, input := range invalidInputs { 47 | _, err := ParseDateTime(input) 48 | if err == nil { 49 | t.Errorf("Expected an error for input %q, but got none", input) 50 | } 51 | } 52 | } 53 | 54 | func TestDatetimeToString(t *testing.T) { 55 | // create a test time 56 | testTime := time.Date(2023, time.March, 20, 10, 30, 0, 0, time.UTC) 57 | // expected result 58 | expectedResult := "2023-03-20T10:30:00Z" 59 | 60 | // call the function 61 | result, err := DatetimeToString(testTime) 62 | 63 | // check for errors 64 | if err != nil { 65 | t.Errorf("unexpected error: %s", err) 66 | } 67 | 68 | // compare the result with the expected result 69 | if result != expectedResult { 70 | t.Errorf("expected %q but got %q", expectedResult, result) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func MakeFilenameUnique(filename string) string { 12 | ext := filepath.Ext(filename) 13 | name := filename[0 : len(filename)-len(ext)] 14 | name = CleanFileName(name) 15 | timestamp := time.Now().Unix() 16 | uniqueFilename := fmt.Sprintf("%s-%d%s", name, timestamp, ext) 17 | return uniqueFilename 18 | } 19 | 20 | func CleanFileName(fileName string) string { 21 | // Remove any characters that are not letters, numbers, or spaces 22 | reg := regexp.MustCompile(`[^a-zA-Z0-9\s]+`) 23 | cleanedName := reg.ReplaceAllString(fileName, "_") 24 | 25 | // Replace any spaces with hyphens 26 | cleanedName = strings.ReplaceAll(cleanedName, " ", "-") 27 | 28 | // Convert the name to lower case 29 | cleanedName = strings.ToLower(cleanedName) 30 | 31 | return cleanedName 32 | } 33 | -------------------------------------------------------------------------------- /utils/file_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestMakeFilenameUnique(t *testing.T) { 10 | // Test MakeFilenameUnique with a sample file name 11 | fileName := "My File Name.jpeg" 12 | uniqueFileName := MakeFilenameUnique(fileName) 13 | 14 | // Check if the unique file name has the correct format 15 | expectedName := "my-file-name-" + fmt.Sprint(time.Now().Unix()) + ".jpeg" 16 | if uniqueFileName != expectedName { 17 | t.Errorf("MakeFilenameUnique(%q) = %q, want %q", fileName, uniqueFileName, expectedName) 18 | } 19 | } 20 | 21 | func TestCleanFileName(t *testing.T) { 22 | // Test CleanFileName with a sample file name 23 | fileName := "My File Name" 24 | cleanedName := CleanFileName(fileName) 25 | 26 | // Check if the cleaned file name has the correct format 27 | expectedName := "my-file-name" 28 | if cleanedName != expectedName { 29 | t.Errorf("CleanFileName(%q) = %q, want %q", fileName, cleanedName, expectedName) 30 | } 31 | 32 | // Test CleanFileName with another sample file name 33 | fileName2 := "My_File-Name_123" 34 | cleanedName2 := CleanFileName(fileName2) 35 | 36 | // Check if the cleaned file name has the correct format 37 | expectedName2 := "my_file_name_123" 38 | if cleanedName2 != expectedName2 { 39 | t.Errorf("CleanFileName(%q) = %q, want %q", fileName2, cleanedName2, expectedName2) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /utils/map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func ExtractMapKeys[T, V comparable](myMap map[T]V) (result []T) { 4 | result = make([]T, len(myMap)) 5 | i := 0 6 | for k := range myMap { 7 | result[i] = k 8 | i++ 9 | } 10 | 11 | return 12 | } 13 | 14 | func GetCsvHeaderIndex(line []string) (output map[string]int) { 15 | output = make(map[string]int) 16 | for i, v := range line { 17 | output[v] = i 18 | } 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /utils/password.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | func HashPassword(password string) (string, error) { 6 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 7 | return string(bytes), err 8 | } 9 | 10 | func VerifyPassword(hashedPassword string, password string) error { 11 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 12 | } 13 | -------------------------------------------------------------------------------- /utils/password_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHashPassword(t *testing.T) { 8 | password := "1234" 9 | 10 | hash, err := HashPassword(password) 11 | if err != nil { 12 | t.Fatalf("HashPassword returned error: %v", err) 13 | } 14 | 15 | if hash == "" { 16 | t.Error("Hashed password is empty") 17 | } 18 | } 19 | 20 | func TestVerifyPassword(t *testing.T) { 21 | password := "1234" 22 | 23 | hash, err := HashPassword(password) 24 | if err != nil { 25 | t.Fatalf("HashPassword returned error: %v", err) 26 | } 27 | 28 | err = VerifyPassword(hash, password) 29 | if err != nil { 30 | t.Errorf("VerifyPassword returned error: %v", err) 31 | } 32 | 33 | err = VerifyPassword(hash, "passwrong") 34 | if err == nil { 35 | t.Error("VerifyPassword succeeded with incorrect password") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /utils/response.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | ResponseStatusCodeSuccess = "success" 5 | ResponseStatusCodeFailed = "error" 6 | ) 7 | 8 | type ResponseSuccess struct { 9 | Code int `json:"code"` 10 | StatusCode string `json:"status_code"` 11 | Message string `json:"message"` 12 | Data interface{} `json:"data"` 13 | } 14 | 15 | type RespSuccess struct { 16 | Message string `json:"message"` 17 | Data interface{} `json:"data"` 18 | } 19 | 20 | type ResponseFailed struct { 21 | Code int `json:"code"` 22 | StatusCode string `json:"status_code"` 23 | Message string `json:"message"` 24 | } 25 | 26 | type RespFailed struct { 27 | Message string `json:"message"` 28 | } 29 | 30 | type StatusResp string 31 | 32 | const ( 33 | StatusError StatusResp = "error" 34 | StatusSuccess StatusResp = "success" 35 | ) 36 | 37 | type Response struct { 38 | Status StatusResp `json:"status"` 39 | Message string `json:"message"` 40 | Data interface{} `json:"data"` 41 | } 42 | 43 | func GetRespError(msg string, data any) *Response { 44 | resp := &Response{ 45 | Status: StatusError, 46 | Message: msg, 47 | Data: map[string]interface{}{}, 48 | } 49 | if data != nil { 50 | resp.Data = data 51 | } 52 | return resp 53 | } 54 | 55 | func GetRespSuccess(msg string, data any) *Response { 56 | return &Response{ 57 | Status: StatusSuccess, 58 | Message: msg, 59 | Data: data, 60 | } 61 | } 62 | --------------------------------------------------------------------------------