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