(Feat): Initial Commit
This commit is contained in:
758
.changes-go-starter/24.11.06.md
Normal file
758
.changes-go-starter/24.11.06.md
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
## 2024-06-11
|
||||||
|
- Update to [golang:1.22.4-bookworm](https://hub.docker.com/layers/library/golang/1.22.4-bookworm/images/sha256-5eb6d52cd78951fa53884e208a030db506ab619a13157b9b7ea3a8eb5e4cbeee?context=explore) (requires `./docker-helper.sh --rebuild`) containing the bump from Debian `bullseye`(11) to `bookworm` (12)
|
||||||
|
- Minor: [Bump github.com/golangci/golangci-lint from 1.55.2 to 1.59.0](https://github.com/golangci/golangci-lint/releases/tag/v1.59.0)
|
||||||
|
- Minor: [Bump github.com/gotestyourself/gotestsum from 1.11.0 to 1.12.0](https://github.com/gotestyourself/gotestsum/releases/tag/v1.12.0)
|
||||||
|
- Extended and fixed the password reset handling by a debounce and reuse duration. This can for example be leveraged to mitigate email flooding. The token reuse fixes an existing solution that was not working due to searching for tokens created on minute in the future instead of the past using `models.PasswordResetTokenWhere.CreatedAt.GT(time.Now().Add(time.Minute*1)),`. The new default behaviour is to debounce the password reset by 60 seconds and not to reuse reset tokens:
|
||||||
|
- `PasswordResetTokenDebounceDuration` / `SERVER_AUTH_PASSWORD_RESET_TOKEN_DEBOUNCE_DURATION_SECONDS`: if a password reset token has been created in this duration, no password reset is initialized (default: 60 seconds)
|
||||||
|
- `PasswordResetTokenReuseDuration` / `SERVER_AUTH_PASSWORD_RESET_TOKEN_REUSE_DURATION_SECONDS`: if a password reset token has been created in this duration and is still valid, it is reused instead of re-created (default-value: 0 seconds->no reuse)
|
||||||
|
- Added test helper to simplify assertion of `httperrors.HTTPError` verifying the http status code and the returned error, example usage:
|
||||||
|
```go
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", payload, nil)
|
||||||
|
response := test.RequireHTTPError(t, res, httperrors.ErrNotFoundTokenNotFound)
|
||||||
|
```
|
||||||
|
- Added helpers to get last sent emails from mock transport using `mail := test.GetLastSentMail(t, s.Mailer)` or `sentMails := test.GetSentMails(t, s.Mailer)`
|
||||||
|
- Bumped Github Actions `github/codeql-action` from v1 to v3 and set go version in `go.mod` to `1.22.4` due to https://github.com/github/codeql/issues/15647
|
||||||
|
|
||||||
|
## 2024-05-28
|
||||||
|
- Fixes the `LogErrorFuncWithRequestInfo` to return the error in order to pass the error to the global error handling mechanism
|
||||||
|
|
||||||
|
## 2024-05-14
|
||||||
|
- Switch [from Go 1.21.6 Go 1.21.10](https://go.dev/doc/devel/release#go1.21.0) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Extended the snapshot and request helper to improve the test experience when snapshotting raw JSON (`.SaveJSON`) and raw bytes like images (`.SaveBytes`)
|
||||||
|
- Improved custom replace function in snapshot helper to only redact explicit matches
|
||||||
|
- Added custom LogErrorFunc for recover middleware to attach request info the recover log messages
|
||||||
|
- Adjusted order of validation error matching to correctly return list of errors wrapped in single error
|
||||||
|
- Added vulnerability scanning to dev container (trivy and govulncheck)
|
||||||
|
|
||||||
|
## 2024-02-01
|
||||||
|
- [Persist bash history in development container](https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Your commands are now persisted between your development container restarts / rebuilds, making it easier to re-run specific commands you've previously executed (e.g. that one go command you cannot remember).
|
||||||
|
- Hotfix [types.NullDecimal error](https://github.com/volatiletech/sqlboiler/issues/1234) by downgrading indirect `github.com/ericlagergren/decimal@v0.0.0-20190420051523-6335edbaa640`.
|
||||||
|
- Note that we do not pin it in direct dependencies, as this downgrade is already in [SQLBoilers master](https://github.com/volatiletech/sqlboiler/commit/bc59c158590800f7810cce241a12d572e898014f) anyways.
|
||||||
|
|
||||||
|
## 2024-01-31
|
||||||
|
- Migration to Docker Compose V2 ([Docker Compose Docs](https://docs.docker.com/compose/reference/)), thx [@eklatzer](https://github.com/eklatzer)
|
||||||
|
- Upgrade to [IntegreSQL v1.1.0](https://github.com/allaboutapps/integresql/blob/v1.1.0/CHANGELOG.md#v110)
|
||||||
|
- Switch [from Go 1.20.3 Go 1.21.6](https://go.dev/doc/devel/release#go1.21.0) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Fix premature optimization in `make swagger` -> `make swagger-generate` (rm `rsync` with `--size-only`), thx [@eklatzer](https://github.com/eklatzer)
|
||||||
|
- Dockerfile deps upgrade:
|
||||||
|
- Upgrade pgFormatter [from v5.3 to v5.5](https://github.com/darold/pgFormatter/releases/tag/v5.5)
|
||||||
|
- Upgrade gotestsum [from 1.9.0 to 1.11.0](https://github.com/gotestyourself/gotestsum/releases/tag/v1.11.0)
|
||||||
|
- Upgrade golangci-lint [from 1.52.2 to 1.55.2](https://github.com/golangci/golangci-lint/releases/tag/v1.55.2)
|
||||||
|
- Upgrade watchexec [from 1.20.6 to 1.25.1](https://github.com/watchexec/watchexec/releases/tag/v1.25.1)
|
||||||
|
- `go.mod` upgrades
|
||||||
|
- Minor: [Bump github.com/BurntSushi/toml from v1.2.1 to v1.3.2](https://github.com/BurntSushi/toml)
|
||||||
|
- Minor: [Bump github.com/davecgh/go-spew from v1.1.1 to v1.1.2-0.20180830191138-d8f796af33cc](https://github.com/davecgh/go-spew/commit/d8f796af33cc11cb798c1aaeb27a4ebc5099927d)
|
||||||
|
- Minor: [Bump github.com/gabriel-vasile/mimetype from v1.4.1 to v1.4.3](https://github.com/gabriel-vasile/mimetype)
|
||||||
|
- Minor: [Bump github.com/go-openapi/errors from v0.20.3 to v0.21.0](https://github.com/go-openapi/errors)
|
||||||
|
- Minor: [Bump github.com/go-openapi/runtime from v0.25.0 to v0.27.1](https://github.com/go-openapi/runtime)
|
||||||
|
- Minor: [Bump github.com/go-openapi/strfmt from v0.21.3 to v0.22.0](https://github.com/go-openapi/strfmt)
|
||||||
|
- Minor: [Bump github.com/go-openapi/swag from v0.22.3 to v0.22.9](https://github.com/go-openapi/swag)
|
||||||
|
- Minor: [Bump github.com/go-openapi/validate from v0.22.0 to v0.22.6](https://github.com/go-openapi/validate)
|
||||||
|
- Minor: [Bump github.com/labstack/echo/v4 from v4.9.1 to v4.11.4](https://github.com/labstack/echo)
|
||||||
|
- Minor: [Bump github.com/lib/pq from v1.10.7 to v1.10.9](https://github.com/lib/pq)
|
||||||
|
- Minor: [Bump github.com/nicksnyder/go-i18n/v2 from v2.2.1 to v2.4.0](https://github.com/nicksnyder/go-i18n)
|
||||||
|
- Minor: [Bump github.com/pmezard/go-difflib from v1.0.0 to v1.0.1-0.20181226105442-5d4384ee4fb2](https://github.com/pmezard/go-difflib) (deprecated)
|
||||||
|
- Minor: [Bump github.com/rs/zerolog from v1.28.0 to v1.31.0](https://github.com/rs/zerolog)
|
||||||
|
- Minor: [Bump github.com/rubenv/sql-migrate from v1.2.0 to v1.6.1](https://github.com/rubenv/sql-migrate)
|
||||||
|
- Minor: [Bump github.com/spf13/cobra from v1.6.1 to v1.8.0](https://github.com/spf13/cobra)
|
||||||
|
- Minor: [Bump github.com/spf13/viper from v1.14.0 to v1.18.2](https://github.com/spf13/viper)
|
||||||
|
- Minor: [Bump github.com/stretchr/testify from v1.8.1 to v1.8.4](https://github.com/stretchr/testify)
|
||||||
|
- Minor: [Bump github.com/subosito/gotenv from v1.4.1 to v1.6.0](https://github.com/subosito/gotenv)
|
||||||
|
- Minor: [Bump github.com/volatiletech/sqlboiler/v4 from v4.13.0 to v4.16.1](https://github.com/volatiletech/sqlboiler)
|
||||||
|
- Minor: [Bump github.com/volatiletech/strmangle from v0.0.4 to v0.0.6](https://github.com/volatiletech/strmangle)
|
||||||
|
- Minor: [Bump golang.org/x/crypto from v0.3.0 to v0.18.0](https://golang.org/x/crypto)
|
||||||
|
- Minor: [Bump golang.org/x/sys from v0.5.0 to v0.16.0](https://golang.org/x/sys)
|
||||||
|
- Minor: [Bump golang.org/x/text from v0.7.0 to v0.14.0](https://golang.org/x/text)
|
||||||
|
- Minor: [Bump google.golang.org/api from v0.103.0 to v0.161.0](https://google.golang.org/api)
|
||||||
|
- Minor: [Bump xxxx from yyy to zzz](https://xxxx)
|
||||||
|
- Replace: [github.com/rogpeppe/go-internal v1.9.0](https://github.com/rogpeppe/go-internal) with [golang.org/x/mod v0.14.0](https://pkg.go.dev/golang.org/x/mod)
|
||||||
|
|
||||||
|
## 2023-05-03
|
||||||
|
- Switch [from Go 1.19.3 to Go 1.20.3](https://go.dev/doc/devel/release#go1.20) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Add new log configuration:
|
||||||
|
- optional `output` param of `LoggerWithConfig` to redirect the log output
|
||||||
|
- optional caller info switched on with `SERVER_LOGGER_LOG_CALLER`
|
||||||
|
- Minor: rename unused function parameters to fix linter errors
|
||||||
|
- Minor: update devcontainer.json syntax to remove deprecation warning
|
||||||
|
- Minor: add `GetFieldsImplementing` to utils and use it to easier add new fixture fields.
|
||||||
|
- `go.mod` changes:
|
||||||
|
- Minor: [Bump github.com/golangci/golangci-lint from 1.50.1 to 1.52.2](https://github.com/golangci/golangci-lint/releases/tag/v1.52.2)
|
||||||
|
- Minor: [Bump golang.org/x/net from 0.2.0 to 0.7.0](https://cs.opensource.google/go/x/net) (Fixing CVE-2022-41723)
|
||||||
|
|
||||||
|
## 2023-03-03
|
||||||
|
- Switch [from Go 1.17.9 to Go 1.19.3](https://go.dev/doc/devel/release#go1.19) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Major: Update base docker image from debian buster to bullseye
|
||||||
|
- Minor: [Bump github.com/darold/pgFormatter from 5.2 to 5.3](https://github.com/darold/pgFormatter/releases/tag/v5.3)
|
||||||
|
- Minor: [Bump github.com/gotestyourself/gotestsum from 1.8.0 to 1.9.0](https://github.com/gotestyourself/gotestsum/releases/tag/v1.9.0)
|
||||||
|
- Minor: [Bump github.com/golangci/golangci-lint from 1.45.2 to 1.50.1](https://github.com/golangci/golangci-lint/releases/tag/v1.50.1)
|
||||||
|
- Minor: [Bump github.com/uw-labs/lichen from 0.1.5 to 0.1.7](https://github.com/uw-labs/lichen/releases/tag/v0.1.7)
|
||||||
|
- Minor: [Bump github.com/watchexec/watchexec from 1.18.11 to 1.20.6](https://github.com/watchexec/watchexec/releases/tag/v1.20.6)
|
||||||
|
- Minor: [Bump github.com/mikefarah/yq from 4.24.2 to 4.30.5](https://github.com/mikefarah/yq/releases/tag/v4.30.5)
|
||||||
|
- Major: Upgrade distroless app image from base-debian10 to base-debian11
|
||||||
|
- Major: Dockerfile is now build to support amd64 and arm64 architecture
|
||||||
|
- Improve speed of `make swagger` when dealing with many files in `/api` by generating to a docker volume instead of the host filesystem, rsyncing only to changes into `/internal/types`. Furthermore split our swagger type generation and validation into two separate make targets, that can run concurrently (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Note that `/app/api/tmp`, `/app/tmp` and `/app/bin` are now baked by proper docker volumes when using our `docker-compose.yml`/`./docker-helper.sh --up`. You **cannot** remove these directories directly inside the container (but its contents) and you can also no longer see its files on your host machine directly!
|
||||||
|
- Fix `make check-gen-dirs` false positives hidden files.
|
||||||
|
- Allow to trace/benchmark `Makefile` targets execution by using a custom shell wrapper for make execution. See `SHELL` and `.SHELLFLAGS` within `Makefile` and the custom `rksh` script in the root working directory. Usage: `MAKE_TRACE_TIME=true make <target>`
|
||||||
|
- `go.mod` changes:
|
||||||
|
- Minor: [Bump github.com/BurntSushi/toml from 1.1.0 to 1.2.1](https://github.com/BurntSushi/toml/releases/tag/v1.2.1)
|
||||||
|
- Minor: [Bump github.com/gabriel-vasile/mimetype from 1.4.0 to 1.4.1](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.4.1)
|
||||||
|
- Minor: [Bump github.com/go-openapi/errors from 0.20.2 to 0.20.3](https://github.com/go-openapi/errors/releases/tag/v0.20.3)
|
||||||
|
- Minor: [Bump github.com/go-openapi/runtime from 0.23.3 to 0.25.0](https://github.com/go-openapi/runtime)
|
||||||
|
- Minor: [Bump github.com/go-openapi/strfmt from 0.21.2 to 0.21.3](https://github.com/go-openapi/strfmt/releases/tag/v0.21.3)
|
||||||
|
- Minor: [Bump github.com/go-openapi/swag from 0.21.1 to 0.22.3](https://github.com/go-openapi/swag/releases/tag/v0.22.3)
|
||||||
|
- Minor: [Bump github.com/go-openapi/validate from 0.21.0 to 0.22.0](https://github.com/go-openapi/validate/releases/tag/v0.22.0)
|
||||||
|
- Minor: [Bump github.com/labstack/echo/v4 from 4.7.2 to 4.9.1](https://github.com/labstack/echo/releases/tag/v4.9.1) (Fixing CVE-2022-40083)
|
||||||
|
- Minor: [Bump github.com/lib/pq from 1.10.5 to 1.10.7](https://github.com/lib/pq/releases/tag/v1.10.7)
|
||||||
|
- Minor: [Bump github.com/nicksnyder/go-i18n/v2 from 2.2.0 to 2.2.1](https://github.com/nicksnyder/go-i18n/releases/tag/v2.2.1)
|
||||||
|
- Minor: [Bump github.com/rogpeppe/go-internal from 1.8.1 to 1.9.0](https://github.com/rogpeppe/go-internal/releases/tag/v1.9.0)
|
||||||
|
- Minor: [Bump github.com/rs/zerolog from 1.26.1 to 1.28.0](https://github.com/rs/zerolog/releases/tag/v1.28.0)
|
||||||
|
- Minor: [Bump github.com/rubenv/sql-migrate from 1.1.1 to 1.2.0](https://github.com/rubenv/sql-migrate/releases/tag/v1.2.0)
|
||||||
|
- Minor: [Bump github.com/spf13/cobra from 1.4.0 to 1.6.1](https://github.com/spf13/cobra/releases/tag/v1.6.1)
|
||||||
|
- Minor: [Bump github.com/spf13/viper from 1.10.1 to 1.14.0](https://github.com/spf13/viper/releases/tag/v1.14.0)
|
||||||
|
- Minor: [Bump github.com/stretchr/testify from 1.7.1 to 1.8.1](https://github.com/stretchr/testify/releases/tag/v1.8.1)
|
||||||
|
- Minor: [Bump github.com/subosito/gotenv from 1.2.0 to 1.4.1](https://github.com/subosito/gotenv/releases/tag/v1.4.1)
|
||||||
|
- Minor: [Bump github.com/volatiletech/sqlboiler/v4 from 4.9.2 to v4.13.0](https://github.com/volatiletech/sqlboiler/blob/master/CHANGELOG.md#v4130---2022-08-28)
|
||||||
|
- Minor: [Bump github.com/volatiletech/strmangle from 0.0.2 to 0.0.4](https://github.com/volatiletech/strmangle/releases/tag/v0.0.4) (changes in enum generation might require manual changes, minor changes)
|
||||||
|
- Minor: [Bump golang.org/x/crypto from v0.0.0-20220411220226-7b82a4e95df4 to 0.3.0](https://cs.opensource.google/go/x/crypto)
|
||||||
|
- Minor: [Bump golang.org/x/sys from v0.0.0-20220412211240-33da011f77ad to 0.2.0](https://cs.opensource.google/go/x/sys)
|
||||||
|
- Minor: [Bump golang.org/x/text from 0.3.7 to 0.4.0](https://cs.opensource.google/go/x/text) (Fixing CVE-2022-32149)
|
||||||
|
- Minor: [Bump google.golang.org/api from 0.74.0 to 0.103.0](https://github.com/googleapis/google-api-go-client/compare/v0.80.0...v0.103.0)
|
||||||
|
|
||||||
|
## 2022-09-13
|
||||||
|
- Hotfix: Previously there was a chance of recursive error wrapping within our [`internal/api/router/error_handler.go`](https://github.com/allaboutapps/go-starter/blob/master/internal/api/router/error_handler.go) in combination with `*echo.HTTPError`. We currently disable this wrapping (as not used anyways) and will schedule a cleaner update regarding this error augmentation approach.
|
||||||
|
|
||||||
|
## 2022-04-15
|
||||||
|
- Switch [from Go 1.17.1 to Go 1.17.9](https://go.dev/doc/devel/release#go1.17.minor) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- **BREAKING** Add [`tenv`](https://github.com/sivchari/tenv) and [`errorlint`](https://github.com/polyfloyd/go-errorlint) linter to our default `.golangci.yml` configuration.
|
||||||
|
- We switch from `os.Setenv` to [`t.Setenv`](https://pkg.go.dev/testing#T.Setenv) within our own test code.
|
||||||
|
- **NOTE**: If you have used `os.Setenv` within your `*_test.go` code previously, simply replace those calls by `t.Setenv`.
|
||||||
|
- **NOTE**: The go-starter base code now properly uses `errors.Is` and `errors.As` for comparisons (and `%w` wrapping where really needed). For a good overview regarding error handling see [Effective Error Handling in Golang](https://earthly.dev/blog/golang-errors/). For example, if you receive linting errors, you'll need to change your code like this:
|
||||||
|
- Wrong: `if err == sql.ErrNoRows {`
|
||||||
|
- Valid: `if errors.Is(err, sql.ErrNoRows) {`
|
||||||
|
- Wrong: `if err != sql.ErrConnDone {`
|
||||||
|
- Valid: `if !errors.Is(err, sql.ErrConnDone) {`
|
||||||
|
- Wrong: `gErr := err.(*googleapi.Error)`, Valid:
|
||||||
|
- `var gErr *googleapi.Error`
|
||||||
|
- `ok := errors.As(err, &gErr)`
|
||||||
|
- `Dockerfile` development stage changes (requires `./docker-helper.sh --rebuild`):
|
||||||
|
- Bump [golang](https://hub.docker.com/_/golang) base image from `golang:1.17.1-buster` to **`golang:1.17.8-buster`**.
|
||||||
|
- Bump [pgFormatter](https://github.com/darold/pgFormatter) from v5.0 to [v5.2](https://github.com/darold/pgFormatter/releases/tag/v5.2)
|
||||||
|
- Bump [golangci-lint](https://github.com/golangci/golangci-lint) from v1.42.1 to [v1.45.2](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md#v1452)
|
||||||
|
- Bump [lichen](https://github.com/uw-labs/lichen) from v0.1.4 to [v0.1.5](https://github.com/uw-labs/lichen/compare/v0.1.4...v0.1.5)
|
||||||
|
- Bump [watchexec](https://github.com/watchexec/watchexec) from v1.17.0 to [v1.18.11](https://github.com/watchexec/watchexec/releases/tag/cli-v1.18.11) (+ switch from gnu to musl)
|
||||||
|
- Bump [yq](https://github.com/mikefarah/yq) from v4.16.2 to [v4.24.2](https://github.com/mikefarah/yq/releases/tag/v4.24.2)
|
||||||
|
- Bump [gotestsum](https://github.com/gotestyourself/gotestsum) from v1.7.0 to [v1.8.0](https://github.com/gotestyourself/gotestsum/releases/tag/v1.8.0)
|
||||||
|
- Adds [tmux](https://github.com/tmux/tmux) (debian apt managed)
|
||||||
|
- `go.mod` changes:
|
||||||
|
- Major: [Bump `github.com/rubenv/sql-migrate` from v0.0.0-20210614095031-55d5740dbbcc to v1.1.1](https://github.com/rubenv/sql-migrate/compare/55d5740dbbccbaa4934009263b37ba52d837241f...v1.1.1) (though this should not lead to any major changes)
|
||||||
|
- Minor: [Bump github.com/volatiletech/sqlboiler/v4 from 4.6.0 to v4.9.2](https://github.com/volatiletech/sqlboiler/blob/v4.9.2/CHANGELOG.md#v492---2022-04-11) (your generated model might slightly change, minor changes).
|
||||||
|
- Note that v5 will prefer wrapping errors (e.g. `sql.ErrNoRows`) to retain the stack trace, thus it's about time for us to start to enforce proper `errors.Is` checks in our codebase (see above).
|
||||||
|
- Minor: [#178: Bump github.com/labstack/echo/v4 from 4.6.1 to 4.7.2](https://github.com/allaboutapps/go-starter/pull/178) (support for HEAD method query params binding, minor changes).
|
||||||
|
- Minor: [#160: Bump github.com/rs/zerolog from 1.25.0 to 1.26.1](https://github.com/allaboutapps/go-starter/pull/160) (minor changes).
|
||||||
|
- Minor: [#179: Bump github.com/nicksnyder/go-i18n/v2 from 2.1.2 to 2.2.0](https://github.com/allaboutapps/go-starter/pull/179) (minor changes).
|
||||||
|
- Minor: [Bump `github.com/gabriel-vasile/mimetype` from v1.3.1 to v1.4.0](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.4.0)
|
||||||
|
- Minor: [Bump `github.com/go-openapi/runtime` from v0.22.0 to v0.23.3](https://github.com/go-openapi/runtime/compare/v0.22.0...v0.23.3)
|
||||||
|
- Patch: [Bump `github.com/go-openapi/strfmt` from v0.21.1 to v0.21.2](https://github.com/go-openapi/strfmt/compare/v0.21.1...v0.21.2)
|
||||||
|
- Patch: [Bump `github.com/go-openapi/validate` from v0.20.3 to v0.21.0](https://github.com/go-openapi/validate/compare/v0.20.3...v0.21.0)
|
||||||
|
- Patch: [Bump `github.com/lib/pq` from v1.10.3 to v1.10.5](https://github.com/lib/pq/compare/v1.10.3...v1.10.5)
|
||||||
|
- Patch: [Bump `github.com/rogpeppe/go-internal` from v1.8.0 to v1.8.1](https://github.com/rogpeppe/go-internal/releases/tag/v1.8.1)
|
||||||
|
- Patch: [Bump `github.com/stretchr/testify` from v1.7.0 to v1.7.1](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1)
|
||||||
|
- Patch: [Bump `github.com/volatiletech/strmangle` from v0.0.1 to v0.0.2](https://github.com/volatiletech/strmangle/compare/v0.0.1...v0.0.2)
|
||||||
|
- Minor: [Bump `google.golang.org/api` from v0.63.0 to v0.74.0](https://github.com/googleapis/google-api-go-client/compare/v0.63.0...v0.74.0)
|
||||||
|
- Minor: [Bump `github.com/BurntSushi/toml` from v1.0.0 to v1.1.0](https://github.com/BurntSushi/toml/releases/tag/v1.1.0)
|
||||||
|
- Bump `golang.org/x/crypto` from v0.0.0-20211215165025-cf75a172585e to v0.0.0-20220411220226-7b82a4e95df4
|
||||||
|
- Bump `golang.org/x/sys` from v0.0.0-20211210111614-af8b64212486 to v0.0.0-20220412211240-33da011f77ad
|
||||||
|
- We now support overriding `ENV` variables during **local** development through a `.env.local` dotenv file.
|
||||||
|
- This does not require a development container restart.
|
||||||
|
- We override the env within the app process through `config.DefaultServiceConfigFromEnv()`, so this does not mess with the actual container ENV.
|
||||||
|
- See `.env.local.sample` for further instructions to use this.
|
||||||
|
- Note that `.env.local` is **NEVER automatically** applied during **test runs**. If you really need that, use the specialized `test.DotEnvLoadLocalOrSkipTest` helper before loading up your server within that very test! This ensures that this test is automatically skipped if the `.env.local` file is no longer available.
|
||||||
|
- VSCode windows closes now explicitly stop Docker containers via [`shutdownAction: "stopCompose"`](https://code.visualstudio.com/docs/remote/devcontainerjson-reference) within `.devcontainer.json`.
|
||||||
|
- Use `./docker-helper --halt` or other `docker` or `docker-compose` management commands to do this explicitly instead.
|
||||||
|
- Drone CI specific (minor): Fix multiline ENV variables were messing up our `.hostenv` for `docker run` command testing of the final image.
|
||||||
|
|
||||||
|
## 2022-03-28
|
||||||
|
|
||||||
|
- Merged [#165: Allow use of db.join* methods more than once](https://github.com/allaboutapps/go-starter/pull/165), thx [danut007ro](https://github.com/danut007ro).
|
||||||
|
- Merged [#169: Switch to standalone cobra-cli dependency](https://github.com/allaboutapps/go-starter/pull/169), thx [liggitt](https://github.com/liggitt) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- [`github.com/spf13/cobra@v1.4.0`](https://github.com/spf13/cobra/releases/tag/v1.4.0) split into `cobra` (the lib) and [`github.com/spf13/cobra-cli`](https://github.com/spf13/cobra-cli/releases) (the generator / scaffolding tool)
|
||||||
|
- We'll now depend on `cobra-cli` directly in our `Dockerfile`, while the core `cobra` dependency stays unchanged within our `go.mod`.
|
||||||
|
- Bumps [`github.com/spf13/cobra`](https://github.com/spf13/cobra) from v1.3.0 to [v1.4.0](https://github.com/spf13/cobra/releases/tag/v1.4.0)
|
||||||
|
- Fixed `test.ApplyMigrations` when combined with the import SQL dump mechanics in the testing context.
|
||||||
|
- Previously, we did still use the default [sql-migrate](https://github.com/rubenv/sql-migrate) `gorp_migrations` table to track applied migrations in our test databases, not our typical `migrations` table used everywhere else.
|
||||||
|
- This especially lead to problems when importing (production / live) SQL dumps via `test.WithTestDatabaseFromDump*`, `test.WithTestServerFromDump*` or `test.WithTestServerConfigurableFromDump` as our implementation tried to apply **all migrations** every time, regardless if a partial migration set was already applied previously (as the already applied migrations were not tracked within the `migrations` table (but within `gorp_migrations`) we did not notice).
|
||||||
|
- We now initialize this pipeline correctly in the test context (similar to our usage within `cmd/db_migrate.go` or `app db migrate`) and explicitly set these globals through `config.DatabaseMigrationTable` and `config.DatabaseMigrationFolder`.
|
||||||
|
- If you encounter problems after the upgrade, please execute `make sql-drop-all` in your local environment to reset the IntegreSQL test databases, then run `make sql-reset && make sql-spec-reset && make sql-spec-migrate && make all` to rebuild and test.
|
||||||
|
|
||||||
|
|
||||||
|
## 2022-02-28
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **BREAKING** Username format change in auth handlers
|
||||||
|
- Added the `util.ToUsernameFormat` helper function, which will **lowercase** and **trim whitespaces**. We use it to format usernames in the login, register, and forgot-password handlers.
|
||||||
|
- This prevents user duplication (e.g. two accounts registered with the same email address with different casing) and
|
||||||
|
- cases where users would inadvertently register with specific casing or a trailing whitespace after their username, and subsequently struggle to log into their account.
|
||||||
|
- **This effectively locks existing users whose username contains uppercase characters and/or whitespaces out of their accounts.**
|
||||||
|
- Before rolling out this change, check whether any existing users are affected and migrate their usernames to a format that is compatible with this change.
|
||||||
|
- Be aware that this may cause conflicts in regard to the uniqueness constraint of usernames and therefore need to be resolved manually, which is why we are not including a database migration to automatically migrate existing usernames to the new format.
|
||||||
|
- For more information and a possible manual database migration flow please see this special WIKI page: https://github.com/allaboutapps/go-starter/wiki/2022-02-28
|
||||||
|
|
||||||
|
## 2022-02-03
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed order of make targets in the `make swagger` pipeline. `make swagger-lint-ref-siblings` will now run after `make swagger-concat`, always linting the current version of our swagger file. This helps avoid errors regarding an invalid `swagger.yml` when resolving merge conflicts as those are often resolved by running `make swagger` and generating a fresh `swagger.yml`.
|
||||||
|
|
||||||
|
## 2022-02-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrades to [go-swagger](https://github.com/go-swagger/go-swagger) from to v0.26.1 to [v0.29.0](https://github.com/go-swagger/go-swagger/releases/tag/v0.29.0) (development stage only, requires `./docker-helper.sh --rebuild`). Includes the following `go.mod` upgrades:
|
||||||
|
- [github.com/go-openapi/runtime](https://github.com/go-openapi/runtime) from v0.19.31 to v0.22.0
|
||||||
|
- [github.com/go-openapi/strfmt](https://github.com/go-openapi/strfmt) from v0.20.2 to v0.21.1
|
||||||
|
- [github.com/go-openapi/validate](https://github.com/go-openapi/validate) from v0.20.2 to v0.20.3
|
||||||
|
- [github.com/go-openapi/errors](https://github.com/go-openapi/errors) from v0.20.1 to v0.20.2
|
||||||
|
- [github.com/go-openapi/swag](https://github.com/go-openapi/swag) from v0.19.15 to v0.21.1
|
||||||
|
- Adds `yq` ([yq: a lightweight and portable command-line YAML processor](https://github.com/mikefarah/yq)) to our `Dockerfile` (development stage only, requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Adds `make swagger-lint-ref-siblings` which is now executed as part of the `make build` (and `make swagger`) pipeline.
|
||||||
|
- Any sibling elements of a Swagger `$ref` are ignored.
|
||||||
|
- We have seen several misuses of `$ref` in our projects causing weird merge/flatten behaviors, thus we now lint for this case explicitly.
|
||||||
|
- Having `$ref` and sibling elements (e.g. `required`, `example`, ...) is unsupported by [OpenAPI v2: $ref and Sibling Elements](https://swagger.io/docs/specification/using-ref/) itself and the [JSON Reference specification](https://datatracker.ietf.org/doc/html/rfc3986) itself.
|
||||||
|
- To mitigate these errors, either expand the referenced element (fully remove `$ref`) or create a new element including your custom siblings elements and `$ref` this new one.
|
||||||
|
- Fix schema visualization generation guide in `docs/schemacrawler/README.md`
|
||||||
|
|
||||||
|
## 2021-12-14
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Add i18n service wrapping `go-i18n` package by nicksnyder.
|
||||||
|
- Allows parsing of Accept-Language header and language string.
|
||||||
|
- Support for templating using go templating language in message values.
|
||||||
|
- Support for [CLDR plural keys](https://cldr.unicode.org/index/cldr-spec/plural-rules)
|
||||||
|
- Added environment variables to configure i18n service
|
||||||
|
- `SERVER_I18N_DEFAULT_LANGUAGE` - set default language for i18n service
|
||||||
|
- `SERVER_I18N_BUNDLE_DIR_ABS` - set directory of i81n messages, available languages are automatically configured by the files present in the folder
|
||||||
|
|
||||||
|
## 2021-11-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The `integresql` service previously bound its port (`5000`) to the host machine. As this conflicts with newer macOS releases and is not necessary for the development workflow, the port is now only exposed to the linked services.
|
||||||
|
|
||||||
|
## 2021-10-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fixes minor `Makefile` typos.
|
||||||
|
- New go-starter releases are now git tagged (starting from the previous release `go-starter-2021-10-19` onwards). See [FAQ: What's the process of a new go-starter release?](https://github.com/allaboutapps/go-starter/wiki/FAQ#whats-the-process-of-a-new-go-starter-release)
|
||||||
|
- You may now specify a **specific** tag/branch/commit from the upstream [go-starter](https://github.com/allaboutapps/go-starter) project while running `make git-fetch-go-starter`, `make git-compare-go-starter` and `make git-merge-go-starter`. This will especially come in handy if you want to do a multi-phased merge (for projects that haven't been updated in a long time):
|
||||||
|
- Merge with the latest: `make git-merge-go-starter`
|
||||||
|
- Merge with a specific tag, e.g. the tag [`go-starter-2021-10-19`](https://github.com/allaboutapps/go-starter/releases/tag/go-starter-2021-10-19): `GIT_GO_STARTER_TARGET=go-starter-2021-10-19 make git-merge-go-starter`
|
||||||
|
- Merge with a specific branch, e.g. the branch [`mr/housekeeping`](https://github.com/allaboutapps/go-starter/tree/mr/housekeeping): `GIT_GO_STARTER_TARGET=go-starter/mr/housekeeping make git-merge-go-starter` (heads up! it's `go-starter/<branchname>`)
|
||||||
|
- Merge with a specific commit, e.g. the commit [`e85bedb94c3562602bc23d2bfd09fca3b13d1e02`](https://github.com/allaboutapps/go-starter/commit/e85bedb94c3562602bc23d2bfd09fca3b13d1e02): `GIT_GO_STARTER_TARGET=e85bedb94c3562602bc23d2bfd09fca3b13d1e02 make git-merge-go-starter`
|
||||||
|
- The primary GitHub Action pipeline `.github/workflows/build-test.yml` has been synced to include most validation tasks from our internal `.drone.yml` pipeline. Furthermore:
|
||||||
|
- Avoid `Build & Test` GitHub Action running twice (on `push` and on `pull_request`).
|
||||||
|
- Add trivy scan to our base Build & Test pipeline (as we know also build and test the `app` target docker image).
|
||||||
|
- Our GitHub Action pipeline will no longer attempt to cache the previously built Docker images by other pipelines, as extracting/restoring from cache (docker buildx) typically takes **longer** than fully rebuilding the whole image. We will reinvestigate caching mechanisms in the future if GitHub Actions provides a speedier and official integration for Docker images.
|
||||||
|
|
||||||
|
## 2021-10-19
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **BREAKING** Upgrades to [Go 1.17.1](https://golang.org/doc/go1.17) `golang:1.17.1-buster`
|
||||||
|
- Switch to `//go:build <tag>` from `// +build <tag>`.
|
||||||
|
- Migrates `go.mod` via `go mod tidy -go=1.17` (pruned module graphs).
|
||||||
|
- Do the following to upgrade:
|
||||||
|
1. `make git-merge-go-starter`
|
||||||
|
2. `./docker-helper --rebuild`
|
||||||
|
3. Manually remove the new **second** `require` block (with all the `// indirect` modules) within your `go.mod`
|
||||||
|
4. Execute `go mod tidy -go=1.17` once so the **second** `require` block appears again.
|
||||||
|
5. Find `// +build <tag>` and replace it with `//go:build <tag>`.
|
||||||
|
6. `make all`.
|
||||||
|
7. Recheck your `go.mod` that the newly added `// indirect` transitive dependencies are the proper version as you were previously using (e.g. via the output from `make get-licenses` and `make get-embedded-modules`). Feel free to move any `// indirect` tagged dependencies in your **first** `require` block to the **second** block. This is where they should live.
|
||||||
|
- **BREAKING** You now need to take special care when it comes to parsing **semicolons** (`;`) in **query strings** via `net/url` and `net/http` from Go >1.17!
|
||||||
|
- Anything before the semicolon will now be stripped. e.g. `example?a=1;b=2&c=3` would have returned `map[a:[1] b:[2] c:[3]]`, while now it returns `map[c:[3]]`
|
||||||
|
- See [Go 1.17 URL query parsing](https://golang.org/doc/go1.17#semicolons).
|
||||||
|
- You may need to manually migrate your handlers/tests regarding this new default handling.
|
||||||
|
|
||||||
|
## 2021-09-27
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added `make test-update-golden` for easily refreshing **all** golden files / snapshot tests (`y + ENTER` confirmation).
|
||||||
|
- Upgrades [golangci-lint](https://github.com/golangci/golangci-lint) from `v1.41.1` to [`v1.42.1`](https://github.com/golangci/golangci-lint/releases/tag/v1.42.1) (for reference [`v1.42.0`](https://github.com/golangci/golangci-lint/releases/tag/v1.42.0)).
|
||||||
|
- Bump github.com/go-openapi/strfmt from [0.20.1 to 0.20.2](https://github.com/go-openapi/strfmt/compare/v0.20.1...v0.20.2)
|
||||||
|
- Bump github.com/go-openapi/errors from [0.20.0 to 0.20.1](https://github.com/go-openapi/errors/compare/v0.20.0...v0.20.1)
|
||||||
|
- Bump github.com/go-openapi/runtime from [0.19.29 to 0.19.31](https://github.com/go-openapi/runtime/compare/v0.19.29...v0.19.31)
|
||||||
|
- Bump github.com/rs/zerolog from [1.23.0 to 1.25.0](https://github.com/rs/zerolog/compare/v1.23.0...v1.25.0)
|
||||||
|
- Bump google.golang.org/api from [0.52.0 to 0.57.0](https://github.com/allaboutapps/go-starter/pull/124)
|
||||||
|
- Bump github.com/lib/pq from [v1.10.2 to v1.10.3](https://github.com/lib/pq/releases/tag/v1.10.3)
|
||||||
|
- Bump github.com/spf13/viper from [1.8.1 to v1.9.0](https://github.com/spf13/viper/releases/tag/v1.9.0)
|
||||||
|
- Bump github.com/labstack/echo from [4.5.0 to v4.6.1](https://github.com/labstack/echo/compare/v4.5.0...v4.6.1)
|
||||||
|
- Update golang.org/x/crypto and golang.org/x/sys
|
||||||
|
|
||||||
|
## 2021-08-17
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Hotfix**: We will pin the `Dockerfile` development and builder stage to `golang:1.16.7-buster` (+ `-buster`) for now, as currently the [new debian bullseye release within the go official docker images](https://github.com/docker-library/golang/commit/48a7371ed6055a97a10adb0b75756192ad5f1c97) breaks some tooling. The upgrade to debian bullseye and Go 1.17 will happen ~simultaneously~ **separately** within go-starter in the following weeks.
|
||||||
|
|
||||||
|
## 2021-08-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- remove ioutil (https://golang.org/doc/go1.16#ioutil)
|
||||||
|
|
||||||
|
## 2021-08-06
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump golang from 1.16.6 to [1.16.7](https://github.com/golang/go/issues?q=milestone%3AGo1.16.7+label%3ACherryPickApproved) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Adds `util.GetEnvAsStringArrTrimmed` and minor `util` test coverage upgrades.
|
||||||
|
|
||||||
|
## 2021-08-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `README.md` badges for go-starter.
|
||||||
|
- Fix some misspellings of English words within `internal/test/*.go` comments.
|
||||||
|
- Upgrades
|
||||||
|
- Bump `github.com/labstack/echo/v4` from 4.4.0 to [4.5.0](https://github.com/labstack/echo/blob/master/CHANGELOG.md#v450---2021-08-01):
|
||||||
|
- Switch from `github.com/dgrijalva/jwt-go` to [`github.com/golang-jwt/jwt`](https://github.com/golang-jwt/jwt) to mitigate [CVE-2020-26160](https://nvd.nist.gov/vuln/detail/CVE-2020-26160).
|
||||||
|
- Note that it might take some time until the former dep fully leaves our dependency graph, as it is also a transitive dependency of various versions of [`github.com/spf13/viper`](https://github.com/spf13/viper/issues/997).
|
||||||
|
- However, even though this functionality was never used by go-starter, this change fixes an important part: The original `github.com/dgrijalva/jwt-go` is no longer included in the **final `app` binary**, it is fully replaced by `github.com/golang-jwt/jwt`.
|
||||||
|
- Our `.trivyignore` still excludes [CVE-2020-26160](https://nvd.nist.gov/vuln/detail/CVE-2020-26160) as trivy cannot skip checking transitive dependencies.
|
||||||
|
- **Breaking**: If you have actually directly depended upon `github.com/dgrijalva/jwt-go`, please switch to `github.com/golang-jwt/jwt` via the following command: `find -type f -name "*.go" -exec sed -i "s/dgrijalva\/jwt-go/golang-jwt\/jwt/g" {} \;`
|
||||||
|
|
||||||
|
## 2021-07-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrades:
|
||||||
|
- Bump golang from 1.16.5 to [1.16.6](https://groups.google.com/g/golang-announce/c/n9FxMelZGAQ)
|
||||||
|
- Bump github.com/labstack/echo/v4 from 4.3.0 to [4.4.0](https://github.com/labstack/echo/blob/master/CHANGELOG.md) (adds `binder.BindHeaders` support, not affecting our goswagger `runtime.Validatable` bind helpers)
|
||||||
|
- Bump github.com/gabriel-vasile/mimetype from 1.3.0 to [1.3.1](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.3.1)
|
||||||
|
- Bump github.com/spf13/cobra from 1.1.3 to [1.2.1](https://github.com/spf13/cobra/releases/tag/v1.2.1) (and see all the big completion upgrades in [1.2.0](https://github.com/spf13/cobra/releases/tag/v1.2.0))
|
||||||
|
- Bump google.golang.org/api from 0.49.0 to [0.52.0](https://github.com/allaboutapps/go-starter/pull/106)
|
||||||
|
- Bump gotestsum to [1.7.0](https://github.com/gotestyourself/gotestsum/releases/tag/v1.7.0) (adds handy keybindings while you are in `make watch-tests` mode, see [While in watch mode, pressing some keys will perform an action](https://github.com/gotestyourself/gotestsum#run-tests-when-a-file-is-saved))
|
||||||
|
- Bump watchexec to [1.17.0](https://github.com/watchexec/watchexec/releases/tag/cli-v1.17.0)
|
||||||
|
- Bump golang.org/x/crypto to `v0.0.0-20210711020723-a769d52b0f97`
|
||||||
|
|
||||||
|
## 2021-07-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fixed `Makefile` has disregarded `pipefail`s in executed targets (e.g. `make sql-spec-migrate` previously returned exit code `0` even if there were migration errors as its output was piped internally). We now set `-cEeuo pipefail` for make's shell args, preventing these issues.
|
||||||
|
|
||||||
|
## 2021-06-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **BREAKING** Switched from [`golint`](https://github.com/golang/lint) to [`revive`](https://github.com/mgechev/revive)
|
||||||
|
- [`golint` is deprecated](https://github.com/golang/go/issues/38968).
|
||||||
|
- [`revive`](https://github.com/mgechev/revive) is considered to be a drop-in replacement for `golint`, however this change still might lead to breaking changes in your codebase.
|
||||||
|
- **BREAKING** `make lint` no longer uses `--fast` when calling `golangci-lint`
|
||||||
|
- Up until now, `make lint` also ran `golangci-lint` using the `--fast` flag to remain consistent with the linting performed by VSCode automatically.
|
||||||
|
- As running only fast linters in both steps meant skipping quite a few validations (only 4/13 enabled linters are actually active), a decision has been made to break consistency between the two lint-steps and perform "full" linting during the build pipeline.
|
||||||
|
- This change could potentially bring up additional warnings and thus fail your build until fixed.
|
||||||
|
- **BREAKING** `gosec` is now also applied to test packages
|
||||||
|
- All linters are now applied to every source code file in this project, removing the previous exclusion of `gosec` from test files/packages
|
||||||
|
- As `gosec` might (incorrectly) detect some hardcoded credentials in your tests (variable names such as `passwordResetLink` get flagged), this change might require some fixes after merging.
|
||||||
|
- Extended auth middleware to allow for multiple auth token sources
|
||||||
|
- Default token validator uses access token table, maintaining previous behavior without any changes required.
|
||||||
|
- Token validator can be changed to e.g. use separate API keys for specific endpoints, allowing for more flexibility if so desired.
|
||||||
|
- Changed `util.LogFromContext` to always return a valid logger
|
||||||
|
- Helper no longer returns a disabled logger if context provided did not have an associated logger set (e.g. by middleware). If you still need to disable the logger for a certain context/function, use `util.DisableLogger(ctx, true)` to force-disable it.
|
||||||
|
- Added request ID to context in logger middleware.
|
||||||
|
- Extended DB query helpers
|
||||||
|
- Fixed TSQuery escaping, should now properly handle all type of user input.
|
||||||
|
- Implemented helper for JSONB queries (see `ExampleWhereJSON` for implementation details).
|
||||||
|
- Added `LeftOuterJoin` helper, similar to already existing `LeftJoin` variants.
|
||||||
|
- Managed transactions (via `WithTransaction`) can now have their options configured via `WithConfiguredTransaction`.
|
||||||
|
- Added util to combine query mods with `OR` expression.
|
||||||
|
- Implemented middleware for parsing `Cache-Control` header
|
||||||
|
- Allows for cache handling in relevant services, parsed directive is stored in request context.
|
||||||
|
- New middleware is enabled by default, can be disabled via env var (`SERVER_ECHO_ENABLE_CACHE_CONTROL_MIDDLEWARE`).
|
||||||
|
- Added extra misc. helpers
|
||||||
|
- Extra helpers for slice handling and generating random strings from a given character set have been included (`util.ContainsAllString`, `util.UniqueString`, `util.GenerateRandomString`).
|
||||||
|
- Added util to check whether current execution runs inside a test environment (`util.RunningInTest`).
|
||||||
|
- Test and snapshot util improvements
|
||||||
|
- Added `snapshoter.SaveU` as a shorthand for updating a single test
|
||||||
|
- Implemented `GenericArrayPayload` with respective request helpers for array payloads in tests
|
||||||
|
- Added VScode launch task for updating all snapshots in a single test file
|
||||||
|
|
||||||
|
## 2021-06-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- We now directly bake the `gsdev` cli "bridge" (it actually just runs `go run -tags scripts /app/scripts/main.go "$@"`) into the `development` stage of our `Dockerfile` and create it at `/usr/bin/gsdev` (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- `gsdev` was previously symlinked to `/app/bin` from `/app/scripts/gsdev` (within the projects' workspace) and `chmod +x` via the `Makefile` during `init`.
|
||||||
|
- However this lead to problems with WSL2 VSCode related development setups (always dirty git workspaces as WSL2 tries to prevent `+x` flags).
|
||||||
|
- **BREAKING** encountered at **2021-06-30**: Upgrading your project via `make git-merge-go-starter` if you already have installed our previous `gsdev` approach from **2021-06-22** may require additional steps:
|
||||||
|
- It might be necessary to unlink the current `gsdev` symlink residing at `/app/bin/gsdev` before merging up (as this symlinked file will no longer exist)!
|
||||||
|
- Do this by issuing `rm -f /app/bin/gsdev` which will remove the symlink which pointed to the previous (now gone bash script) at `/app/scripts/gsdev`.
|
||||||
|
- It might also be handy to install the newer variant directly into your container (without requiring a image rebuild). Do this by:
|
||||||
|
- `sudo su` to become root in the container,
|
||||||
|
- issuing the following command: `printf '#!/bin/bash\nset -Eeo pipefail\ncd /app && go run -tags scripts ./scripts/main.go "$@"' > /usr/bin/gsdev && chmod 755 /usr/bin/gsdev` (in sync with what we do in our `Dockerfile`) and
|
||||||
|
- `[CTRL + c]` to return to being the `development` user within your container.
|
||||||
|
|
||||||
|
## 2021-06-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Introduces GitHub Actions docker layer caching via docker buildx. For details see `.github/workflows/build-test.yml`.
|
||||||
|
- Upgrades:
|
||||||
|
- Bump golang from 1.16.4 to [1.16.5](https://groups.google.com/g/golang-announce/c/RgCMkAEQjSI/m/r_EP-NlKBgAJ)
|
||||||
|
- golangci-lint@[v1.41.1](https://github.com/golangci/golangci-lint/releases/tag/v1.41.1)
|
||||||
|
- Bump github.com/rs/zerolog from 1.22.0 to [1.23.0](https://github.com/allaboutapps/go-starter/pull/92)
|
||||||
|
- Bump github.com/go-openapi/runtime from 0.19.28 to 0.19.29
|
||||||
|
- Bump github.com/volatiletech/sqlboiler/v4 from 4.5.0 to [4.6.0](https://github.com/volatiletech/sqlboiler/blob/HEAD/CHANGELOG.md#v460---2021-06-06)
|
||||||
|
- Bump github.com/rubenv/sql-migrate v0.0.0-20210408115534-a32ed26c37ea to v0.0.0-20210614095031-55d5740dbbcc
|
||||||
|
- Bump github.com/spf13/viper v1.7.1 to v1.8.0
|
||||||
|
- Bump golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a to v0.0.0-20210616213533-5ff15b29337e
|
||||||
|
- Bump golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea to v0.0.0-20210616094352-59db8d763f22
|
||||||
|
- Bump google.golang.org/api v0.47.0 to v0.49.0
|
||||||
|
- Fixes linting within `/scripts/**/*.go`, now activated by default.
|
||||||
|
|
||||||
|
## 2021-06-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Development scripts are no longer called via `go run [script]` but via `gsdev`:
|
||||||
|
- The `gsdev` cli is our new entrypoint for development workflow specific scripts, these scripts are not available in the final `app` binary.
|
||||||
|
- All previous `go run` scripts have been moved to their respective `/scripts/cmd` cli entrypoint + internal implementation within `/scripts/internal/**`.
|
||||||
|
- Please use `gsdev --help` to get an overview of available development specific commands.
|
||||||
|
- `gsdev` relys on a tiny helper bash script `scripts/gsdev` which gets symlinked to `/app/bin` on `make init`.
|
||||||
|
- Use `make test-scripts` to run tests regarding these internal scripts within `/scripts/**/*_test.go`.
|
||||||
|
- We now enforce that all `/scripts/**/*.go` files set the `// +build scripts` build tag. We do this to ensure these files are not directly depended upon from the actual `app` source-code within `/internal`.
|
||||||
|
- VSCode's `.devcontainer/devcontainer.json` now defines that the go tooling must use the `scripts` build tag for its IntelliSense. This is neccessary to still get proper code-completion when modifying resources at `/scripts/**/*.go`. You may need to reattach VSCode and/or run `./docker-helper.sh --rebuild`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Scaffolding tool to quickly generate generic CRUD endpoint stubs. Usage: `gsdev scaffold [resource name] [flags]`, also see `gsdev scaffold --help`.
|
||||||
|
|
||||||
|
## 2021-05-26
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Scans for [CVE-2020-26160](https://nvd.nist.gov/vuln/detail/CVE-2020-26160) also match for our final `app` binary, however, we do not use `github.com/dgrijalva/jwt-go` as part of our auth logic. This dependency is mostly here because of child dependencies, that yet need to upgrade to `>=v4.0.0`. Therefore, we currently disable this CVE for scans in this project (via `.trivyignore`).
|
||||||
|
- Upgrades `Dockerfile`: [`watchexec@v1.16.1`](https://github.com/watchexec/watchexec/releases/tag/cli-v1.16.1), [`lichen@v0.1.4`](https://github.com/uw-labs/lichen/releases/tag/v0.1.4) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
|
||||||
|
## 2021-05-18
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `Dockerfile` to `golang:1.16.4`, `gotestsum@v1.6.4`, `golangci-lint@v1.40.1`, `watchexec@v1.16.0` (requires `./docker-helper.sh --rebuild`).
|
||||||
|
- Upgraded `go.mod`:
|
||||||
|
- [github.com/labstack/echo/v4@v4.3.0](https://github.com/labstack/echo/releases/tag/v4.3.0)
|
||||||
|
- [github.com/lib/pq@v1.10.2](https://github.com/lib/pq/releases/tag/v1.10.2)
|
||||||
|
- [github.com/gabriel-vasile/mimetype@v1.3.0](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.3.0)
|
||||||
|
- `github.com/go-openapi/runtime@v0.19.28`
|
||||||
|
- [github.com/rs/zerolog@v1.22.0](https://github.com/rs/zerolog/releases/tag/v1.22.0)
|
||||||
|
- `github.com/rubenv/sql-migrate@v0.0.0-20210408115534-a32ed26c37ea`
|
||||||
|
- `golang.org/x/crypto@v0.0.0-20210513164829-c07d793c2f9a`
|
||||||
|
- `golang.org/x/sys@v0.0.0-20210514084401-e8d321eab015`
|
||||||
|
- [google.golang.org/api@v0.46.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.46.0)
|
||||||
|
- GitHub Actions:
|
||||||
|
- Pin to `actions/checkout@v2.3.4`.
|
||||||
|
- Remove unnecessary `git checkout HEAD^2` in CodeQL step (Code Scanning recommends analyzing the merge commit for best results).
|
||||||
|
- Limit trivy and codeQL actions to `push` against `master` and `pull_request` against `master` to overcome read-only access workflow errors.
|
||||||
|
|
||||||
|
## 2021-04-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Adds `test.WithTestDatabaseFromDump*`, `test.WithTestServerFromDump` methods for writing tests based on a database dump file that needs to be imported first:
|
||||||
|
- We dynamically setup IntegreSQL pools for all combinations passed through a `test.DatabaseDumpConfig{}` object:
|
||||||
|
- `DumpFile string` is required, absolute path to dump file
|
||||||
|
- `ApplyMigrations bool` optional, default `false`, automigrate after installing the dump
|
||||||
|
- `ApplyTestFixtures bool` optional, default `false`, import fixtures after (migrating) installing the dump
|
||||||
|
- `test.ApplyDump(ctx context.Context, t *testing.T, db *sql.DB, dumpFile string) error` may be used to apply a dump to an existing database connection.
|
||||||
|
- As we have dedicated IntegreSQL pools for each combination, testing performance should be on par with the default IntegreSQL database pool.
|
||||||
|
- Adds `test.WithTestDatabaseEmpty*` methods for writing tests based on an empty database (also a dedicated IntegreSQL pool).
|
||||||
|
- Adds context aware `test.WithTest*Context` methods reusing the provided `context.Context` (first arg).
|
||||||
|
- Adds `make sql-dump` command to easily create a dump of the local `development` database to `/app/dumps/development_YYYY-MM-DD-hh-mm-ss.sql` (.gitignored).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `test.ApplyMigrations(t *testing.T, db *sql.DB) (countMigrations int, err error)` is now public (e.g. for usage with `test.WithTestDatabaseEmpty*` or `test.WithTestDatabaseFromDump*`)
|
||||||
|
- `test.ApplyTestFixtures(ctx context.Context, t *testing.T, db *sql.DB) (countFixtures int, err error)` is now public (e.g. for usage with `test.WithTestDatabaseEmpty*` or `test.WithTestDatabaseFromDump*`)
|
||||||
|
- `internal/test/test_database_test.go` and `/app/internal/test/test_server_test.go` were massively refactored to allow for better extensibility later on (non breaking, all method signatures are backward-compatible).
|
||||||
|
|
||||||
|
## 2021-04-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Adds echo `NoCache` middleware: Use `middleware.NoCache()` and `middleware.NoCacheWithConfig(Skipper)` to explicitly force browsers to never cache calls to these handlers/groups.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `/swagger.yml` and `/-/*` now explicity set no-cache headers by default, forcing browsers to re-execute calls each and every time.
|
||||||
|
- Upgrade [watchexec@v1.15.0](https://github.com/watchexec/watchexec/releases/tag/1.15.0) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
|
||||||
|
## 2021-04-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Live-Reload for our swagger-ui is now available out of the box:
|
||||||
|
- [allaboutapps/browser-sync](https://hub.docker.com/r/allaboutapps/browser-sync) acts as proxy at [localhost:8081](http://localhost:8081/).
|
||||||
|
- Requires `./docker-helper.sh --up`.
|
||||||
|
- Best used in combination with `make watch-swagger` (still refreshes `make all` or `make swagger` of course).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrades to [swaggerapi/swagger-ui:v3.46.0](https://github.com/swagger-api/swagger-ui/tree/v3.46.0) from [swaggerapi/swagger-ui:v3.28.0](https://github.com/swagger-api/swagger-ui/compare/v3.28.0...v3.46.0)
|
||||||
|
- Upgrades to [github.com/labstack/echo@v4.2.2](https://github.com/labstack/echo/releases/tag/v4.2.2)
|
||||||
|
- `golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2`
|
||||||
|
- Upgrades to [google.golang.org/api@v0.44.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.44.0)
|
||||||
|
|
||||||
|
## 2021-04-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved `/api/main.yml` to `/api/config/main.yml` to overcome path resolve issues (`../definitions`) with the VSCode [42crunch.vscode-openapi](https://github.com/42Crunch/vscode-openapi) extension (auto-included in our devContainer) and our go-swagger concat behaviour.
|
||||||
|
- Updated [api/README.md](https://github.com/allaboutapps/go-starter/blob/master/api/README.md) information about `/api/swagger.yml` generation logic and changed `make swagger-concat` accordingly
|
||||||
|
|
||||||
|
## 2021-04-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump [golang from v1.16.2 to v1.16.3](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
|
||||||
|
## 2021-04-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump golang.org/x/crypto@v0.0.0-20210322153248-0c34fe9e7dc2
|
||||||
|
- Bump golang.org/x/sys@v0.0.0-20210331175145-43e1dd70ce54
|
||||||
|
- Bump [github.com/go-openapi/swag@v0.19.15](https://github.com/allaboutapps/go-starter/pull/71)
|
||||||
|
- Bump [github.com/go-openapi/strfmt@v0.20.1](https://github.com/allaboutapps/go-starter/pull/70)
|
||||||
|
|
||||||
|
## 2021-03-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump [github.com/gotestyourself/gotestsum@v1.6.3](https://github.com/gotestyourself/gotestsum/releases/tag/v1.6.3) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
|
||||||
|
## 2021-03-26
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump [golangci-lint@v1.39.0](https://github.com/golangci/golangci-lint/releases/tag/v1.39.0) (requires `./docker-helper.sh --rebuild`).
|
||||||
|
|
||||||
|
## 2021-03-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump github.com/rs/zerolog from [1.20.0 to 1.21.0](https://github.com/allaboutapps/go-starter/pull/69)
|
||||||
|
- Bump google.golang.org/api from [0.42.0 to 0.43.0](https://github.com/allaboutapps/go-starter/pull/68)
|
||||||
|
|
||||||
|
## 2021-03-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- We no longer do explicit calls to `t.Parallel()` in our go-starter tests (except autogenerated code). For the reasons why see [FAQ: Should I use `t.Parallel()` in my tests?](https://github.com/allaboutapps/go-starter/wiki/FAQ#should-i-use-tparallel-in-my-tests).
|
||||||
|
- Switched to [github.com/uw-labs/lichen](https://github.com/uw-labs/lichen) for getting license information of embedded dependencies in our final `./bin/app` binary.
|
||||||
|
- The following make targets are no longer flagged as `(opt)` and thus move into the main `make help` target (use `make help-all` to see all targets):
|
||||||
|
- `make lint`: Runs golangci-lint and make check-\*.
|
||||||
|
- `make go-test-print-slowest`: Print slowest running tests (must be done after running tests).
|
||||||
|
- `make get-licenses`: Prints licenses of embedded modules in the compiled bin/app.
|
||||||
|
- `make get-embedded-modules`: Prints embedded modules in the compiled bin/app.
|
||||||
|
- `make clean`: Cleans ./tmp and ./api/tmp folder.
|
||||||
|
- `make get-module-name`: Prints current go module-name (pipeable).
|
||||||
|
- `make check-gen-dirs` now ignores `.DS_Store` within `/internal/models/**/*` and `/internal/types/**/*` and echo an errors detailing what happened.
|
||||||
|
- Upgrade to [`github.com/go-openapi/runtime@v0.19.27`](https://github.com/go-openapi/runtime/compare/v0.19.26...v0.19.27)
|
||||||
|
|
||||||
|
## 2021-03-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `make all` no longer executes `make info` as part of its targets chain.
|
||||||
|
- It's very common to use `make all` multiple times per day during development and thats fine! However, the output of `make info` is typically ignored by our engineers (if they explicitly want this information, they use `make info`). So `make all` was just too spammy in it's previous form.
|
||||||
|
- `make info` does network calls and typically takes around 5sec to execute. This slowdown is not acceptable when running `make all`, especially if the information it provides isn't used anyways.
|
||||||
|
- Thus: Just trigger `make info` manually if you need the information of the `[spec DB]` structure, current `[handlers]` and `[go.mod]` information. Furthermore you may also visit `tmp/.info-db`, `tmp/.info-handlers` and `tmp/.info-go` after triggering `make info` as we store this information there after a run.
|
||||||
|
|
||||||
|
## 2021-03-15
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrades `go.mod`:
|
||||||
|
- [`github.com/volatiletech/sqlboiler/v4@v4.5.0`](https://github.com/volatiletech/sqlboiler/blob/master/CHANGELOG.md#v450---2021-03-14)
|
||||||
|
- [`github.com/rogpeppe/go-internal@v1.8.0`](https://github.com/rogpeppe/go-internal/releases/tag/v1.8.0)
|
||||||
|
- `golang.org/x/crypto@v0.0.0-20210314154223-e6e6c4f2bb5b`
|
||||||
|
- ~`golang.org/x/sys@v0.0.0-20210314195730-07df6a141424`~
|
||||||
|
- `golang.org/x/sys@v0.0.0-20210315160823-c6e025ad8005`
|
||||||
|
- [`google.golang.org/api@v0.42.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.42.0)
|
||||||
|
- `make help` no longer reports `(opt)` flagged targets, use `make help-all` instead.
|
||||||
|
- `make tools` now executes `go install {}` in parallel
|
||||||
|
- `make info` now fetches information in parallel
|
||||||
|
- Seeding: Switch to `db|dbUtil.WithTransaction` instead of manually managing the db transaction. _Note_: We will enforce using `WithTransaction` instead of manually managing the life-cycle of db transactions through a custom linter in an upcoming change. It's way safer and manually managing db transactions only makes sense in very very special cases (where you will be able to opt-out via linter excludes). Also see [What's `WithTransaction`, shouldn't I use `db.BeginTx`, `db.Commit`, and `db.Rollback`?](https://github.com/allaboutapps/go-starter/wiki/FAQ#whats-withtransaction-shouldnt-i-use-dbbegintx-dbcommit-and-dbrollback).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The correct implementation of `(util|scripts).GetProjectRootDir() string` now gets automatically selected based on the `scripts` build tag.
|
||||||
|
- We currently have 2 different `GetProjectRootDir()` implementations and each one is useful on its own:
|
||||||
|
- `util.GetProjectRootDir()` gets used while `app` or `go test` runs and resolves in the following way: use `PROJECT_ROOT_DIR` (if set), else default to the resolved path to the executable unless we can't resolve that, then **panic**!
|
||||||
|
- `scripts.GetProjectRootDir()` gets used while **generation time** (`make go-generate`) and resolves in the following way: use `PROJECT_ROOT_DIR` (if set), otherwise default to `/app` (baked, as we can assume we are in the `development` container).
|
||||||
|
- `/internal/util/(get_project_root_dir.go|get_project_root_dir_scripts.go)` is now introduced to automatically switch to the proper implementation based on the `// +build !scripts` or `// +build scripts` build tag, thus it's now consistent to import `util.GetProjectRootDir()`, especially while handler generation time (`make go-generate`).
|
||||||
|
|
||||||
|
## 2021-03-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrades to `golang@v1.16.2` (use `./docker-helper.sh --rebuild`).
|
||||||
|
- Silence resolve of `GO_MODULE_NAME` if `go` was not found in path (typically host env related).
|
||||||
|
|
||||||
|
## 2021-03-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `make build` (`make go-build`) now sets `internal/config.ModuleName`, `internal/config.Commit` and `internal/config.BuildDate` via `-ldflags`.
|
||||||
|
- `/-/version` (mgmt key auth) endpoint is now available, prints the same as `app -v`.
|
||||||
|
- `app -v` is now available and prints out buildDate and commit. Sample:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
app -v
|
||||||
|
allaboutapps.dev/aw/go-starter @ 19c4cdd0da151df432cd5ab33c35c8987b594cac (2021-03-11T15:42:27+00:00)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrades to `golang@v1.16.1` (use `./docker-helper.sh --rebuild`).
|
||||||
|
- Updates `google.golang.org/api@v0.41.0`, `github.com/gabriel-vasile/mimetype@v1.2.0` ([new supported formats](https://github.com/gabriel-vasile/mimetype/tree/v1.2.0)), `golang.org/x/sys`
|
||||||
|
- Removed `**/.git` from `.dockerignore` (`builder` stage) as we want the local git repo available while running `make go-build`.
|
||||||
|
- `app --help` now prominently includes the module name of the project.
|
||||||
|
- Prominently recommend `make force-module-name` after running `make git-merge-go-starter` to fix all import paths.
|
||||||
|
|
||||||
|
## 2021-03-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduces `CHANGELOG.md`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `make git-merge-go-starter` now uses `--allow-unrelated-histories` by default.
|
||||||
|
- `README.md` and FAQ now mention that it's recommended to execute `make git-merge-go-starter` during project setup (especially for single commit generated from template project project setups).
|
||||||
|
- See [FAQ: I want to compare or update my project/fork to the latest go-starter master.](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-want-to-compare-or-update-my-projectfork-to-the-latest-go-starter-master)
|
||||||
|
- Various typos in `README.md` and `Makefile`.
|
||||||
|
- Upgrade to [`golangci-lint@v1.38.0`](https://github.com/golangci/golangci-lint/releases/tag/v1.38.0)
|
||||||
|
|
||||||
|
## 2021-03-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `allaboutapps/nullable` is now included by default. See [#58](https://github.com/allaboutapps/go-starter/pull/58), [FAQ: I need an optional Swagger payload property that is nullable!](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-need-an-optional-swagger-payload-property-that-is-nullable)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrade to [`labstack/echo@v4.2.1`](https://github.com/labstack/echo/releases/tag/v4.2.1), [`lib/pq@v1.10.0`](https://github.com/lib/pq/releases/tag/v1.10.0)
|
||||||
|
|
||||||
|
## 2021-02-23
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- `util.BindAndValidate` is now marked as deprecated as [`labstack/echo@v4.2.0`](https://github.com/labstack/echo/releases/tag/v4.2.0) exposes a more granular binding through its `DefaultBinder`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The more specialized variants `util.BindAndValidatePathAndQueryParams` and `util.BindAndValidateBody` are now available. See [`/internal/util/http.go`](https://github.com/allaboutapps/go-starter/blob/master/internal/util/http.go#L87).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `golang@v1.16.0`
|
||||||
|
- [`labstack/echo@v4.2.0`](https://github.com/labstack/echo/releases/tag/v4.2.0)
|
||||||
|
|
||||||
|
## 2021-02-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgrades to [`pgFormatter@v5.0.0`](https://github.com/darold/pgFormatter/releases) + forces VSCode to use that version within the devcontainer through it's extension.
|
||||||
|
|
||||||
|
## 2021-02-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `golang@v1.15.8`, `go-swagger@v0.26.1`
|
||||||
|
|
||||||
|
## 2021-02-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
```
|
||||||
|
- Dockerfile updates:
|
||||||
|
- golang@1.15.7
|
||||||
|
- apt add icu-devtools (VSCode live sharing)
|
||||||
|
- gotestsum@1.6.1
|
||||||
|
- golangci-lint@v1.36.0
|
||||||
|
- goswagger@v0.26.0
|
||||||
|
- go.mod:
|
||||||
|
- sqlboiler@4.4.0
|
||||||
|
- swag@0.19.3
|
||||||
|
- strfmt@0.20.0
|
||||||
|
- testify@1.7.0
|
||||||
|
- go-openapi/runtime@v0.19.26
|
||||||
|
- go-openapi/swag@v0.19.13
|
||||||
|
- go-openapi/validate@v0.20.1
|
||||||
|
- jordan-wright/email
|
||||||
|
- rogpeppe/go-internal@v1.7.0
|
||||||
|
- golang.org/x/crypto
|
||||||
|
- golang.org/x/sys
|
||||||
|
- google.golang.org/api@v0.38.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- disabled goswagger generate server flag `--keep-spec-order` as relative resolution of its temporal created yml file is broken - see https://github.com/go-swagger/go-swagger/issues/2216
|
||||||
|
|
||||||
|
## 2020-11-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `make watch-swagger` and `make watch-sql`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- sqlboiler@4.3.0
|
||||||
|
|
||||||
|
## 2020-11-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `make watch-tests`: Watches .go files and runs package tests on modifications.
|
||||||
|
|
||||||
|
## 2020-09-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `pprof` handlers, see [FAQ: I need to (remotely) pprof my running service!](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-need-to-remotely-pprof-my-running-service)
|
||||||
|
|
||||||
|
## 2020-09-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `make git-merge-go-starter`, see [FAQ: I want to compare or update my project/fork to the latest go-starter master.](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-want-to-compare-or-update-my-projectfork-to-the-latest-go-starter-master)
|
||||||
|
|
||||||
|
## 2020-09-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `app probe readiness` and `app probe liveness` sub-commands.
|
||||||
|
- `/-/ready` and `/-/healthy` handlers.
|
||||||
|
|
||||||
|
## 2020-09-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Force VSCode to use our installed version of golang-cilint
|
||||||
|
- All `*.go` files in `/scripts` now use the build tag `scripts` so we can ensure they are not compiled into the final `app` binary.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `go.not` file to ensure certain generation- / test-only dependencies don't end up in the final `app` binary. Automatically checked though `make` (sub-target `make check-embedded-modules-go-not`).
|
||||||
|
|
||||||
|
## 2020-09-11
|
||||||
|
|
||||||
|
- Switch to `distroless` as final app stage, see [FAQ: Should I use distroless/base or debian:buster-slim in the Dockerfile app stage?](https://github.com/allaboutapps/go-starter/wiki/FAQ#should-i-use-distrolessbase-or-debianbuster-slim-in-the-dockerfile-app-stage)
|
||||||
31
.changes-go-starter/25.01.0.md
Normal file
31
.changes-go-starter/25.01.0.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## 25.01.0 - 2025-01-02
|
||||||
|
### Added
|
||||||
|
* Migrate the changelog tracking from manual tracking to the changelog tracking tool [changie](https://github.com/miniscruff/changie)
|
||||||
|
### Changed
|
||||||
|
* Update to [golang:1.23.4-bookworm](https://hub.docker.com/layers/library/golang/1.23.4-bookworm/images/sha256-5c3223fcb23efeccf495739c9fd9bbfe76cee51caea90591860395057eab3113) (requires `./docker-helper.sh --rebuild`)
|
||||||
|
* [Bump github.com/golangci/golangci-lint from v1.59.0 to v1.62.2](https://github.com/golangci/golangci-lint/releases/tag/v1.62.2)
|
||||||
|
* `go.mod` updates:
|
||||||
|
- Minor: [Bump github.com/BurntSushi/toml from v1.3.2 to v1.4.0](https://github.com/BurntSushi/toml/releases/tag/v1.4.0)
|
||||||
|
- Patch: [Bump github.com/gabriel-vasile/mimetype from v1.4.3 to v1.4.7](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.4.7)
|
||||||
|
- Minor: [Bump github.com/go-openapi/errors from v0.21.0 to v0.22.0](https://github.com/go-openapi/errors/releases/tag/v0.22.0)
|
||||||
|
- Minor: [Bump github.com/go-openapi/runtime from v0.27.1 to v0.28.0](https://github.com/go-openapi/runtime/releases/tag/v0.28.0)
|
||||||
|
- Minor: [Bump github.com/go-openapi/strfmt from v0.22.0 to v0.23.0](https://github.com/go-openapi/strfmt/releases/tag/v0.23.0)
|
||||||
|
- Minor: [Bump github.com/go-openapi/swag from v0.22.9 to v0.23.0](https://github.com/go-openapi/swag/releases/tag/v0.23.0)
|
||||||
|
- Minor: [Bump github.com/go-openapi/validate from v0.22.6 to v0.24.0](https://github.com/go-openapi/validate/releases/tag/v0.24.0)
|
||||||
|
- Patch: [Bump github.com/labstack/echo/v4 from v4.11.4 to v4.13.3](https://github.com/labstack/echo/releases/tag/v4.13.3)
|
||||||
|
- Patch: [Bump github.com/nicksnyder/go-i18n/v2 from v2.4.0 to v2.4.1](https://github.com/nicksnyder/go-i18n/releases/tag/v2.4.1)
|
||||||
|
- Minor: [Bump github.com/rs/zerolog from v1.31.0 to v1.33.0](https://github.com/rs/zerolog/releases/tag/v1.33.0)
|
||||||
|
- Minor: [Bump github.com/rubenv/sql-migrate from v1.6.1 to v1.7.1](https://github.com/rubenv/sql-migrate/releases/tag/v1.7.1)
|
||||||
|
- Patch: [Bump github.com/spf13/cobra from v1.8.0 to v1.8.1](https://github.com/spf13/cobra/releases/tag/v1.8.1)
|
||||||
|
- Minor: [Bump github.com/spf13/viper from v1.18.2 to v1.19.0](https://github.com/spf13/viper/releases/tag/v1.19.0)
|
||||||
|
- Minor: [Bump github.com/stretchr/testify from v1.8.4 to v1.10.0](https://github.com/stretchr/testify/releases/tag/v1.10.0)
|
||||||
|
- Patch: [Bump github.com/volatiletech/sqlboiler/v4 from v4.16.1 to v4.17.1](https://github.com/volatiletech/sqlboiler/releases/tag/v4.17.1)
|
||||||
|
- Patch: [Bump github.com/volatiletech/strmangle from v0.0.6 to v0.0.8](https://github.com/volatiletech/strmangle/releases/tag/v0.0.8)
|
||||||
|
- Minor: [Bump golang.org/x/crypto from v0.18.0 to v0.31.0](https://github.com/golang/crypto/releases/tag/v0.31.0)
|
||||||
|
- Minor: [Bump golang.org/x/mod from v0.14.0 to v0.22.0](https://github.com/golang/mod/releases/tag/v0.22.0)
|
||||||
|
- Minor: [Bump golang.org/x/sys from v0.16.0 to v0.28.0](https://github.com/golang/sys/releases/tag/v0.28.0)
|
||||||
|
- Minor: [Bump golang.org/x/text from v0.14.0 to v0.21.0](https://github.com/golang/text/releases/tag/v0.21.0)
|
||||||
|
- Minor: [Bump google.golang.org/api from v0.161.0 to v0.214.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.214.0)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Remove `util.MinInt` and `util.MaxInt`, use the built-in functions `min` and `max` instead. See https://go.dev/ref/spec#Min_and_max
|
||||||
3
.changes-go-starter/25.01.1.md
Normal file
3
.changes-go-starter/25.01.1.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 25.01.1 - 2025-01-03
|
||||||
|
### Changed
|
||||||
|
* Bump [github.com/volatiletech/sqlboiler/v4 from v4.17.1 to v4.18.0](https://github.com/volatiletech/sqlboiler/releases/tag/v4.18.0)
|
||||||
6
.changes-go-starter/25.01.2.md
Normal file
6
.changes-go-starter/25.01.2.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
## 25.01.2 - 2025-01-17
|
||||||
|
### Changed
|
||||||
|
* Refactor migration application logic to apply each missing migration individually and to print infos about the migration being applied
|
||||||
|
* Refactor the cobra cmd setup structure and the flag parsing logic using struct-based flags.
|
||||||
|
**BREAKING**: The restructuring breaks additionally created subcommands attached to the existing commands. See [Migration Guide Command Restructuring](https://github.com/allaboutapps/go-starter/wiki/Migration-Guide-Command-Restructuring)
|
||||||
|
|
||||||
4
.changes-go-starter/25.01.3.md
Normal file
4
.changes-go-starter/25.01.3.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## 25.01.3 - 2025-01-17
|
||||||
|
### Fixed
|
||||||
|
* Fix shutdown handling to check all errors
|
||||||
|
* Fix nil pointer dereference of shutdown without s.Echo
|
||||||
29
.changes-go-starter/25.02.0.md
Normal file
29
.changes-go-starter/25.02.0.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## 25.02.0 - 2025-02-17
|
||||||
|
### Added
|
||||||
|
* Add STARTTLS support to the mailer, thx [@mwieser](https://github.com/mwieser)
|
||||||
|
* Extend util and test package with additional helper functions, thx [@mwieser](https://github.com/mwieser)
|
||||||
|
* Add MIME interface to use *mimtype.MIME or an already KnownMIME, thx [@mwieser](https://github.com/mwieser)
|
||||||
|
* Add function to detach context to avoid context cancelation. Can be used to pass context information to go routines without a deadline or cancel, thx [@mwieser](https://github.com/mwieser)
|
||||||
|
* Add oauth2 helper for PKCE extention to generate verifier and challenge, thx [@mwieser](https://github.com/mwieser)
|
||||||
|
* Extend mailer mock to support waiting for all expected mails to arrive to check asynchronously sent mails in tests, thx [@mwieser](https://github.com/mwieser)
|
||||||
|
* Add mock clock to server struct to ensure consistent and mockable time during tests
|
||||||
|
* Add devcontainer startup commands to import local gitconfig into the container
|
||||||
|
### Changed
|
||||||
|
* Separated the go-starter and child project readme and changelog files to prevent conflicts
|
||||||
|
* Simplify the endpoint tests by using test.RequireHTTPError, response snapshots and table-driven tests
|
||||||
|
* Update to [golang:1.24.0-bookworm](https://hub.docker.com/layers/library/golang/1.24.0-bookworm/images/sha256-b95002399f27188790f0a2b598bee84ae7bfbf1043fb60921da3b81928e303ba) (requires `./docker-helper.sh --rebuild`)
|
||||||
|
* Minor: [Bump github.com/golangci/golangci-lint from v1.62.2 to v1.64.5](https://github.com/golangci/golangci-lint/releases/tag/v1.64.5)
|
||||||
|
* Dependency updates:
|
||||||
|
- Patch: [Bump github.com/gabriel-vasile/mimetype from v1.4.7 to v1.4.8](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.4.8)
|
||||||
|
- Minor: [Bump github.com/spf13/cobra from v1.8.1 to v1.9.1](https://github.com/spf13/cobra/releases/tag/v1.9.1)
|
||||||
|
- Patch: [Bump github.com/volatiletech/sqlboiler/v4 from v4.17.1 to v4.18.0](https://github.com/volatiletech/sqlboiler/releases/tag/v4.18.0)
|
||||||
|
- Minor: [Bump golang.org/x/crypto from v0.31.0 to v0.33.0](https://github.com/golang/crypto/releases/tag/v0.33.0)
|
||||||
|
- Minor: [Bump golang.org/x/mod from v0.22.0 to v0.23.0](https://github.com/golang/mod/releases/tag/v0.23.0)
|
||||||
|
- Minor: [Bump golang.org/x/sys from v0.28.0 to v0.30.0](https://github.com/golang/sys/releases/tag/v0.30.0)
|
||||||
|
- Minor: [Bump golang.org/x/text from v0.21.0 to v0.22.0](https://github.com/golang/text/releases/tag/v0.22.0)
|
||||||
|
- Minor: [Bump google.golang.org/api from v0.214.0 to v0.221.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.221.0)
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* Deprecated the mailer option UseTLS (`SERVER_SMTP_USE_TLS`) in favour of Encryption (`SERVER_SMTP_ENCRYPTION`). If you were using the `SERVER_SMTP_USE_TLS` flag before to enable TLS, you will need to migrate to the `SERVER_SMTP_ENCRYPTION` setting of `tls`. For the moment, both settings are supported (with a warning being printed when using `SERVER_SMTP_USE_TLS`, however support for the deprecated config will be dropped in one of the next releases. See [Mailer UseTLS SERVER_SMTP_USE_TLS Deprecation](https://github.com/allaboutapps/go-starter/wiki/Mailer-UseTLS-SERVER_SMTP_USE_TLS-Deprecation)
|
||||||
|
### Removed
|
||||||
|
* Remove `util.RunningInTest()` in favour of `testing.Testing()`, see https://pkg.go.dev/testing@master#Testing
|
||||||
7
.changes-go-starter/25.02.1.md
Normal file
7
.changes-go-starter/25.02.1.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## 25.02.1 - 2025-02-17
|
||||||
|
### Changed
|
||||||
|
* Improve the makefile help messages, thx [@rainchen](https://github.com/rainchen)
|
||||||
|
### Removed
|
||||||
|
* Remove `util.ContainsString`, use `slices.Contains` instead. Fixes https://github.com/allaboutapps/go-starter/issues/275. See https://go.dev/doc/go1.21#slices
|
||||||
|
### Fixed
|
||||||
|
* Fix Upsertable interface to match Upsert func of sqlboiler, fixes https://github.com/allaboutapps/go-starter/issues/277
|
||||||
35
.changes-go-starter/25.04.0.md
Normal file
35
.changes-go-starter/25.04.0.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
## 25.04.0 - 2025-04-04
|
||||||
|
### Added
|
||||||
|
* Add helper for streaming a file with a correctly encoded content disposition header to allow utf-8 filenames
|
||||||
|
* Add setup for collecting and exposing prometheus metrics with custom metrics and sqlstats metrics.
|
||||||
|
* Add custom not found handler for echo to show a HTML page if text/html is requested
|
||||||
|
* Add endpoint to delete user account with all related data
|
||||||
|
### Changed
|
||||||
|
* Moved the data fixtures from `internal/data/` to a separate package in `internal/data/fixtures`. **BREAKING**: Existing changes to the fixtures need to be moved from `internal/data/` to `internal/data/fixtures` and imports need to be updated
|
||||||
|
* Move the application logic from the handlers to dedicated services. **BREAKING**: This might break existing applications that have custom changes to the handlers. The custom changes need to be reflected in the new services.
|
||||||
|
* Fix the naming of the put push token endpoint from post to put
|
||||||
|
* Moved the auth related parts from `internal/api/auth` to `internal/auth`. **BREAKING**: Changes to `authentication_result.go`, `context.go` and `scopes.go` need to be reflected in `internal/auth` instead of `internal/api/auth` and imports need to be updated
|
||||||
|
* Bump Postgres from `postgres:12.4-alpine` to `postgres:17.4-alpine`. See the official Postgres release notes for more details:
|
||||||
|
* https://www.postgresql.org/docs/release/13.0/
|
||||||
|
* https://www.postgresql.org/docs/release/14.0/
|
||||||
|
* https://www.postgresql.org/docs/release/15.0/
|
||||||
|
* https://www.postgresql.org/docs/release/16.0/
|
||||||
|
* https://www.postgresql.org/docs/release/17.0/
|
||||||
|
|
||||||
|
* Move the test fixtures from `internal/test/` to a separate package in `internal/test/fixtures`. **BREAKING**: Existing changes to the fixtures need to be moved from `internal/test/` to `internal/test/fixtures` and imports need to be updated
|
||||||
|
* Update to `golang:1.24.2-bookworm` (requires `./docker-helper.sh --rebuild`)
|
||||||
|
* Dependency updates:
|
||||||
|
- Patch: [Bump github.com/BurntSushi/toml from v1.4.0 to v1.5.0](https://github.com/BurntSushi/toml/releases/tag/v1.5.0)
|
||||||
|
- Patch: [Bump github.com/go-openapi/errors from v0.22.0 to v0.22.1](https://github.com/go-openapi/errors/releases/tag/v0.22.1)
|
||||||
|
- Patch: [Bump github.com/go-openapi/swag from v0.23.0 to v0.23.1](https://github.com/go-openapi/swag/releases/tag/v0.23.1)
|
||||||
|
- Patch: [Bump github.com/spf13/viper from v1.19.0 to v1.20.1](https://github.com/spf13/viper/releases/tag/v1.20.1)
|
||||||
|
- Patch: [Bump golang.org/x/sys from v0.30.0 to v0.31.0](https://github.com/golang/sys/releases/tag/v0.31.0)
|
||||||
|
- Minor: [Bump github.com/prometheus/client_golang from v1.20.5 to v1.21.1](https://github.com/prometheus/client_golang/releases/tag/v1.21.1)
|
||||||
|
- Minor: [Bump github.com/rs/zerolog from v1.33.0 to v1.34.0](https://github.com/rs/zerolog/releases/tag/v1.34.0)
|
||||||
|
- Minor: [Bump golang.org/x/crypto from v0.33.0 to v0.36.0](https://github.com/golang/crypto/releases/tag/v0.36.0)
|
||||||
|
- Minor: [Bump golang.org/x/mod from v0.23.0 to v0.24.0](https://github.com/golang/mod/releases/tag/v0.24.0)
|
||||||
|
- Minor: [Bump golang.org/x/text from v0.22.0 to v0.23.0](https://github.com/golang/text/releases/tag/v0.23.0)
|
||||||
|
- Minor: [Bump google.golang.org/api from v0.221.0 to v0.228.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.228.0)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Remove deprecated endpoint GetPushTestRoute
|
||||||
3
.changes-go-starter/25.04.1.md
Normal file
3
.changes-go-starter/25.04.1.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 25.04.1 - 2025-04-04
|
||||||
|
### Changed
|
||||||
|
* Bump Github action workflow `actions/checkout` from `v2.3.4` to `v4`
|
||||||
34
.changes-go-starter/25.10.0.md
Normal file
34
.changes-go-starter/25.10.0.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## 25.10.0 - 2025-10-16
|
||||||
|
### Added
|
||||||
|
* Add handling for registration with account confirmation
|
||||||
|
* Add swagger spec linting using spectral
|
||||||
|
* Add base setup for apple site association file and android assetlinks for handling universal links
|
||||||
|
* Add [wire](https://github.com/google/wire) for initing the `Server` using dependency injection. **BREAKING**: Changes to the init logic need to be migrated to `wire`, see [docs/server-initialization.md](docs/server-initialization.md) for details.
|
||||||
|
* Add helper to create subcommand group
|
||||||
|
* Add a test executing the datbase migrations up, down and up again
|
||||||
|
### Changed
|
||||||
|
* the GenHandlers script can accept trailing wildcards for suffix matching
|
||||||
|
* replaced the deprecated tenv linter with usetesting
|
||||||
|
* Refactor the public http error to use a enum for the error type. **BREAKING**: All existing errors need to be migrated to use the enum as error type. Therefore the error type keys need to be added to the Enum `PublicHTTPErrorType` and need to be used in the error definition.
|
||||||
|
* Update to `golang:1.24.4-bookworm` (requires `./docker-helper.sh --rebuild`)
|
||||||
|
- Patch: Bump [github.com/gotestyourself/gotestsum](https://github.com/gotestyourself/gotestsum/releases/tag/v1.12.2) from 1.12.0 to 1.12.2
|
||||||
|
|
||||||
|
* Bump [github.com/golangci/golangci-lint](https://golangci-lint.run/product/migration-guide) from v1.64.5 to v2.1.6
|
||||||
|
* Update to [golang:1.25.0-bookworm](https://hub.docker.com/layers/library/golang/1.25.0-bookworm/images/sha256-89bd1d243f3a5a48c9acc03e5fb01e97e0ba4874c249a54e693dee430f9ef74e) (requires `./docker-helper.sh --rebuild`)
|
||||||
|
* Minor: [Bump github.com/golangci/golangci-lint from v2.1.6 to v2.4.0](https://github.com/golangci/golangci-lint/releases/tag/v2.4.0)
|
||||||
|
* Patch: [Bump github.com/gotestyourself/gotestsum from v1.12.2 to v1.12.3](https://github.com/gotestyourself/gotestsum/releases/tag/v1.12.3)
|
||||||
|
* Dependency updates:
|
||||||
|
- Package replacement: [Replaced github.com/volatiletech packages with github.com/aarondl equivalents and bumped to latest versions](https://github.com/aarondl)
|
||||||
|
- Patch: [Bump github.com/gabriel-vasile/mimetype from v1.4.8 to v1.4.9](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.4.9)
|
||||||
|
- Patch: [Bump github.com/go-openapi/errors from v0.22.1 to v0.22.2](https://github.com/go-openapi/errors/releases/tag/v0.22.2)
|
||||||
|
- Patch: [Bump github.com/labstack/echo/v4 from v4.13.3 to v4.13.4](https://github.com/labstack/echo/releases/tag/v4.13.4)
|
||||||
|
- Minor: [Bump github.com/rubenv/sql-migrate from v1.7.1 to v1.8.0](https://github.com/rubenv/sql-migrate/releases/tag/v1.8.0)
|
||||||
|
- Minor: [Bump github.com/prometheus/client_golang from v1.21.1 to v1.23.0](https://github.com/prometheus/client_golang/releases/tag/v1.23.0)
|
||||||
|
- Minor: [Bump golang.org/x/crypto from v0.36.0 to v0.41.0](https://github.com/golang/crypto/releases/tag/v0.41.0)
|
||||||
|
- Minor: [Bump golang.org/x/mod from v0.24.0 to v0.26.0](https://github.com/golang/mod/releases/tag/v0.26.0)
|
||||||
|
- Minor: [Bump golang.org/x/sys from v0.31.0 to v0.35.0](https://github.com/golang/sys/releases/tag/v0.35.0)
|
||||||
|
- Minor: [Bump golang.org/x/text from v0.23.0 to v0.28.0](https://github.com/golang/text/releases/tag/v0.28.0)
|
||||||
|
- Minor: [Bump google.golang.org/api from v0.228.0 to v0.247.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.247.0)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Remove the mailer option UseTLS (`SERVER_SMTP_USE_TLS`) in favour of Encryption (`SERVER_SMTP_ENCRYPTION`). **BREAKING**: If you were using the `SERVER_SMTP_USE_TLS` flag before to enable TLS, you will need to migrate to the `SERVER_SMTP_ENCRYPTION` setting of `tls`. See [Mailer UseTLS SERVER_SMTP_USE_TLS Deprecation](https://github.com/allaboutapps/go-starter/wiki/Mailer-UseTLS-SERVER_SMTP_USE_TLS-Deprecation)
|
||||||
9
.changes-go-starter/header.tpl.md
Normal file
9
.changes-go-starter/header.tpl.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
- All notable changes to this project will be documented in this file.
|
||||||
|
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
- We do not follow [semantic versioning](https://semver.org/).
|
||||||
|
- All changes are solely **tracked by date** and have a **git tag** available (from 2021-10-19 onwards):
|
||||||
|
- Git tags are formatted like `go-starter-YYYY-MM-DD`. See [GitHub tags](https://github.com/allaboutapps/go-starter/tags) for all available go-starter git tags.
|
||||||
|
- The latest `master` is considered **stable** and should be periodically merged into our customer projects.
|
||||||
|
- Please follow the update process in *[I just want to update / upgrade my project!](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-just-want-to-update--upgrade-my-project)*.
|
||||||
0
.changes-go-starter/prerelease/.gitkeep
Normal file
0
.changes-go-starter/prerelease/.gitkeep
Normal file
0
.changes-go-starter/unreleased/.gitkeep
Normal file
0
.changes-go-starter/unreleased/.gitkeep
Normal file
20
.changie-go-starter.yaml
Normal file
20
.changie-go-starter.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
changesDir: .changes-go-starter
|
||||||
|
unreleasedDir: unreleased
|
||||||
|
headerPath: header.tpl.md
|
||||||
|
changelogPath: CHANGELOG-go-starter.md
|
||||||
|
versionExt: md
|
||||||
|
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
|
||||||
|
kindFormat: '### {{.Kind}}'
|
||||||
|
changeFormat: '* {{.Body}}'
|
||||||
|
kinds:
|
||||||
|
- label: Added
|
||||||
|
- label: Changed
|
||||||
|
- label: Deprecated
|
||||||
|
- label: Removed
|
||||||
|
- label: Fixed
|
||||||
|
- label: Security
|
||||||
|
newlines:
|
||||||
|
afterChangelogHeader: 1
|
||||||
|
beforeChangelogVersion: 1
|
||||||
|
endOfVersion: 1
|
||||||
|
envPrefix: CHANGIE_
|
||||||
114
.devcontainer/devcontainer.json
Normal file
114
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
|
||||||
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.106.0/containers/docker-existing-docker-compose
|
||||||
|
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
|
||||||
|
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
|
||||||
|
"dockerComposeFile": [
|
||||||
|
"../docker-compose.yml"
|
||||||
|
],
|
||||||
|
// The 'service' property is the name of the service for the container that VS Code should
|
||||||
|
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
|
||||||
|
"service": "service",
|
||||||
|
// The optional 'workspaceFolder' property is the path VS Code should open by default when
|
||||||
|
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
|
||||||
|
"workspaceFolder": "/app",
|
||||||
|
// All containers should stop if we close / reload the VSCode window.
|
||||||
|
"shutdownAction": "stopCompose",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
// Set *default* container specific settings.json values on container create.
|
||||||
|
"settings": {
|
||||||
|
// https://github.com/golang/tools/blob/master/gopls/doc/vscode.md#vscode
|
||||||
|
"go.useLanguageServer": true,
|
||||||
|
"[go]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true
|
||||||
|
},
|
||||||
|
// Optional: Disable snippets, as they conflict with completion ranking.
|
||||||
|
"editor.snippetSuggestions": "none"
|
||||||
|
},
|
||||||
|
"[go.mod]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"[sql]": {
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
// There are handly utility scripts within /scripts that we invoke via go run.
|
||||||
|
// These scripts (and its dependencies) should never be consumed by the actual server directly
|
||||||
|
// Thus they are flagged to require the "scripts" build tag.
|
||||||
|
// We only inform gopls and the vscode go compiler here, that it has to set this build tag if it sees such a file.
|
||||||
|
"go.buildTags": "scripts",
|
||||||
|
"gopls": {
|
||||||
|
// Add parameter placeholders when completing a function.
|
||||||
|
"usePlaceholders": true,
|
||||||
|
// If true, enable additional analyses with staticcheck.
|
||||||
|
// Warning: This will significantly increase memory usage.
|
||||||
|
// DISABLED, done via
|
||||||
|
"staticcheck": false
|
||||||
|
},
|
||||||
|
// https://code.visualstudio.com/docs/languages/go#_intellisense
|
||||||
|
"go.autocompleteUnimportedPackages": true,
|
||||||
|
// https://github.com/golangci/golangci-lint#editor-integration
|
||||||
|
"go.lintTool": "golangci-lint",
|
||||||
|
"go.lintFlags": [
|
||||||
|
"--fast",
|
||||||
|
"--timeout",
|
||||||
|
"5m"
|
||||||
|
],
|
||||||
|
// disable test caching, race and show coverage (in sync with makefile)
|
||||||
|
"go.testFlags": [
|
||||||
|
"-cover",
|
||||||
|
"-race",
|
||||||
|
"-count=1",
|
||||||
|
"-v"
|
||||||
|
],
|
||||||
|
"go.coverMode": "atomic", // atomic is required when utilizing -race
|
||||||
|
"go.delveConfig": {
|
||||||
|
"dlvLoadConfig": {
|
||||||
|
// increase max length of strings displayed in debugger
|
||||||
|
"maxStringLen": 2048
|
||||||
|
},
|
||||||
|
"apiVersion": 2
|
||||||
|
},
|
||||||
|
// ensure that the pgFormatter VSCode extension uses the pgFormatter that comes preinstalled in the Dockerfile
|
||||||
|
"pgFormatter.pgFormatterPath": "/usr/local/bin/pg_format",
|
||||||
|
// only validate the merged swagger file as the individual files do not meet all linting requirements
|
||||||
|
"spectral.validateFiles": [
|
||||||
|
"**/api/swagger.yml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
// required:
|
||||||
|
"golang.go",
|
||||||
|
"bradymholt.pgformatter",
|
||||||
|
// optional:
|
||||||
|
"42crunch.vscode-openapi",
|
||||||
|
"stoplight.spectral",
|
||||||
|
"heaths.vscode-guid",
|
||||||
|
"bungcip.better-toml",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"casualjim.gotemplate",
|
||||||
|
"yzhang.markdown-all-in-one"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// import host-local git config, with all applicable includes, into the container (https://github.com/microsoft/vscode-remote-release/issues/2084#issuecomment-2259986798)
|
||||||
|
"initializeCommand": "git config -l --global --include > \"${localWorkspaceFolder}\"/.gitconfig.global",
|
||||||
|
"postAttachCommand": "while IFS='=' read -r key value; do git config --global \"$key\" \"$value\"; done < \"${containerWorkspaceFolder}\"/.gitconfig.global; rm -f \"${containerWorkspaceFolder}\"/.gitconfig.global",
|
||||||
|
// Uncomment the next line if you want start specific services in your Docker Compose config.
|
||||||
|
// "runServices": [],
|
||||||
|
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
|
||||||
|
// "shutdownAction": "none",
|
||||||
|
// Uncomment the next line to run commands after the container is created - for example installing git.
|
||||||
|
"postCreateCommand": "go version"
|
||||||
|
// "postCreateCommand": "apt-get update && apt-get install -y git",
|
||||||
|
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
||||||
|
// "remoteUser": ""
|
||||||
|
}
|
||||||
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.devcontainer
|
||||||
|
.vscode
|
||||||
|
.pkg
|
||||||
|
.tools-versions
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.*
|
||||||
|
docker-helper.sh
|
||||||
453
.drone.yml
Normal file
453
.drone.yml
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Drone matrix: Additional ENV vars for substitution - http://docs.drone.io/matrix-builds/
|
||||||
|
# Will be evaluated BEFORE the YAML is parsed, ONLY strings allowed, NO substitutions ${XXX} here.
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- BUILD_ENV: all
|
||||||
|
# The name of the k8s namespaces that these pipelines will target.
|
||||||
|
# K8S_DEPLOY_NS_DEV:
|
||||||
|
# K8S_DEPLOY_NS_STAGING:
|
||||||
|
# K8S_DEPLOY_NS_PRODUCTION:
|
||||||
|
|
||||||
|
# YAML Configuration anchors - https://learnxinyminutes.com/docs/yaml/
|
||||||
|
# Will be evaluated WHILE the YAML is parsed, any valid yaml allowed, substitutions ${XXX} allowed.
|
||||||
|
|
||||||
|
alias:
|
||||||
|
# The image will be tagged with this, pushed to gcr and referenced with this key in the k8s deployment
|
||||||
|
- &IMAGE_DEPLOY_TAG ${DRONE_COMMIT_SHA}
|
||||||
|
|
||||||
|
# The image name, defaults to lowercase repo name <owner>/<project>, e.g. aw/aaa-cab-kubernetes-test
|
||||||
|
- &IMAGE_DEPLOY_NAME ${DRONE_REPO,,}
|
||||||
|
|
||||||
|
# The intermediate builder image name
|
||||||
|
- &IMAGE_BUILDER_ID ${DRONE_REPO,,}-builder:${DRONE_COMMIT_SHA}
|
||||||
|
|
||||||
|
# The full uniquely tagged app image name
|
||||||
|
- &IMAGE_DEPLOY_ID ${DRONE_REPO,,}:${DRONE_COMMIT_SHA}
|
||||||
|
|
||||||
|
# # Defines which branches will trigger a docker image push our Google Cloud Registry (tags are always published)
|
||||||
|
# - &GCR_PUBLISH_BRANCHES [dev, staging, master]
|
||||||
|
|
||||||
|
# # Docker registry publish default settings
|
||||||
|
# - &GCR_REGISTRY_SETTINGS
|
||||||
|
# image: plugins/gcr
|
||||||
|
# repo: a3cloud-192413/${DRONE_REPO,,}
|
||||||
|
# registry: eu.gcr.io
|
||||||
|
# secrets:
|
||||||
|
# - source: AAA_GCR_SERVICE_ACCOUNT_JSON
|
||||||
|
# target: google_credentials
|
||||||
|
# # local short-time-cache: don't cleanup any image layers after pushing
|
||||||
|
# purge: false
|
||||||
|
# # force compress of docker build context
|
||||||
|
# compress: true
|
||||||
|
# volumes: # mount needed to push the already build container
|
||||||
|
# - /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
# # Deployment default settings
|
||||||
|
# - &K8S_DEPLOY_SETTINGS
|
||||||
|
# image: eu.gcr.io/a3cloud-192413/aw/aaa-drone-kubernetes:latest
|
||||||
|
# pull: true
|
||||||
|
# secrets:
|
||||||
|
# - source: AAA_K8S_SERVER
|
||||||
|
# target: KUBERNETES_SERVER
|
||||||
|
# - source: AAA_K8S_SERVICE_ACCOUNT_CRT
|
||||||
|
# target: KUBERNETES_CERT
|
||||||
|
# - source: AAA_K8S_SERVICE_ACCOUNT_TOKEN
|
||||||
|
# target: KUBERNETES_TOKEN
|
||||||
|
# - source: AAA_GCR_SERVICE_ACCOUNT_JSON
|
||||||
|
# target: GCR_SERVICE_ACCOUNT
|
||||||
|
# deployment: app
|
||||||
|
# repo: eu.gcr.io/a3cloud-192413/${DRONE_REPO,,}
|
||||||
|
# container: [app]
|
||||||
|
# tag: *IMAGE_DEPLOY_TAG
|
||||||
|
# gcr_service_account_email: drone-ci-a3cloud@a3cloud-192413.iam.gserviceaccount.com
|
||||||
|
# mgmt_repo: https://github.com/allaboutapps/a3cloud-mgmt.git
|
||||||
|
# mgmt_git_email: infrastructure+drone@allaboutapps.at
|
||||||
|
|
||||||
|
# ENV variables for executing within the test env (similar to the env in docker-compose.yml)
|
||||||
|
- &TEST_ENV
|
||||||
|
CI: ${CI}
|
||||||
|
|
||||||
|
# required: env for main working database, service
|
||||||
|
# default for sql-migrate (target development) and psql cli tool
|
||||||
|
PGDATABASE: &PGDATABASE "development"
|
||||||
|
PGUSER: &PGUSER "dbuser"
|
||||||
|
PGPASSWORD: &PGPASSWORD "dbpass"
|
||||||
|
PGHOST: &PGHOST "postgres"
|
||||||
|
PGPORT: &PGPORT "5432"
|
||||||
|
PGSSLMODE: &PGSSLMODE "disable"
|
||||||
|
|
||||||
|
# optional: env for sql-boiler (ability to generate models out of a "spec" database)
|
||||||
|
# sql-boiler should operate on a "spec" database only
|
||||||
|
PSQL_DBNAME: "spec"
|
||||||
|
PSQL_USER: *PGUSER
|
||||||
|
PSQL_PASS: *PGPASSWORD
|
||||||
|
PSQL_HOST: *PGHOST
|
||||||
|
PSQL_PORT: *PGPORT
|
||||||
|
PSQL_SSLMODE: *PGSSLMODE
|
||||||
|
|
||||||
|
# required for drone: project root directory, used for relative path resolution (e.g. fixtures)
|
||||||
|
PROJECT_ROOT_DIR: /app
|
||||||
|
|
||||||
|
# docker run related.
|
||||||
|
SERVER_MANAGEMENT_SECRET: "mgmt-secret"
|
||||||
|
|
||||||
|
# Which build events should trigger the main pipeline (defaults to all)
|
||||||
|
- &BUILD_EVENTS [push, tag]
|
||||||
|
|
||||||
|
# Pipeline merge helper: only execute if build event received
|
||||||
|
- &WHEN_BUILD_EVENT
|
||||||
|
when:
|
||||||
|
event: *BUILD_EVENTS
|
||||||
|
|
||||||
|
# The actual pipeline building our product
|
||||||
|
pipeline:
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# BUILD
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
"database connection":
|
||||||
|
group: build
|
||||||
|
image: postgres:17.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live
|
||||||
|
commands:
|
||||||
|
# wait for postgres service to become available
|
||||||
|
- |
|
||||||
|
until psql -U $PGUSER -d $PGDATABASE -h postgres \
|
||||||
|
-c "SELECT 1;" >/dev/null 2>&1; do sleep 1; done
|
||||||
|
# query the database
|
||||||
|
- |
|
||||||
|
psql -U $PGUSER -d $PGDATABASE -h postgres \
|
||||||
|
-c "SELECT name, setting FROM pg_settings;"
|
||||||
|
environment: *TEST_ENV
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"docker build (target builder)":
|
||||||
|
group: build
|
||||||
|
image: docker:latest
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
IMAGE_TAG: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- "docker build --target builder --compress -t $${IMAGE_TAG} ."
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"docker build (target app)":
|
||||||
|
group: build-app
|
||||||
|
image: docker:latest
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
IMAGE_TAG: *IMAGE_DEPLOY_ID
|
||||||
|
commands:
|
||||||
|
- "docker build --target app --compress -t $${IMAGE_TAG} ."
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CHECK
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
"trivy scan":
|
||||||
|
group: pre-test
|
||||||
|
image: aquasec/trivy:latest
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /server/drone/trivy-cache:/root/.cache/
|
||||||
|
environment:
|
||||||
|
IMAGE_TAG: *IMAGE_DEPLOY_ID
|
||||||
|
commands:
|
||||||
|
# Print report
|
||||||
|
- "trivy image --exit-code 0 --no-progress $${IMAGE_TAG}"
|
||||||
|
# Fail on severity HIGH and CRITICAL
|
||||||
|
- "trivy image --exit-code 1 --severity HIGH,CRITICAL --no-progress --ignore-unfixed $${IMAGE_TAG}"
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"build & diff":
|
||||||
|
group: pre-test
|
||||||
|
image: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage
|
||||||
|
- make tidy
|
||||||
|
- make build
|
||||||
|
- /bin/cp -Rf $PROJECT_ROOT_DIR/* $DRONE_WORKSPACE # switch back to drone workspace ...
|
||||||
|
- cd $DRONE_WORKSPACE
|
||||||
|
- "git diff --exit-code" # ... for git diffing (otherwise not possible as .git is .dockerignored)
|
||||||
|
environment: *TEST_ENV
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"info":
|
||||||
|
group: test
|
||||||
|
image: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage
|
||||||
|
- make info
|
||||||
|
environment: *TEST_ENV
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"test":
|
||||||
|
group: test
|
||||||
|
image: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage
|
||||||
|
- make test
|
||||||
|
environment: *TEST_ENV
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"test-scripts (gsdev, go-starter only)":
|
||||||
|
group: test
|
||||||
|
image: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage
|
||||||
|
- make test-scripts
|
||||||
|
environment: *TEST_ENV
|
||||||
|
when:
|
||||||
|
repo: allaboutapps/AW-go-starter
|
||||||
|
event: *BUILD_EVENTS
|
||||||
|
|
||||||
|
"git-compare-go-starter":
|
||||||
|
group: test
|
||||||
|
image: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- make git-compare-go-starter
|
||||||
|
environment: *TEST_ENV
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"swagger-codegen-cli":
|
||||||
|
group: test
|
||||||
|
# https://github.com/swagger-api/swagger-codegen/blob/master/modules/swagger-codegen-cli/Dockerfile
|
||||||
|
image: swaggerapi/swagger-codegen-cli
|
||||||
|
commands:
|
||||||
|
# run the main swagger.yml validation.
|
||||||
|
- "java -jar /opt/swagger-codegen-cli/swagger-codegen-cli.jar validate -i ./api/swagger.yml"
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"binary: deps":
|
||||||
|
group: test
|
||||||
|
image: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- cd $PROJECT_ROOT_DIR
|
||||||
|
- make get-embedded-modules-count
|
||||||
|
- make get-embedded-modules
|
||||||
|
environment: *TEST_ENV
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"binary: licenses":
|
||||||
|
group: test
|
||||||
|
image: *IMAGE_BUILDER_ID
|
||||||
|
commands:
|
||||||
|
- cd $PROJECT_ROOT_DIR
|
||||||
|
- make get-licenses
|
||||||
|
environment: *TEST_ENV
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"docker run (target app)":
|
||||||
|
group: test
|
||||||
|
image: docker:latest
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
<<: *TEST_ENV
|
||||||
|
IMAGE_TAG: *IMAGE_DEPLOY_ID
|
||||||
|
commands:
|
||||||
|
# Note: NO network related tests are possible here, dnd can just
|
||||||
|
# run sibling containers. We have no possibility to connect them
|
||||||
|
# into the drone user defined per build docker network!
|
||||||
|
# https://github.com/drone-plugins/drone-docker/issues/193
|
||||||
|
# https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/
|
||||||
|
- (env | grep "^\S*=" | grep -v -e "DRONE=" -e "DRONE_" -e "CI_" -e "CI=" -e "HOME=" -e "HOSTNAME=" -e "SHELL=" -e "PWD=" -e "PATH=") > .hostenv
|
||||||
|
- cat .hostenv
|
||||||
|
- "docker run --env-file .hostenv $${IMAGE_TAG} help"
|
||||||
|
- "docker run --env-file .hostenv $${IMAGE_TAG} -v"
|
||||||
|
- "docker run --env-file .hostenv $${IMAGE_TAG} env"
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PUBLISH
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# # Built a allowed branch? Push to cloud registry
|
||||||
|
# "publish ${DRONE_BRANCH}_${DRONE_COMMIT_SHA:0:10}":
|
||||||
|
# group: publish
|
||||||
|
# <<: *GCR_REGISTRY_SETTINGS
|
||||||
|
# tags:
|
||||||
|
# - build_${DRONE_BUILD_NUMBER}
|
||||||
|
# - ${DRONE_BRANCH/\//-}_${DRONE_COMMIT_SHA:0:10}
|
||||||
|
# - *IMAGE_DEPLOY_TAG
|
||||||
|
# - latest
|
||||||
|
# - ${DRONE_BRANCH/\//-}
|
||||||
|
# - '${DRONE_COMMIT_SHA:0:10}'
|
||||||
|
# when:
|
||||||
|
# branch: *GCR_PUBLISH_BRANCHES
|
||||||
|
# event: *BUILD_EVENTS
|
||||||
|
|
||||||
|
# # Built a tag? Push to cloud registry
|
||||||
|
# "publish tag_${DRONE_COMMIT_SHA:0:10}":
|
||||||
|
# group: publish
|
||||||
|
# <<: *GCR_REGISTRY_SETTINGS
|
||||||
|
# tags:
|
||||||
|
# - build_${DRONE_BUILD_NUMBER}
|
||||||
|
# - tag_${DRONE_COMMIT_SHA:0:10}
|
||||||
|
# - *IMAGE_DEPLOY_TAG
|
||||||
|
# - latest
|
||||||
|
# - ${DRONE_TAG}
|
||||||
|
# - ${DRONE_COMMIT_SHA:0:10}
|
||||||
|
# when:
|
||||||
|
# event: tag
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DEPLOYMENT
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# # autodeploy dev if it hits the branch
|
||||||
|
# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_DEV} (auto)":
|
||||||
|
# <<: *K8S_DEPLOY_SETTINGS
|
||||||
|
# namespace: ${K8S_DEPLOY_NS_DEV}
|
||||||
|
# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_DEV}/app.deployment.yaml
|
||||||
|
# when:
|
||||||
|
# event: *BUILD_EVENTS
|
||||||
|
# branch: [dev]
|
||||||
|
|
||||||
|
# # promote dev through "drone deploy <repo> <build> dev"
|
||||||
|
# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_DEV} (promote)":
|
||||||
|
# <<: *K8S_DEPLOY_SETTINGS
|
||||||
|
# namespace: ${K8S_DEPLOY_NS_DEV}
|
||||||
|
# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_DEV}/app.deployment.yaml
|
||||||
|
# when:
|
||||||
|
# environment: dev
|
||||||
|
# event: deployment
|
||||||
|
|
||||||
|
# # autodeploy staging if it hits the branch
|
||||||
|
# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_STAGING} (auto)":
|
||||||
|
# <<: *K8S_DEPLOY_SETTINGS
|
||||||
|
# namespace: ${K8S_DEPLOY_NS_STAGING}
|
||||||
|
# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_STAGING}/app.deployment.yaml
|
||||||
|
# when:
|
||||||
|
# event: *BUILD_EVENTS
|
||||||
|
# branch: [staging]
|
||||||
|
|
||||||
|
# # promote staging through "drone deploy <repo> <build> staging"
|
||||||
|
# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_STAGING} (promote)":
|
||||||
|
# <<: *K8S_DEPLOY_SETTINGS
|
||||||
|
# namespace: ${K8S_DEPLOY_NS_STAGING}
|
||||||
|
# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_STAGING}/app.deployment.yaml
|
||||||
|
# when:
|
||||||
|
# environment: staging
|
||||||
|
# event: deployment
|
||||||
|
|
||||||
|
# # promote production through "drone deploy <repo> <build> production"
|
||||||
|
# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_PRODUCTION} (promote)":
|
||||||
|
# <<: *K8S_DEPLOY_SETTINGS
|
||||||
|
# namespace: ${K8S_DEPLOY_NS_PRODUCTION}
|
||||||
|
# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_PRODUCTION}/app.deployment.yaml
|
||||||
|
# when:
|
||||||
|
# environment: production
|
||||||
|
# event: deployment
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DEPLOYMENT go-starter
|
||||||
|
# Purpose: go-starter drone specific publish and deployment steps
|
||||||
|
# NOTE: you do not need to uncomment them for our customer projects
|
||||||
|
# These steps won't be executed unless we work in the main "allaboutapps/AW-go-starter" repo
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
"go-starter publish ${DRONE_BRANCH}_${DRONE_COMMIT_SHA:0:10}":
|
||||||
|
group: go-starter-publish
|
||||||
|
image: plugins/gcr
|
||||||
|
repo: a3cloud-192413/${DRONE_REPO,,}
|
||||||
|
registry: eu.gcr.io
|
||||||
|
secrets:
|
||||||
|
- source: AAA_GCR_SERVICE_ACCOUNT_JSON
|
||||||
|
target: google_credentials
|
||||||
|
# local short-time-cache: don't cleanup any image layers after pushing
|
||||||
|
purge: false
|
||||||
|
# force compress of docker build context
|
||||||
|
compress: true
|
||||||
|
volumes: # mount needed to push the already build container
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
tags:
|
||||||
|
- build_${DRONE_BUILD_NUMBER}
|
||||||
|
- ${DRONE_BRANCH/\//-}_${DRONE_COMMIT_SHA:0:10}
|
||||||
|
- *IMAGE_DEPLOY_TAG
|
||||||
|
- latest
|
||||||
|
- ${DRONE_BRANCH/\//-}
|
||||||
|
- "${DRONE_COMMIT_SHA:0:10}"
|
||||||
|
when:
|
||||||
|
repo: allaboutapps/AW-go-starter
|
||||||
|
branch: [master]
|
||||||
|
event: [push, tag]
|
||||||
|
|
||||||
|
"go-starter deploy ${DRONE_COMMIT_SHA:0:10} to allaboutapps-go-starter-dev (auto)":
|
||||||
|
group: go-starter-deploy
|
||||||
|
image: eu.gcr.io/a3cloud-192413/aw/aaa-drone-kubernetes:latest
|
||||||
|
pull: true
|
||||||
|
secrets:
|
||||||
|
- source: AAA_K8S_SERVER
|
||||||
|
target: KUBERNETES_SERVER
|
||||||
|
- source: AAA_K8S_SERVICE_ACCOUNT_CRT
|
||||||
|
target: KUBERNETES_CERT
|
||||||
|
- source: AAA_K8S_SERVICE_ACCOUNT_TOKEN
|
||||||
|
target: KUBERNETES_TOKEN
|
||||||
|
- source: AAA_GCR_SERVICE_ACCOUNT_JSON
|
||||||
|
target: GCR_SERVICE_ACCOUNT
|
||||||
|
deployment: app
|
||||||
|
repo: eu.gcr.io/a3cloud-192413/${DRONE_REPO,,}
|
||||||
|
container: [app]
|
||||||
|
tag: *IMAGE_DEPLOY_TAG
|
||||||
|
gcr_service_account_email: drone-ci-a3cloud@a3cloud-192413.iam.gserviceaccount.com
|
||||||
|
mgmt_repo: https://github.com/allaboutapps/a3cloud-mgmt.git
|
||||||
|
mgmt_git_email: infrastructure+drone@allaboutapps.at
|
||||||
|
namespace: allaboutapps-go-starter-dev
|
||||||
|
mgmt_deployment_yaml: namespaces/allaboutapps-go-starter-dev/app.deployment.yaml
|
||||||
|
skip_live_update: true
|
||||||
|
kubernetes_cluster: a3cloud-dev
|
||||||
|
# pre-sync migration job
|
||||||
|
job: pre-sync-migration
|
||||||
|
mgmt_job_yaml: namespaces/allaboutapps-go-starter-dev/application.yaml
|
||||||
|
when:
|
||||||
|
repo: allaboutapps/AW-go-starter
|
||||||
|
branch: [master]
|
||||||
|
event: [push, tag]
|
||||||
|
|
||||||
|
# Long living services where the startup order does not matter (otherwise use detach: true)
|
||||||
|
services:
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SERVICES
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
"env":
|
||||||
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- "env | sort"
|
||||||
|
|
||||||
|
"postgres":
|
||||||
|
image: postgres:17.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: *PGDATABASE
|
||||||
|
POSTGRES_USER: *PGUSER
|
||||||
|
POSTGRES_PASSWORD: *PGPASSWORD
|
||||||
|
# ATTENTION
|
||||||
|
# fsync=off, synchronous_commit=off and full_page_writes=off
|
||||||
|
# gives us a major speed up during local development and testing (~30%),
|
||||||
|
# however you should NEVER use these settings in PRODUCTION unless
|
||||||
|
# you want to have CORRUPTED data.
|
||||||
|
# DO NOT COPY/PASTE THIS BLINDLY.
|
||||||
|
# YOU HAVE BEEN WARNED.
|
||||||
|
# Apply some performance improvements to pg as these guarantees are not needed while running integration tests
|
||||||
|
command: "-c 'shared_buffers=128MB' -c 'fsync=off' -c 'synchronous_commit=off' -c 'full_page_writes=off' -c 'max_connections=100' -c 'client_min_messages=warning'"
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"integresql":
|
||||||
|
image: ghcr.io/allaboutapps/integresql:v1.1.0
|
||||||
|
environment:
|
||||||
|
PGHOST: *PGHOST
|
||||||
|
PGUSER: *PGUSER
|
||||||
|
PGPASSWORD: *PGPASSWORD
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
|
|
||||||
|
"mailhog":
|
||||||
|
image: mailhog/mailhog
|
||||||
|
<<: *WHEN_BUILD_EVENT
|
||||||
30
.env.local.sample
Normal file
30
.env.local.sample
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This is a dotenv file.
|
||||||
|
# Syntax Overview: https://hexdocs.pm/dotenvy/dotenv-file-format.html
|
||||||
|
# Parser: github.com/subosito/gotenv
|
||||||
|
|
||||||
|
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
# DO NOT USE IN PRODUCTION!
|
||||||
|
# DO NOT COMMIT INTO VERSION CONTROL!
|
||||||
|
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
# Why?
|
||||||
|
# Use .env.local to easily inject secrets into your server config.
|
||||||
|
|
||||||
|
# When?
|
||||||
|
# Only use this locally, never in production.
|
||||||
|
|
||||||
|
# What?
|
||||||
|
# .env.local.sample is only a "sample" and should never hold any secret values!
|
||||||
|
# .env.local is .gitignored.
|
||||||
|
# .env.local is automatically loaded when running app <command>.
|
||||||
|
# .env.local is **not** automatically loaded when running go test / make test.
|
||||||
|
# .env.local ENV variables override OS ENV variables.
|
||||||
|
# .env.local is applied by /internal/config's DefaultServiceConfigFromEnv().
|
||||||
|
|
||||||
|
# How?
|
||||||
|
# Copy this file to `.env.local` to use it, e.g. with the following command:
|
||||||
|
# cp .env.local.sample .env.local
|
||||||
|
|
||||||
|
# Be kind to your colleagues and show them which typical ENV **keys** they
|
||||||
|
# have to set (without setting the actual value within .env.local.sample):
|
||||||
|
SECRET_KEY=
|
||||||
1
.github/README.md
vendored
Symbolic link
1
.github/README.md
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../README-go-starter.md
|
||||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Enable version updates for gomod
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
# Enable version updates for Docker
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
# Look for a `Dockerfile` in the `root` directory
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
# Enable version updates for github-actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
# Workflow files stored in the
|
||||||
|
# default location of `.github/workflows`
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
102
.github/workflows/build-test.yml
vendored
Normal file
102
.github/workflows/build-test.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
name: Build & Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: "**"
|
||||||
|
# pull_request:
|
||||||
|
# branches: [master]
|
||||||
|
# types: [opened, reopened] # avoid running twice (on push above), see https://github.com/open-telemetry/opentelemetry-python/issues/1370
|
||||||
|
env:
|
||||||
|
DOCKER_ENV_FILE: ".github/workflows/docker.env"
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
build-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: "development"
|
||||||
|
POSTGRES_USER: "dbuser"
|
||||||
|
POSTGRES_PASSWORD: "dbpass"
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
integresql:
|
||||||
|
image: ghcr.io/allaboutapps/integresql:v1.1.0
|
||||||
|
env:
|
||||||
|
PGHOST: "postgres"
|
||||||
|
PGUSER: "dbuser"
|
||||||
|
PGPASSWORD: "dbpass"
|
||||||
|
mailhog:
|
||||||
|
image: mailhog/mailhog
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: docker build (target builder)
|
||||||
|
run: DOCKER_BUILDKIT=1 docker build --target builder --file Dockerfile --tag allaboutapps.dev/aw/go-starter:builder-${GITHUB_SHA} .
|
||||||
|
- name: docker build (target app)
|
||||||
|
run: DOCKER_BUILDKIT=1 docker build --target app --file Dockerfile --tag allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} .
|
||||||
|
- name: trivy scan
|
||||||
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
image-ref: 'allaboutapps.dev/aw/go-starter:app-${{ github.sha }}'
|
||||||
|
format: 'template'
|
||||||
|
template: '@/contrib/sarif.tpl'
|
||||||
|
output: 'trivy-results.sarif'
|
||||||
|
severity: 'CRITICAL,HIGH'
|
||||||
|
ignore-unfixed: true
|
||||||
|
- name: docker run (target builder)
|
||||||
|
run: docker run -d --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" --name=builder -it allaboutapps.dev/aw/go-starter:builder-${GITHUB_SHA}
|
||||||
|
- name: "build & diff"
|
||||||
|
# Note builder stage now includes .git, thus we rm it again to again diff with the original git workspace
|
||||||
|
run: |
|
||||||
|
docker exec builder make tidy
|
||||||
|
docker exec builder make build
|
||||||
|
docker cp builder:/app ./post-build && rm -rf ./post-build/.git && git -C post-build diff --exit-code
|
||||||
|
- name: test
|
||||||
|
run: docker exec builder make test
|
||||||
|
- name: upload coverage to codecov
|
||||||
|
run: docker cp builder:/tmp/coverage.out ./coverage.out && bash <(curl -s https://codecov.io/bash)
|
||||||
|
- name: test-scripts (gsdev, go-starter only)
|
||||||
|
if: ${{ github.repository == 'allaboutapps/go-starter' }}
|
||||||
|
run: docker exec builder make test-scripts
|
||||||
|
- name: info
|
||||||
|
run: docker exec builder make info
|
||||||
|
- name: "binary: deps"
|
||||||
|
run: docker exec builder bash -c 'make get-embedded-modules-count && make get-embedded-modules'
|
||||||
|
- name: "binary: licenses"
|
||||||
|
run: docker exec builder make get-licenses
|
||||||
|
- name: docker run (target app)
|
||||||
|
run: |
|
||||||
|
docker run --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} help
|
||||||
|
docker run --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} -v
|
||||||
|
docker run --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} env
|
||||||
|
- name: upload trivy scan results to GitHub security tab
|
||||||
|
# Currently limited to master because of the following:
|
||||||
|
# Workflows triggered by Dependabot on the "push" event run with read-only access. Uploading Code Scanning results requires write access.
|
||||||
|
# To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches.
|
||||||
|
# See https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events.
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
- name: stop container
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: docker stop builder
|
||||||
|
- name: remove container
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: docker rm builder
|
||||||
|
swagger-codegen-cli:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: run the main swagger.yml validation
|
||||||
|
run: |
|
||||||
|
docker run --rm -v ${{ github.workspace }}:/local swaggerapi/swagger-codegen-cli validate -i /local/api/swagger.yml
|
||||||
75
.github/workflows/codeql-analysis.yml
vendored
Normal file
75
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Currently limited to master because of the following:
|
||||||
|
# Workflows triggered by Dependabot on the "push" event run with read-only access. Uploading Code Scanning results requires write access.
|
||||||
|
# To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches.
|
||||||
|
# See https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events.
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [master]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 19 * * 5'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Override automatic language detection by changing the below list
|
||||||
|
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||||
|
language: ['go']
|
||||||
|
# Learn more...
|
||||||
|
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2.3.4
|
||||||
|
with:
|
||||||
|
# We must fetch at least the immediate parents so that if this is
|
||||||
|
# a pull request then we can checkout the head.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
22
.github/workflows/docker.env
vendored
Normal file
22
.github/workflows/docker.env
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
CI=true
|
||||||
|
GITHUB_ACTIONS=true
|
||||||
|
# required: env for main working database, service
|
||||||
|
# default for sql-migrate (target development) and psql cli tool
|
||||||
|
PGDATABASE=development
|
||||||
|
PGUSER=dbuser
|
||||||
|
PGPASSWORD=dbpass
|
||||||
|
PGHOST=postgres
|
||||||
|
PGPORT=5432
|
||||||
|
PGSSLMODE=disable
|
||||||
|
|
||||||
|
# optional: env for sql-boiler (ability to generate models out of a "spec" database)
|
||||||
|
# sql-boiler should operate on a "spec" database only
|
||||||
|
PSQL_DBNAME=spec
|
||||||
|
PSQL_USER=dbuser
|
||||||
|
PSQL_PASS=dbpass
|
||||||
|
PSQL_HOST=postgres
|
||||||
|
PSQL_PORT=5432
|
||||||
|
PSQL_SSLMODE=disable
|
||||||
|
|
||||||
|
# optional: project root directory, used for relative path resolution (e.g. fixtures)
|
||||||
|
PROJECT_ROOT_DIR=/app
|
||||||
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# based on https://github.com/github/gitignore/blob/master/Go.gitignore
|
||||||
|
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# GOBIN
|
||||||
|
bin
|
||||||
|
|
||||||
|
# local go mod cache
|
||||||
|
.pkg
|
||||||
|
|
||||||
|
# temporary files
|
||||||
|
tmp
|
||||||
|
|
||||||
|
# mac specific
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# directory for rw files (PV mounted into the container)
|
||||||
|
# /assets/mnt folder should stay gitignored!
|
||||||
|
assets/mnt/**
|
||||||
|
!assets/mnt/.gitkeep
|
||||||
|
dumps
|
||||||
|
|
||||||
|
# go debug / delve
|
||||||
|
__debug_bin
|
||||||
|
|
||||||
|
# we support overloading ENV vars by parsing a dotenv formatted file
|
||||||
|
# this file can be used for local testing (secrets!) but should never be commited into git
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# the postAttachCommand should clean this up, ensure it's not commited anyway
|
||||||
|
.gitconfig.global
|
||||||
141
.golangci.yml
Normal file
141
.golangci.yml
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
# also lint files within /scripts. Those have "//go:build scripts" set.
|
||||||
|
build-tags:
|
||||||
|
- scripts
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- revive
|
||||||
|
- testpackage
|
||||||
|
- gosec
|
||||||
|
- usetesting
|
||||||
|
- errorlint
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
|
- canonicalheader
|
||||||
|
- containedctx
|
||||||
|
- copyloopvar
|
||||||
|
- decorder
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- dupword
|
||||||
|
- durationcheck
|
||||||
|
- errchkjson
|
||||||
|
- errname
|
||||||
|
- exptostd
|
||||||
|
- fatcontext
|
||||||
|
- forbidigo
|
||||||
|
- forcetypeassert
|
||||||
|
- funcorder
|
||||||
|
- gocheckcompilerdirectives
|
||||||
|
- gochecknoinits
|
||||||
|
- gochecksumtype
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- godox
|
||||||
|
- goheader
|
||||||
|
- gomoddirectives
|
||||||
|
- gomodguard
|
||||||
|
- goprintffuncname
|
||||||
|
- grouper
|
||||||
|
- iface
|
||||||
|
- importas
|
||||||
|
- inamedparam
|
||||||
|
- interfacebloat
|
||||||
|
- ireturn
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- misspell
|
||||||
|
- mnd
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- nilnesserr
|
||||||
|
- nilnil
|
||||||
|
- noctx
|
||||||
|
- nonamedreturns
|
||||||
|
- nosprintfhostport
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- protogetter
|
||||||
|
- reassign
|
||||||
|
- recvcheck
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- tagalign
|
||||||
|
- testableexamples
|
||||||
|
- testifylint
|
||||||
|
- thelper
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- usestdlibvars
|
||||||
|
- varnamelen
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- wrapcheck
|
||||||
|
- zerologlint
|
||||||
|
exclusions:
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
settings:
|
||||||
|
wrapcheck:
|
||||||
|
ignore-package-globs:
|
||||||
|
- "*/internal/*"
|
||||||
|
varnamelen:
|
||||||
|
ignore-names:
|
||||||
|
- tt
|
||||||
|
- t
|
||||||
|
ignore-decls:
|
||||||
|
- c echo.Context
|
||||||
|
- s *api.Server
|
||||||
|
- u *url.URL
|
||||||
|
- i int
|
||||||
|
- n int
|
||||||
|
- n int32
|
||||||
|
- tx boil.ContextExecutor
|
||||||
|
- exec boil.ContextExecutor
|
||||||
|
- db *sql.DB
|
||||||
|
- v runtime.Validatable
|
||||||
|
- r io.ReadCloser
|
||||||
|
- ok bool
|
||||||
|
- wg sync.WaitGroup
|
||||||
|
- d time.Time
|
||||||
|
- e *echo.Echo
|
||||||
|
- tx *sql.Tx
|
||||||
|
- re *regexp.Regexp
|
||||||
|
- to string
|
||||||
|
- f FixtureMap
|
||||||
|
- le *zerolog.Event
|
||||||
|
ireturn:
|
||||||
|
allow:
|
||||||
|
- anon
|
||||||
|
- error
|
||||||
|
- empty
|
||||||
|
- stdlib
|
||||||
|
- github.com/aarondl/sqlboiler/v4/queries/qm.QueryMod
|
||||||
|
- github.com/labstack/echo/v4.Context
|
||||||
|
- github.com/dropbox/godropbox/time2.Clock
|
||||||
|
godox:
|
||||||
|
keywords:
|
||||||
|
- "FIXME"
|
||||||
|
forbidigo:
|
||||||
|
forbid:
|
||||||
|
- pattern: "^(fmt\\.Print(|f|ln)|print|println)$"
|
||||||
|
- pattern: "boil\\.WithDebug"
|
||||||
|
mnd:
|
||||||
|
ignored-numbers:
|
||||||
|
- '0644'
|
||||||
|
- '0755'
|
||||||
|
ignored-files:
|
||||||
|
- internal/config/server_config.go
|
||||||
|
- fixtures.go
|
||||||
|
gocritic:
|
||||||
|
disabled-checks:
|
||||||
|
- regexpMust
|
||||||
87
.spectral.yml
Normal file
87
.spectral.yml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Spectral OpenAPI Guidelines: https://docs.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
extends: ["spectral:oas"]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Custom overrides
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
overrides:
|
||||||
|
# types that are not referenced in the swagger spec
|
||||||
|
# but are referenced in code (e.g. because parameter enums are not generated as Golang types)
|
||||||
|
- files:
|
||||||
|
- "swagger.yml#/definitions/orderDir"
|
||||||
|
rules:
|
||||||
|
oas2-unused-definition: "off"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Custom rules
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
rules:
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Rules from the spectral:oas preset, that are not needed
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
operation-tag-defined: "off"
|
||||||
|
oas2-api-host: "off"
|
||||||
|
oas2-api-schemes: "off"
|
||||||
|
info-contact: "off"
|
||||||
|
operation-description: "off"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Custom AAA Rules
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
aaa-properties-id-casing:
|
||||||
|
description: Properties ending with 'ID' must be cased as 'Id'
|
||||||
|
severity: error
|
||||||
|
recommended: true
|
||||||
|
message: "{{property}} must end with 'Id' instead of 'ID'"
|
||||||
|
given: $.definitions..properties[*]~
|
||||||
|
then:
|
||||||
|
function: pattern
|
||||||
|
functionOptions:
|
||||||
|
notMatch: "ID$"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Rules from the Adidas API Guidelines: https://github.com/adidas/api-guidelines/blob/master/.spectral.yml
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
adidas-path-parameters-camelCase-alphanumeric:
|
||||||
|
description: Path parameters MUST follow camelCase
|
||||||
|
severity: warn
|
||||||
|
recommended: true
|
||||||
|
message: "{{property}} path parameter is not camelCase: {{error}}"
|
||||||
|
given: $..parameters[?(@.in == 'path')].name
|
||||||
|
then:
|
||||||
|
function: pattern
|
||||||
|
functionOptions:
|
||||||
|
match: "^[a-z][a-zA-Z0-9]+$"
|
||||||
|
|
||||||
|
adidas-definitions-camelCase-alphanumeric:
|
||||||
|
description: All YAML/JSON definitions MUST follow fields-camelCase and be ASCII alphanumeric characters or `_` or `$`.
|
||||||
|
severity: error
|
||||||
|
recommended: true
|
||||||
|
message: "{{property}} MUST follow camelCase and be ASCII alphanumeric characters or `_` or `$`."
|
||||||
|
given: $.definitions[*]~
|
||||||
|
then:
|
||||||
|
function: pattern
|
||||||
|
functionOptions:
|
||||||
|
match: "/^[a-z$_]{1}[A-Z09$_]*/"
|
||||||
|
|
||||||
|
adidas-properties-camelCase-alphanumeric:
|
||||||
|
description: All JSON Schema properties MUST follow fields-camelCase and be ASCII alphanumeric characters or _ or $.
|
||||||
|
severity: error
|
||||||
|
recommended: true
|
||||||
|
message: "{{property}} MUST follow camelCase and be ASCII alphanumeric characters or _ or $."
|
||||||
|
given: $.definitions..properties[*]~
|
||||||
|
then:
|
||||||
|
function: pattern
|
||||||
|
functionOptions:
|
||||||
|
match: "/^[a-z$]{1}[A-Z09$]*/"
|
||||||
|
|
||||||
|
adidas-request-GET-no-body:
|
||||||
|
description: "A 'GET' request MUST NOT accept a 'body` parameter"
|
||||||
|
severity: error
|
||||||
|
given: $.paths..get.parameters..in
|
||||||
|
then:
|
||||||
|
function: pattern
|
||||||
|
functionOptions:
|
||||||
|
notMatch: "/^body$/"
|
||||||
0
.trivyignore
Normal file
0
.trivyignore
Normal file
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
||||||
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch app server",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}",
|
||||||
|
"env": {},
|
||||||
|
"args": [
|
||||||
|
"server"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch file",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${fileDirname}",
|
||||||
|
"env": {},
|
||||||
|
"args": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch file and update snapshots",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${fileDirname}",
|
||||||
|
"env": {
|
||||||
|
"TEST_UPDATE_GOLDEN": true
|
||||||
|
},
|
||||||
|
"args": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
.vscode/tasks.json
vendored
Normal file
16
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "make",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
276
Dockerfile
Normal file
276
Dockerfile
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
### -----------------------
|
||||||
|
# --- Stage: development
|
||||||
|
# --- Purpose: Local development environment
|
||||||
|
# --- https://hub.docker.com/_/golang
|
||||||
|
# --- https://github.com/microsoft/vscode-remote-try-go/blob/master/.devcontainer/Dockerfile
|
||||||
|
### -----------------------
|
||||||
|
FROM golang:1.25.0-bookworm AS development
|
||||||
|
|
||||||
|
# Avoid warnings by switching to noninteractive
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Our Makefile / env fully supports parallel job execution
|
||||||
|
ENV MAKEFLAGS "-j 8 --no-print-directory"
|
||||||
|
|
||||||
|
# postgresql-support: Add the official postgres repo to install the matching postgresql-client tools of your stack
|
||||||
|
# https://wiki.postgresql.org/wiki/Apt
|
||||||
|
# run lsb_release -c inside the container to pick the proper repository flavor
|
||||||
|
# e.g. stretch=>stretch-pgdg, buster=>buster-pgdg, bullseye=>bullseye-pgdg, bookworm=>bookworm-pgdg
|
||||||
|
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bookworm-pgdg main" \
|
||||||
|
| tee /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||||
|
| apt-key add -
|
||||||
|
|
||||||
|
# Setup GPG key to install Trivy to locally scan for vulnerabilities
|
||||||
|
RUN mkdir -m 0755 -p /etc/apt/keyrings/ \
|
||||||
|
&& wget -O- https://aquasecurity.github.io/trivy-repo/deb/public.key | \
|
||||||
|
gpg --dearmor | \
|
||||||
|
tee /etc/apt/keyrings/trivy.gpg > /dev/null; \
|
||||||
|
chmod 644 /etc/apt/keyrings/trivy.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb bookworm main" \
|
||||||
|
| tee /etc/apt/sources.list.d/trivy.list
|
||||||
|
|
||||||
|
# Install required system dependencies
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y \
|
||||||
|
#
|
||||||
|
# Mandadory minimal linux packages
|
||||||
|
# Installed at development stage and app stage
|
||||||
|
# Do not forget to add mandadory linux packages to the final app Dockerfile stage below!
|
||||||
|
#
|
||||||
|
# -- START MANDADORY --
|
||||||
|
ca-certificates \
|
||||||
|
# --- END MANDADORY ---
|
||||||
|
#
|
||||||
|
# Development specific packages
|
||||||
|
# Only installed at development stage and NOT available in the final Docker stage
|
||||||
|
# based upon
|
||||||
|
# https://github.com/microsoft/vscode-remote-try-go/blob/master/.devcontainer/Dockerfile
|
||||||
|
# https://raw.githubusercontent.com/microsoft/vscode-dev-containers/master/script-library/common-debian.sh
|
||||||
|
#
|
||||||
|
# icu-devtools: https://stackoverflow.com/questions/58736399/how-to-get-vscode-liveshare-extension-working-when-running-inside-vscode-remote
|
||||||
|
# graphviz: https://github.com/google/pprof#building-pprof
|
||||||
|
# -- START DEVELOPMENT --
|
||||||
|
apt-utils \
|
||||||
|
dialog \
|
||||||
|
openssh-client \
|
||||||
|
less \
|
||||||
|
iproute2 \
|
||||||
|
procps \
|
||||||
|
lsb-release \
|
||||||
|
locales \
|
||||||
|
sudo \
|
||||||
|
bash-completion \
|
||||||
|
bsdmainutils \
|
||||||
|
graphviz \
|
||||||
|
xz-utils \
|
||||||
|
postgresql-client-17 \
|
||||||
|
icu-devtools \
|
||||||
|
tmux \
|
||||||
|
rsync \
|
||||||
|
trivy \
|
||||||
|
# --- END DEVELOPMENT ---
|
||||||
|
#
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# env/vscode support: LANG must be supported, requires installing the locale package first
|
||||||
|
# https://github.com/Microsoft/vscode/issues/58015
|
||||||
|
# https://stackoverflow.com/questions/28405902/how-to-set-the-locale-inside-a-debian-ubuntu-docker-container
|
||||||
|
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||||
|
dpkg-reconfigure --frontend=noninteractive locales && \
|
||||||
|
update-locale LANG=en_US.UTF-8
|
||||||
|
|
||||||
|
ENV LANG en_US.UTF-8
|
||||||
|
|
||||||
|
# sql pgFormatter: Integrates with vscode-pgFormatter (we pin pgFormatter.pgFormatterPath for the extension to this version)
|
||||||
|
# requires perl to be installed
|
||||||
|
# https://github.com/bradymholt/vscode-pgFormatter/commits/master
|
||||||
|
# https://github.com/darold/pgFormatter/releases
|
||||||
|
RUN mkdir -p /tmp/pgFormatter \
|
||||||
|
&& cd /tmp/pgFormatter \
|
||||||
|
&& wget https://github.com/darold/pgFormatter/archive/v5.5.tar.gz \
|
||||||
|
&& tar xzf v5.5.tar.gz \
|
||||||
|
&& cd pgFormatter-5.5 \
|
||||||
|
&& perl Makefile.PL \
|
||||||
|
&& make && make install \
|
||||||
|
&& rm -rf /tmp/pgFormatter
|
||||||
|
|
||||||
|
# go gotestsum: (this package should NOT be installed via go get)
|
||||||
|
# https://github.com/gotestyourself/gotestsum/releases
|
||||||
|
RUN mkdir -p /tmp/gotestsum \
|
||||||
|
&& cd /tmp/gotestsum \
|
||||||
|
&& ARCH="$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)" \
|
||||||
|
&& wget "https://github.com/gotestyourself/gotestsum/releases/download/v1.12.3/gotestsum_1.12.3_linux_${ARCH}.tar.gz" \
|
||||||
|
&& tar xzf "gotestsum_1.12.3_linux_${ARCH}.tar.gz" \
|
||||||
|
&& cp gotestsum /usr/local/bin/gotestsum \
|
||||||
|
&& rm -rf /tmp/gotestsum
|
||||||
|
|
||||||
|
# go linting: (this package should NOT be installed via go get)
|
||||||
|
# https://github.com/golangci/golangci-lint#binary
|
||||||
|
# https://github.com/golangci/golangci-lint/releases
|
||||||
|
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \
|
||||||
|
| sh -s -- -b $(go env GOPATH)/bin v2.4.0
|
||||||
|
|
||||||
|
# go swagger: (this package should NOT be installed via go get)
|
||||||
|
# https://github.com/go-swagger/go-swagger/releases
|
||||||
|
RUN ARCH="$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)" \
|
||||||
|
&& curl -o /usr/local/bin/swagger -L'#' \
|
||||||
|
"https://github.com/go-swagger/go-swagger/releases/download/v0.29.0/swagger_linux_${ARCH}" \
|
||||||
|
&& chmod +x /usr/local/bin/swagger
|
||||||
|
|
||||||
|
# lichen: go license util
|
||||||
|
# TODO: Install from static binary as soon as it becomes available.
|
||||||
|
# https://github.com/uw-labs/lichen/tags
|
||||||
|
RUN go install github.com/uw-labs/lichen@v0.1.7
|
||||||
|
|
||||||
|
# cobra-cli: cobra cmd scaffolding generator
|
||||||
|
# TODO: Install from static binary as soon as it becomes available.
|
||||||
|
# https://github.com/spf13/cobra-cli/releases
|
||||||
|
RUN go install github.com/spf13/cobra-cli@v1.3.0
|
||||||
|
|
||||||
|
# govulncheck: go vulnerability checker
|
||||||
|
RUN go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
|
||||||
|
# changie: a tool to help manage changelogs
|
||||||
|
RUN go install github.com/miniscruff/changie@v1.21.0
|
||||||
|
|
||||||
|
# watchexec
|
||||||
|
# https://github.com/watchexec/watchexec/releases
|
||||||
|
RUN mkdir -p /tmp/watchexec \
|
||||||
|
&& cd /tmp/watchexec \
|
||||||
|
&& wget https://github.com/watchexec/watchexec/releases/download/v1.25.1/watchexec-1.25.1-$(arch)-unknown-linux-musl.tar.xz \
|
||||||
|
&& tar xf watchexec-1.25.1-$(arch)-unknown-linux-musl.tar.xz \
|
||||||
|
&& cp watchexec-1.25.1-$(arch)-unknown-linux-musl/watchexec /usr/local/bin/watchexec \
|
||||||
|
&& rm -rf /tmp/watchexec
|
||||||
|
|
||||||
|
# yq
|
||||||
|
# https://github.com/mikefarah/yq/releases
|
||||||
|
RUN mkdir -p /tmp/yq \
|
||||||
|
&& cd /tmp/yq \
|
||||||
|
&& ARCH="$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)" \
|
||||||
|
&& wget "https://github.com/mikefarah/yq/releases/download/v4.40.5/yq_linux_${ARCH}.tar.gz" \
|
||||||
|
&& tar xzf "yq_linux_${ARCH}.tar.gz" \
|
||||||
|
&& cp "yq_linux_${ARCH}" /usr/local/bin/yq \
|
||||||
|
&& rm -rf /tmp/yq
|
||||||
|
|
||||||
|
# spectral: OpenAPI / JSON schema linter
|
||||||
|
# https://github.com/stoplightio/spectral
|
||||||
|
RUN curl -L https://raw.github.com/stoplightio/spectral/master/scripts/install.sh | sh
|
||||||
|
|
||||||
|
# gsdev
|
||||||
|
# The sole purpose of the "gsdev" cli util is to provide a handy short command for the following (all args are passed):
|
||||||
|
# go run -tags scripts /app/scripts/main.go "$@"
|
||||||
|
RUN printf '#!/bin/bash\nset -Eeo pipefail\ncd /app && go run -tags scripts ./scripts/main.go "$@"' > /usr/bin/gsdev && chmod 755 /usr/bin/gsdev
|
||||||
|
|
||||||
|
# linux permissions / vscode support: Add user to avoid linux file permission issues
|
||||||
|
# Detail: Inside the container, any mounted files/folders will have the exact same permissions
|
||||||
|
# as outside the container - including the owner user ID (UID) and group ID (GID).
|
||||||
|
# Because of this, your container user will either need to have the same UID or be in a group with the same GID.
|
||||||
|
# The actual name of the user / group does not matter. The first user on a machine typically gets a UID of 1000,
|
||||||
|
# so most containers use this as the ID of the user to try to avoid this problem.
|
||||||
|
# 2020-04: docker-compose does not support passing id -u / id -g as part of its config, therefore we assume uid 1000
|
||||||
|
# https://code.visualstudio.com/docs/remote/containers-advanced#_adding-a-nonroot-user-to-your-dev-container
|
||||||
|
# https://code.visualstudio.com/docs/remote/containers-advanced#_creating-a-nonroot-user
|
||||||
|
ARG USERNAME=development
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=$USER_UID
|
||||||
|
|
||||||
|
RUN groupadd --gid $USER_GID $USERNAME \
|
||||||
|
&& useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
|
||||||
|
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/$USERNAME
|
||||||
|
|
||||||
|
# vscode support: cached extensions install directory
|
||||||
|
# https://code.visualstudio.com/docs/remote/containers-advanced#_avoiding-extension-reinstalls-on-container-rebuild
|
||||||
|
RUN mkdir -p /home/$USERNAME/.vscode-server/extensions \
|
||||||
|
/home/$USERNAME/.vscode-server-insiders/extensions \
|
||||||
|
&& chown -R $USERNAME \
|
||||||
|
/home/$USERNAME/.vscode-server \
|
||||||
|
/home/$USERNAME/.vscode-server-insiders
|
||||||
|
|
||||||
|
# linux permissions / vscode support: chown $GOPATH so $USERNAME can directly work with it
|
||||||
|
# Note that this should be the final step after installing all build deps
|
||||||
|
RUN mkdir -p /$GOPATH/pkg && chown -R $USERNAME /$GOPATH
|
||||||
|
|
||||||
|
# https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history
|
||||||
|
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" \
|
||||||
|
&& mkdir /home/$USERNAME/commandhistory \
|
||||||
|
&& touch /home/$USERNAME/commandhistory/.bash_history \
|
||||||
|
&& chown -R $USERNAME /home/$USERNAME/commandhistory \
|
||||||
|
&& echo "$SNIPPET" >> "/home/$USERNAME/.bashrc"
|
||||||
|
|
||||||
|
# $GOBIN is where our own compiled binaries will live and other go.mod / VSCode binaries will be installed.
|
||||||
|
# It should always come AFTER our other $PATH segments and should be earliest targeted in stage "builder",
|
||||||
|
# as /app/bin will the shadowed by a volume mount via docker-compose!
|
||||||
|
# E.g. "which golangci-lint" should report "/go/bin" not "/app/bin" (where VSCode will place it).
|
||||||
|
# https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md#walk-through
|
||||||
|
WORKDIR /app
|
||||||
|
ENV GOBIN /app/bin
|
||||||
|
ENV PATH $PATH:$GOBIN
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Stage: builder
|
||||||
|
# --- Purpose: Statically built binaries and CI environment
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
FROM development AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Makefile /app/Makefile
|
||||||
|
COPY --chmod=0755 rksh /app/rksh
|
||||||
|
COPY go.mod /app/go.mod
|
||||||
|
COPY go.sum /app/go.sum
|
||||||
|
RUN make modules
|
||||||
|
RUN make tools
|
||||||
|
COPY . /app/
|
||||||
|
RUN make go-build
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Stage: app
|
||||||
|
# --- Purpose: Image for actual deployment
|
||||||
|
# --- Prefer https://github.com/GoogleContainerTools/distroless over
|
||||||
|
# --- debian:buster-slim https://hub.docker.com/_/debian (if you need apt-get).
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
# Distroless images are minimal and lack shell access.
|
||||||
|
# https://github.com/GoogleContainerTools/distroless/blob/master/base/README.md
|
||||||
|
# The :debug image provides a busybox shell to enter (base-debian10 only, not static).
|
||||||
|
# https://github.com/GoogleContainerTools/distroless#debug-images
|
||||||
|
FROM gcr.io/distroless/base-debian12:debug AS app
|
||||||
|
|
||||||
|
# FROM debian:bookworm-slim AS app
|
||||||
|
# RUN apt-get update \
|
||||||
|
# && apt-get install -y \
|
||||||
|
# #
|
||||||
|
# # Mandadory minimal linux packages
|
||||||
|
# # Installed at development stage and app stage
|
||||||
|
# # Do not forget to add mandadory linux packages to the base development Dockerfile stage above!
|
||||||
|
# #
|
||||||
|
# # -- START MANDADORY --
|
||||||
|
# ca-certificates \
|
||||||
|
# # --- END MANDADORY ---
|
||||||
|
# #
|
||||||
|
# && apt-get clean \
|
||||||
|
# && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /app/bin/app /app/
|
||||||
|
COPY --from=builder /app/api/swagger.yml /app/api/
|
||||||
|
COPY --from=builder /app/assets /app/assets/
|
||||||
|
COPY --from=builder /app/migrations /app/migrations/
|
||||||
|
COPY --from=builder /app/web /app/web/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Must comply to vector form
|
||||||
|
# https://github.com/GoogleContainerTools/distroless#entrypoints
|
||||||
|
# Sample usage of this image:
|
||||||
|
# docker run <image> help
|
||||||
|
# docker run <image> db migrate
|
||||||
|
# docker run <image> db seed
|
||||||
|
# docker run <image> env
|
||||||
|
# docker run <image> probe readiness
|
||||||
|
# docker run <image> probe liveness
|
||||||
|
# docker run <image> server
|
||||||
|
# docker run <image> server --migrate
|
||||||
|
ENTRYPOINT ["/app/app"]
|
||||||
|
CMD ["server", "--migrate"]
|
||||||
494
Makefile
Normal file
494
Makefile
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
### -----------------------
|
||||||
|
# --- Building
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
# first is default target when running "make" without args
|
||||||
|
build: ##- Default 'make' target: sql, swagger, go-generate-handlers, go-format, go-build and lint.
|
||||||
|
@$(MAKE) build-pre
|
||||||
|
@$(MAKE) go-format
|
||||||
|
@$(MAKE) go-build
|
||||||
|
@$(MAKE) lint
|
||||||
|
|
||||||
|
# useful to ensure that everything gets resetuped from scratch
|
||||||
|
all: clean init ##- Runs all of our common make targets: clean, init, build and test.
|
||||||
|
@$(MAKE) build
|
||||||
|
@$(MAKE) test
|
||||||
|
@$(MAKE) trivy
|
||||||
|
|
||||||
|
info: info-db info-handlers info-go ##- Prints info about spec db, handlers, and go.mod updates, module-name and current go version.
|
||||||
|
|
||||||
|
info-db: ##- (opt) Prints info about spec db.
|
||||||
|
@echo "[spec DB]" > tmp/.info-db
|
||||||
|
@cat scripts/sql/info.sql | psql -q -d "${PSQL_DBNAME}" >> tmp/.info-db
|
||||||
|
@cat tmp/.info-db
|
||||||
|
|
||||||
|
info-handlers: ##- (opt) Prints info about handlers.
|
||||||
|
@echo "[handlers]" > tmp/.info-handlers
|
||||||
|
@gsdev handlers check --print-all >> tmp/.info-handlers
|
||||||
|
@echo "" >> tmp/.info-handlers
|
||||||
|
@cat tmp/.info-handlers
|
||||||
|
|
||||||
|
info-go: ##- (opt) Prints go.mod updates, module-name and current go version.
|
||||||
|
@echo "[go.mod]" > tmp/.info-go
|
||||||
|
@$(MAKE) get-go-outdated-modules >> tmp/.info-go
|
||||||
|
@$(MAKE) info-module-name >> tmp/.info-go
|
||||||
|
@go version >> tmp/.info-go
|
||||||
|
@cat tmp/.info-go
|
||||||
|
|
||||||
|
lint: check-gen-dirs check-script-dir check-handlers check-embedded-modules-go-not go-lint ##- Runs golangci-lint and make check-*.
|
||||||
|
|
||||||
|
# these recipies may execute in parallel
|
||||||
|
build-pre: sql swagger go-generate ##- (opt) Runs pre-build related targets (sql, swagger, go-generate-handlers, go-generate).
|
||||||
|
@$(MAKE) go-generate-handlers
|
||||||
|
|
||||||
|
go-format: ##- (opt) Runs go format.
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
go-build: ##- (opt) Runs go build.
|
||||||
|
go build -ldflags $(LDFLAGS) -o bin/app
|
||||||
|
|
||||||
|
go-lint: ##- (opt) Runs golangci-lint.
|
||||||
|
golangci-lint run --timeout 5m
|
||||||
|
|
||||||
|
go-generate-handlers: ##- (opt) Generates the internal/api/handlers/handlers.go binding.
|
||||||
|
gsdev handlers gen
|
||||||
|
|
||||||
|
go-generate: ##- Runs go generate.
|
||||||
|
@go generate ./...
|
||||||
|
|
||||||
|
check-handlers: ##- (opt) Checks if implemented handlers match their spec (path).
|
||||||
|
gsdev handlers check
|
||||||
|
|
||||||
|
# https://golang.org/pkg/cmd/go/internal/generate/
|
||||||
|
# To convey to humans and machine tools that code is generated,
|
||||||
|
# generated source should have a line that matches the following
|
||||||
|
# regular expression (in Go syntax):
|
||||||
|
# ^// Code generated .* DO NOT EDIT\.$
|
||||||
|
check-gen-dirs: ##- (opt) Ensures internal/models|types only hold generated files.
|
||||||
|
@echo "make check-gen-dirs"
|
||||||
|
@find ./internal/types -name ".*" -prune -o -type f -print | xargs -L1 grep -L '// Code generated .* DO NOT EDIT\.' \
|
||||||
|
|| (echo "Error: Non generated file(s) in ./internal/types!" && exit 1)
|
||||||
|
@find ./internal/models -name ".*" -prune -o -type f -print | xargs -L1 grep -L '// Code generated .* DO NOT EDIT\.' \
|
||||||
|
|| (echo "Error: Non generated file(s) in ./internal/models!" && exit 1)
|
||||||
|
|
||||||
|
check-script-dir: ##- (opt) Ensures all scripts/**/*.go files have the "//go:build scripts" build tag set.
|
||||||
|
@echo "make check-script-dir"
|
||||||
|
@find ./scripts -type f -name '*.go' | xargs -L1 grep -L '//go:build scripts' || (echo "Error: Found unset '//go:build scripts' in ./scripts/**/*.go!" && exit 1)
|
||||||
|
|
||||||
|
# https://github.com/gotestyourself/gotestsum#format
|
||||||
|
# w/o cache https://github.com/golang/go/issues/24573 - see "go help testflag"
|
||||||
|
# note that these tests should not run verbose by default (e.g. use your IDE for this)
|
||||||
|
# TODO: add test shuffling/seeding when landed in go v1.15 (https://github.com/golang/go/issues/28592)
|
||||||
|
# tests by pkgname
|
||||||
|
test: ##- Run tests, output by package, print coverage.
|
||||||
|
@$(MAKE) go-test-by-pkg
|
||||||
|
@$(MAKE) go-test-print-coverage
|
||||||
|
|
||||||
|
# tests by testname
|
||||||
|
test-by-name: ##- Run tests, output by testname, print coverage.
|
||||||
|
@$(MAKE) go-test-by-name
|
||||||
|
@$(MAKE) go-test-print-coverage
|
||||||
|
|
||||||
|
test-update-golden: ##- Refreshes all golden files / snapshot tests by running tests, output by package.
|
||||||
|
@echo "Attempting to refresh all golden files / snapshot tests (TEST_UPDATE_GOLDEN=true)!"
|
||||||
|
@echo -n "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ]
|
||||||
|
@TEST_UPDATE_GOLDEN=true gotestsum --hide-summary=skipped -- -race -count=1 ./...
|
||||||
|
|
||||||
|
# note that we explicitly don't want to use a -coverpkg=./... option, per pkg coverage take precedence
|
||||||
|
go-test-by-pkg: ##- (opt) Run tests, output by package.
|
||||||
|
gotestsum --format pkgname-and-test-fails --format-hide-empty-pkg --jsonfile /tmp/test.log -- -race -cover -count=1 -coverprofile=/tmp/coverage.out ./...
|
||||||
|
|
||||||
|
go-test-by-name: ##- (opt) Run tests, output by testname.
|
||||||
|
gotestsum --format testname --jsonfile /tmp/test.log -- -race -cover -count=1 -coverprofile=/tmp/coverage.out ./...
|
||||||
|
|
||||||
|
go-test-print-coverage: ##- (opt) Print overall test coverage (must be done after running tests).
|
||||||
|
@printf "coverage "
|
||||||
|
@go tool cover -func=/tmp/coverage.out | tail -n 1 | awk '{$$1=$$1;print}'
|
||||||
|
|
||||||
|
go-test-print-slowest: ##- Print slowest running tests (must be done after running tests).
|
||||||
|
gotestsum tool slowest --jsonfile /tmp/test.log --threshold 2s
|
||||||
|
|
||||||
|
# TODO: switch to "-m direct" after go 1.17 hits: https://github.com/golang/go/issues/40364
|
||||||
|
get-go-outdated-modules: ##- (opt) Prints outdated (direct) go modules (from go.mod).
|
||||||
|
@((go list -u -m -f '{{if and .Update (not .Indirect)}}{{.}}{{end}}' all) 2>/dev/null | grep " ") || echo "go modules are up-to-date."
|
||||||
|
|
||||||
|
watch-tests: ##- Watches *.go files and runs package tests on modifications.
|
||||||
|
gotestsum --format testname --watch -- -race -count=1
|
||||||
|
|
||||||
|
test-scripts: ##- (opt) Run scripts tests (gsdev), output by package, print coverage.
|
||||||
|
@$(MAKE) go-test-scripts-by-pkg
|
||||||
|
@printf "coverage "
|
||||||
|
@go tool cover -func=/tmp/coverage-scripts.out | tail -n 1 | awk '{$$1=$$1;print}'
|
||||||
|
|
||||||
|
go-test-scripts-by-pkg: ##- (opt) Run scripts tests (gsdev), output by package.
|
||||||
|
gotestsum --format pkgname-and-test-fails --jsonfile /tmp/test.log -- $$(go list -tags scripts ./... | grep "${GO_MODULE_NAME}/scripts") -tags scripts -race -cover -count=1 -coverprofile=/tmp/coverage-scripts.out ./...
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Initializing
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
init: ##- Runs make modules, tools and tidy.
|
||||||
|
@$(MAKE) modules
|
||||||
|
@$(MAKE) tools
|
||||||
|
@$(MAKE) tidy
|
||||||
|
|
||||||
|
# cache go modules (locally into .pkg)
|
||||||
|
modules: ##- (opt) Cache packages as specified in go.mod.
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
tools: ##- (opt) Install tools.
|
||||||
|
@go install tool
|
||||||
|
|
||||||
|
tidy: ##- (opt) Tidy our go.sum file.
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- SQL
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
sql-reset: ##- Wizard to drop and create our development database.
|
||||||
|
@echo "DROP & CREATE database:"
|
||||||
|
@echo " PGHOST=${PGHOST} PGDATABASE=${PGDATABASE}" PGUSER=${PGUSER}
|
||||||
|
@echo -n "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ]
|
||||||
|
psql -d postgres -c 'DROP DATABASE IF EXISTS "${PGDATABASE}";'
|
||||||
|
psql -d postgres -c 'CREATE DATABASE "${PGDATABASE}" WITH OWNER ${PGUSER} TEMPLATE "template0";'
|
||||||
|
|
||||||
|
sql-drop-all: ##- Wizard to drop ALL databases: spec, development and tracked by integresql.
|
||||||
|
@echo "DROP ALL:"
|
||||||
|
TO_DROP=$$(psql -qtz0 -d postgres -c "SELECT 'DROP DATABASE \"' || datname || '\";' FROM pg_database WHERE datistemplate = FALSE AND datname != 'postgres';")
|
||||||
|
@echo "$$TO_DROP"
|
||||||
|
@echo -n "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ]
|
||||||
|
@echo "Resetting integresql..."
|
||||||
|
curl --fail -X DELETE http://integresql:5000/api/v1/admin/templates
|
||||||
|
@echo "Drop databases..."
|
||||||
|
echo $$TO_DROP | psql -tz0 -d postgres
|
||||||
|
@echo "Done. Please run 'make sql-reset && make sql-spec-reset && make sql-spec-migrate' to reinitialize."
|
||||||
|
|
||||||
|
# This step is only required to be executed when the "migrations" folder has changed!
|
||||||
|
sql: ##- Runs sql format, all sql related checks and finally generates internal/models/*.go.
|
||||||
|
@$(MAKE) sql-format
|
||||||
|
@$(MAKE) sql-regenerate
|
||||||
|
|
||||||
|
sql-regenerate: ##- (opt) Runs sql related checks and finally generates internal/models/*.go.
|
||||||
|
@$(MAKE) sql-check-files
|
||||||
|
@$(MAKE) sql-spec-reset
|
||||||
|
@$(MAKE) sql-spec-migrate
|
||||||
|
@$(MAKE) sql-check-and-generate
|
||||||
|
|
||||||
|
sql-check-and-generate: sql-check-structure sql-boiler ##- (opt) Runs make sql-check-structure and sql-boiler.
|
||||||
|
|
||||||
|
sql-boiler: ##- (opt) Runs sql-boiler introspects the spec db to generate internal/models/*.go.
|
||||||
|
@echo "make sql-boiler"
|
||||||
|
sqlboiler psql
|
||||||
|
|
||||||
|
sql-format: ##- (opt) Formats all *.sql files.
|
||||||
|
@echo "make sql-format"
|
||||||
|
@find ${PWD} -path "*/tmp/*" -prune -name ".*" -prune -o -type f -iname "*.sql" -print \
|
||||||
|
| grep --invert "/app/dumps/" \
|
||||||
|
| grep --invert "/app/test/" \
|
||||||
|
| xargs -i pg_format --inplace {}
|
||||||
|
|
||||||
|
sql-check-files: sql-check-syntax sql-check-migrations-unnecessary-null ##- (opt) Check syntax and unnecessary use of NULL keyword.
|
||||||
|
|
||||||
|
# check syntax via the real database
|
||||||
|
# https://stackoverflow.com/questions/8271606/postgresql-syntax-check-without-running-the-query
|
||||||
|
sql-check-syntax: ##- (opt) Checks syntax of all *.sql files.
|
||||||
|
@echo "make sql-check-syntax"
|
||||||
|
@find ${PWD} -path "*/tmp/*" -prune -name ".*" -prune -path ./dumps -prune -false -o -type f -iname "*.sql" -print \
|
||||||
|
| grep --invert "/app/dumps/" \
|
||||||
|
| grep --invert "/app/test/" \
|
||||||
|
| xargs -i sed '1s#^#DO $$SYNTAX_CHECK$$ BEGIN RETURN;#; $$aEND; $$SYNTAX_CHECK$$;' {} \
|
||||||
|
| psql -d postgres --quiet -v ON_ERROR_STOP=1
|
||||||
|
|
||||||
|
sql-check-migrations-unnecessary-null: ##- (opt) Checks migrations/*.sql for unnecessary use of NULL keywords.
|
||||||
|
@echo "make sql-check-migrations-unnecessary-null"
|
||||||
|
@(grep -R "NULL" ./migrations/ | grep --invert "DEFAULT NULL" | grep --invert "NOT NULL" | grep --invert "WITH NULL" | grep --invert "NULL, " | grep --invert ", NULL" | grep --invert "RETURN NULL" | grep --invert "SET NULL") \
|
||||||
|
&& exit 1 || exit 0
|
||||||
|
|
||||||
|
sql-spec-reset: ##- (opt) Drop and creates our spec database.
|
||||||
|
@echo "make sql-spec-reset"
|
||||||
|
@psql --quiet -d postgres -c 'DROP DATABASE IF EXISTS "${PSQL_DBNAME}";'
|
||||||
|
@psql --quiet -d postgres -c 'CREATE DATABASE "${PSQL_DBNAME}" WITH OWNER ${PSQL_USER} TEMPLATE "template0";'
|
||||||
|
|
||||||
|
sql-spec-migrate: ##- (opt) Applies migrations/*.sql to our spec database.
|
||||||
|
@echo "make sql-spec-migrate"
|
||||||
|
@sql-migrate up -env spec | xargs -i echo "[spec DB]" {}
|
||||||
|
|
||||||
|
sql-check-structure: sql-check-structure-fk-missing-index sql-check-structure-default-zero-values ##- (opt) Runs make sql-check-structure-*.
|
||||||
|
|
||||||
|
sql-check-structure-fk-missing-index: ##- (opt) Ensures spec database objects have FK-indices set.
|
||||||
|
@echo "make sql-check-structure-fk-missing-index"
|
||||||
|
@cat scripts/sql/fk_missing_index.sql | psql -qtz0 --no-align -d "${PSQL_DBNAME}" -v ON_ERROR_STOP=1
|
||||||
|
|
||||||
|
sql-check-structure-default-zero-values: ##- (opt) Ensures spec database objects default values match go zero values.
|
||||||
|
@echo "make sql-check-structure-default-zero-values"
|
||||||
|
@cat scripts/sql/default_zero_values.sql | psql -qtz0 --no-align -d "${PSQL_DBNAME}" -v ON_ERROR_STOP=1
|
||||||
|
|
||||||
|
dumpfile := /app/dumps/development_$(shell date '+%Y-%m-%d-%H-%M-%S').sql
|
||||||
|
sql-dump: ##- Dumps the development database to '/app/dumps/development_YYYY-MM-DD-hh-mm-ss.sql'.
|
||||||
|
@mkdir -p /app/dumps
|
||||||
|
@pg_dump development --format=p --clean --if-exists > $(dumpfile)
|
||||||
|
@echo "Dumped '$(dumpfile)'. Use 'cat $(dumpfile) | psql' to restore"
|
||||||
|
|
||||||
|
watch-sql: ##- Watches *.sql files in /migrations and runs 'make sql-regenerate' on modifications.
|
||||||
|
@echo Watching /migrations. Use Ctrl-c to stop a run or exit.
|
||||||
|
watchexec -p -w migrations --exts sql $(MAKE) sql-regenerate
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Swagger
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
swagger: ##- Runs make swagger-concat and swagger-server.
|
||||||
|
@$(MAKE) swagger-concat
|
||||||
|
@$(MAKE) swagger-server
|
||||||
|
|
||||||
|
# Any sibling elements of a $ref are ignored. This is because $ref works by replacing itself and everything on its level with the definition it is pointing at.
|
||||||
|
# https://swagger.io/docs/specification/using-ref/
|
||||||
|
swagger-lint-ref-siblings: ##- (opt) Checks api/**/*.[yml|yaml] for invalid usage of $ref (no siblings).
|
||||||
|
@echo "make swagger-lint-ref-siblings"
|
||||||
|
@rm -f /tmp/swagger-lint-ref-siblings-errors.log && touch /tmp/swagger-lint-ref-siblings-errors.log
|
||||||
|
@find api -type f -name "*.yml" -o -name "*.yaml" \
|
||||||
|
| { \
|
||||||
|
while read ymlfile; \
|
||||||
|
do \
|
||||||
|
ref_siblings=$$(yq e '.. | select(has("$$ref") and length != 1)' $$ymlfile); \
|
||||||
|
([[ -z "$$ref_siblings" ]] \
|
||||||
|
|| (echo "Error: Found invalid \$$ref siblings within $$ymlfile:" \
|
||||||
|
&& (yq -P e '[.. | select(has("$$ref") and length != 1)]' $$ymlfile) \
|
||||||
|
&& (echo $$ymlfile >> /tmp/swagger-lint-ref-siblings-errors.log))); \
|
||||||
|
done \
|
||||||
|
};
|
||||||
|
@[[ "$$(cat /tmp/swagger-lint-ref-siblings-errors.log | wc -l)" -eq "0" ]] \
|
||||||
|
|| (echo "Error: $$(cat /tmp/swagger-lint-ref-siblings-errors.log | wc -l) files have \$$ref(s) with siblings!" \
|
||||||
|
&& false)
|
||||||
|
|
||||||
|
swagger-lint:
|
||||||
|
@echo "make swagger-lint"
|
||||||
|
@spectral lint --fail-severity=warn api/swagger.yml
|
||||||
|
|
||||||
|
# https://goswagger.io/usage/mixin.html
|
||||||
|
# https://goswagger.io/usage/flatten.html
|
||||||
|
swagger-concat: ##- (opt) Regenerates api/swagger.yml based on api/paths/*.
|
||||||
|
@echo "make swagger-concat"
|
||||||
|
@mkdir -p api/tmp
|
||||||
|
@rm -rf api/tmp/*
|
||||||
|
@swagger mixin \
|
||||||
|
--output=api/tmp/tmp.yml \
|
||||||
|
--format=yaml \
|
||||||
|
--keep-spec-order \
|
||||||
|
api/config/main.yml api/paths/* \
|
||||||
|
-q
|
||||||
|
@swagger flatten api/tmp/tmp.yml \
|
||||||
|
--output=api/swagger.yml \
|
||||||
|
--format=yaml \
|
||||||
|
-q
|
||||||
|
@sed -i '1s@^@# // Code generated by "make swagger"; DO NOT EDIT.\n@' api/swagger.yml
|
||||||
|
|
||||||
|
swagger-server: swagger-generate swagger-lint-ref-siblings swagger-validate swagger-lint ##- (opt) Lint/validate api/swagger.yml and generate /internal/types.
|
||||||
|
|
||||||
|
# https://goswagger.io/generate/server.html
|
||||||
|
# Note that we first flag all files to delete (as previously generated), regenerate, then delete all still flagged files
|
||||||
|
# This allows us to ensure that any filewatchers (VScode) don't panic as these files are removed.
|
||||||
|
# --keep-spec-order is broken (/tmp spec resolving): https://github.com/go-swagger/go-swagger/issues/2216
|
||||||
|
swagger-generate: ##- (opt) Generate swagger /internal/types.
|
||||||
|
@echo "make swagger-generate"
|
||||||
|
@rm -rf tmp/testdata/types
|
||||||
|
@mkdir -p tmp/testdata/types
|
||||||
|
@swagger generate server \
|
||||||
|
--allow-template-override \
|
||||||
|
--template-dir=api/templates \
|
||||||
|
--spec=api/swagger.yml \
|
||||||
|
--server-package=tmp/testdata/types \
|
||||||
|
--model-package=tmp/testdata/types \
|
||||||
|
--exclude-main \
|
||||||
|
--skip-validation \
|
||||||
|
--config-file=api/config/go-swagger-config.yml \
|
||||||
|
-q
|
||||||
|
@find tmp/testdata/types -type f -exec sed -i "s|${GO_MODULE_NAME}/tmp/testdata/types|${GO_MODULE_NAME}/internal/types|g" {} \;
|
||||||
|
rsync -au --ignore-times --delete tmp/testdata/types/ internal/types/
|
||||||
|
|
||||||
|
swagger-validate: ##- (opt) Validate api/swagger.yml.
|
||||||
|
@echo "make swagger-validate"
|
||||||
|
@swagger validate --skip-warnings --stop-on-error -q api/swagger.yml
|
||||||
|
|
||||||
|
watch-swagger: ##- Watches *.yml|yaml|gotmpl files in /api and runs 'make swagger' on modifications.
|
||||||
|
@echo "Watching /api/**/*.yml|yaml|gotmpl. Use Ctrl-c to stop a run or exit."
|
||||||
|
watchexec -p -w api -i tmp -i api/swagger.yml --exts yml,yaml,gotmpl $(MAKE) swagger
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Binary checks
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
# Got license issues with some dependencies? Provide a custom lichen --config
|
||||||
|
# see https://github.com/uw-labs/lichen#config
|
||||||
|
get-licenses: ##- Prints licenses of embedded modules in the compiled bin/app.
|
||||||
|
lichen bin/app
|
||||||
|
|
||||||
|
get-embedded-modules: ##- Prints embedded modules in the compiled bin/app.
|
||||||
|
go version -m -v bin/app
|
||||||
|
|
||||||
|
get-embedded-modules-count: ##- (opt) Prints count of embedded modules in the compiled bin/app.
|
||||||
|
go version -m -v bin/app | grep $$'\tdep' | wc -l
|
||||||
|
|
||||||
|
check-embedded-modules-go-not: ##- (opt) Checks embedded modules in compiled bin/app against go.not, throws on occurrence.
|
||||||
|
@echo "make check-embedded-modules-go-not"
|
||||||
|
@(mkdir -p tmp 2> /dev/null && go version -m -v bin/app > tmp/.modules)
|
||||||
|
grep -f go.not -F tmp/.modules && (echo "go.not: Found disallowed embedded module(s) in bin/app!" && exit 1) || exit 0
|
||||||
|
|
||||||
|
trivy-report: ##- Prints trivy report for all severities without failing
|
||||||
|
@echo "make trivy-report"
|
||||||
|
@trivy fs /app --exit-code 0 --quiet
|
||||||
|
|
||||||
|
trivy: ##- Runs trivy analysis and fails on HIGH, CRITICAL vulnerabilities.
|
||||||
|
@echo "make trivy"
|
||||||
|
@trivy fs /app --exit-code 1 --severity HIGH,CRITICAL --quiet
|
||||||
|
|
||||||
|
govulncheck: ##- Runs govulncheck.
|
||||||
|
@echo "make govulncheck"
|
||||||
|
@govulncheck /app/...
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Git related
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
# This is the default upstream go-starter branch we will use for our comparisons.
|
||||||
|
# You may use a different tag/branch/commit like this:
|
||||||
|
# - Merge with a specific tag, e.g. `go-starter-2021-10-19`: `GIT_GO_STARTER_TARGET=go-starter-2021-10-19 make git-merge-go-starter`
|
||||||
|
# - Merge with a specific branch, e.g. `mr/housekeeping`: `GIT_GO_STARTER_TARGET=go-starter/mr/housekeeping make git-merge-go-starter` (heads up! it's `go-starter/<branchname>`)
|
||||||
|
# - Merge with a specific commit, e.g. `e85bedb9`: `GIT_GO_STARTER_TARGET=e85bedb94c3562602bc23d2bfd09fca3b13d1e02 make git-merge-go-starter`
|
||||||
|
GIT_GO_STARTER_TARGET ?= go-starter/master
|
||||||
|
GIT_GO_STARTER_BASE ?= $(GIT_GO_STARTER_TARGET:go-starter/%=%)
|
||||||
|
|
||||||
|
git-fetch-go-starter: ##- (opt) Fetches upstream GIT_GO_STARTER_TARGET (creating git remote 'go-starter').
|
||||||
|
@echo "GIT_GO_STARTER_TARGET=${GIT_GO_STARTER_TARGET} GIT_GO_STARTER_BASE=${GIT_GO_STARTER_BASE}"
|
||||||
|
@git config remote.go-starter.url >&- || git remote add go-starter https://github.com/allaboutapps/go-starter.git
|
||||||
|
@git fetch go-starter ${GIT_GO_STARTER_BASE}
|
||||||
|
|
||||||
|
git-compare-go-starter: ##- (opt) Compare upstream GIT_GO_STARTER_TARGET to HEAD displaying commits away and git log.
|
||||||
|
@$(MAKE) git-fetch-go-starter
|
||||||
|
@echo "Commits away from upstream go-starter ${GIT_GO_STARTER_TARGET}:"
|
||||||
|
git --no-pager rev-list --pretty=oneline --left-only --count ${GIT_GO_STARTER_TARGET}...HEAD
|
||||||
|
@echo ""
|
||||||
|
@echo "Git log:"
|
||||||
|
git --no-pager log --left-only --pretty="%C(Yellow)%h %C(reset)%ad (%C(Green)%cr%C(reset))%x09 %C(Cyan)%an: %C(reset)%s" --abbrev-commit --count ${GIT_GO_STARTER_TARGET}...HEAD
|
||||||
|
|
||||||
|
git-merge-go-starter: ##- Merges upstream GIT_GO_STARTER_TARGET into current HEAD.
|
||||||
|
@$(MAKE) git-compare-go-starter
|
||||||
|
@(echo "" \
|
||||||
|
&& echo "Attempting to execute 'git merge --no-commit --no-ff --allow-unrelated-histories ${GIT_GO_STARTER_TARGET}' into your current HEAD." \
|
||||||
|
&& echo -n "Are you sure? [y/N]" \
|
||||||
|
&& read ans && [ $${ans:-N} = y ]) || exit 1
|
||||||
|
git merge --no-commit --no-ff --allow-unrelated-histories ${GIT_GO_STARTER_TARGET} || true
|
||||||
|
@echo "Done. We recommend to run 'make force-module-name' to automatically fix all import paths."
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Helpers
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
clean: ##- Cleans ./tmp and ./api/tmp folder.
|
||||||
|
@echo "make clean"
|
||||||
|
@rm -rf tmp/* 2> /dev/null
|
||||||
|
@rm -rf api/tmp/* 2> /dev/null
|
||||||
|
|
||||||
|
get-module-name: ##- Prints current go module-name (pipeable).
|
||||||
|
@echo "${GO_MODULE_NAME}"
|
||||||
|
|
||||||
|
info-module-name: ##- (opt) Prints current go module-name.
|
||||||
|
@echo "go module-name: '${GO_MODULE_NAME}'"
|
||||||
|
|
||||||
|
set-module-name: ##- Wizard to set a new go module-name.
|
||||||
|
@rm -f tmp/.modulename
|
||||||
|
@$(MAKE) info-module-name
|
||||||
|
@echo "Enter new go module-name:" \
|
||||||
|
&& read new_module_name \
|
||||||
|
&& echo "new go module-name: '$${new_module_name}'" \
|
||||||
|
&& echo -n "Are you sure? [y/N]" \
|
||||||
|
&& read ans && [ $${ans:-N} = y ] \
|
||||||
|
&& echo -n "Please wait..." \
|
||||||
|
&& find . -not -path '*/\.*' -not -path './Makefile' -type f -exec sed -i "s|${GO_MODULE_NAME}|$${new_module_name}|g" {} \; \
|
||||||
|
&& echo "new go module-name: '$${new_module_name}'!"
|
||||||
|
@rm -f tmp/.modulename
|
||||||
|
|
||||||
|
force-module-name: ##- Overwrite occurrences of 'allaboutapps.dev/aw/go-starter' with current go module-name.
|
||||||
|
find . -not -path '*/\.*' -not -path './Makefile' -type f -exec sed -i "s|allaboutapps.dev/aw/go-starter|${GO_MODULE_NAME}|g" {} \;
|
||||||
|
|
||||||
|
get-go-ldflags: ##- (opt) Prints used -ldflags as evaluated in Makefile used in make go-build
|
||||||
|
@echo $(LDFLAGS)
|
||||||
|
|
||||||
|
help: ##- Show common make targets.
|
||||||
|
@echo -e "usage: make \033[36m<target>\033[0m"
|
||||||
|
@echo "note: use 'make help-all' to see all make targets."
|
||||||
|
@awk 'BEGIN {FS = ":.*#\#-"; } /^[a-zA-Z_-]+:.*?/ && !/(opt)/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^# --- / { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
help-all: ##- Show all make targets.
|
||||||
|
@echo -e "usage: make \033[36m<target>\033[0m"
|
||||||
|
@awk 'BEGIN {FS = ":.*#\#-"; } /^[a-zA-Z_-]+:.*?/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^# --- / { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Changelog
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
changelog-prerelease: ##- Generates a changelog prerelease.
|
||||||
|
@echo "make changelog-prerelease"
|
||||||
|
@changie batch 0.0.0 --prerelease prerelease-$(shell date +%Y-%m-%d-%H%M%S) --move-dir prerelease
|
||||||
|
@git add --all
|
||||||
|
@git commit -m "PRERELEASE: $(shell date +%Y-%m-%d-%H%M%S)"
|
||||||
|
|
||||||
|
changelog-release: ##- Generates a changelog release using the $(VERSION) variable. Usage: make changelog-release VERSION=24.11.0
|
||||||
|
@echo "make changelog-release with version $(VERSION)"
|
||||||
|
@changie batch $(VERSION) --include prerelease --remove-prereleases
|
||||||
|
@changie merge
|
||||||
|
@git add --all
|
||||||
|
@git commit -m "RELEASE: $(VERSION)"
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Make variables
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
# only evaluated if required by a recipe
|
||||||
|
# http://make.mad-scientist.net/deferred-simple-variable-expansion/
|
||||||
|
|
||||||
|
# go module name (as in go.mod)
|
||||||
|
GO_MODULE_NAME = $(eval GO_MODULE_NAME := $$(shell \
|
||||||
|
(mkdir -p tmp 2> /dev/null && cat tmp/.modulename 2> /dev/null) \
|
||||||
|
|| (gsdev modulename 2> /dev/null | tee tmp/.modulename) || echo "unknown" \
|
||||||
|
))$(GO_MODULE_NAME)
|
||||||
|
|
||||||
|
# https://medium.com/the-go-journey/adding-version-information-to-go-binaries-e1b79878f6f2
|
||||||
|
ARG_COMMIT = $(eval ARG_COMMIT := $$(shell \
|
||||||
|
(git rev-list -1 HEAD 2> /dev/null) \
|
||||||
|
|| (echo "unknown") \
|
||||||
|
))$(ARG_COMMIT)
|
||||||
|
|
||||||
|
ARG_BUILD_DATE = $(eval ARG_BUILD_DATE := $$(shell \
|
||||||
|
(date -Is 2> /dev/null || date 2> /dev/null || echo "unknown") \
|
||||||
|
))$(ARG_BUILD_DATE)
|
||||||
|
|
||||||
|
# https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications
|
||||||
|
LDFLAGS = $(eval LDFLAGS := "\
|
||||||
|
-X '$(GO_MODULE_NAME)/internal/config.ModuleName=$(GO_MODULE_NAME)'\
|
||||||
|
-X '$(GO_MODULE_NAME)/internal/config.Commit=$(ARG_COMMIT)'\
|
||||||
|
-X '$(GO_MODULE_NAME)/internal/config.BuildDate=$(ARG_BUILD_DATE)'\
|
||||||
|
")$(LDFLAGS)
|
||||||
|
|
||||||
|
### -----------------------
|
||||||
|
# --- Special targets
|
||||||
|
### -----------------------
|
||||||
|
|
||||||
|
# https://www.gnu.org/software/make/manual/html_node/Special-Targets.html
|
||||||
|
# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html
|
||||||
|
# ignore matching file/make rule combinations in working-dir
|
||||||
|
.PHONY: test help
|
||||||
|
|
||||||
|
# https://unix.stackexchange.com/questions/153763/dont-stop-makeing-if-a-command-fails-but-check-exit-status
|
||||||
|
# https://www.gnu.org/software/make/manual/html_node/One-Shell.html
|
||||||
|
# required to ensure make fails if one recipe fails (even on parallel jobs) and on pipefails
|
||||||
|
.ONESHELL:
|
||||||
|
|
||||||
|
# # normal POSIX bash shell mode
|
||||||
|
# SHELL = /bin/bash
|
||||||
|
# .SHELLFLAGS = -cEeuo pipefail
|
||||||
|
|
||||||
|
# wrapped make time tracing shell, use it via MAKE_TRACE_TIME=true make <target>
|
||||||
|
SHELL = /app/rksh
|
||||||
|
.SHELLFLAGS = $@
|
||||||
11
SECURITY.md
Normal file
11
SECURITY.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please do not publicly open issues related to security concerns!
|
||||||
|
|
||||||
|
To report a security issue, email `security [AT] allaboutapps [DOT] at` and include the word `SECURITY` in the subject line:
|
||||||
|
* Do not include anyone else on the disclosure email.
|
||||||
|
* Tell us what you found, how to reproduce it, and any concerns you have about it.
|
||||||
|
|
||||||
|
We will do our best to get back to you ASAP.
|
||||||
22
api/README.md
Normal file
22
api/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# `/api`
|
||||||
|
|
||||||
|
OpenAPI/Swagger specs, JSON schema files, protocol definition files.
|
||||||
|
|
||||||
|
Use `make swagger` do generate `/internal/types/**/*.go` from these specs. You may use `make watch-swagger` while writing these schemas to automatically execute `make swagger` every time there are changes (especially useful if you have `http://localhost:8081/` opened in your browser while mutating these specs).
|
||||||
|
|
||||||
|
The final `/api/swagger.yml` is auto-generated from the following specs:
|
||||||
|
1. The skeleton spec at `/api/config/main.yml`,
|
||||||
|
2. all path specs living within `/api/paths/*.yml` and
|
||||||
|
3. any `/api/definitions/*.yml` referenced by in step 1 and 2.
|
||||||
|
|
||||||
|
Further reading:
|
||||||
|
* [Swagger v2 specification](https://swagger.io/specification/v2/)
|
||||||
|
* [go-swagger](https://github.com/go-swagger/go-swagger)
|
||||||
|
* [go-swagger: Schema Generation Rules](https://github.com/go-swagger/go-swagger/blob/master/docs/use/models/schemas.md)
|
||||||
|
|
||||||
|
|
||||||
|
Related examples regarding this project layout:
|
||||||
|
|
||||||
|
* https://github.com/golang-standards/project-layout/blob/master/api/README.md
|
||||||
|
* https://github.com/kubernetes/kubernetes/tree/master/api
|
||||||
|
* https://github.com/moby/moby/tree/master/api
|
||||||
16
api/config/go-swagger-config.yml
Normal file
16
api/config/go-swagger-config.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
layout:
|
||||||
|
models:
|
||||||
|
- name: definition
|
||||||
|
source: asset:model
|
||||||
|
target: "{{ joinFilePath .Target .ModelPackage }}"
|
||||||
|
file_name: "{{ (snakize (pascalize .Name)) }}.go"
|
||||||
|
operations:
|
||||||
|
- name: parameters
|
||||||
|
source: asset:serverParameter
|
||||||
|
target: "{{ joinFilePath .Target .ServerPackage .Package }}"
|
||||||
|
file_name: "{{ (snakize (pascalize .Name)) }}_parameters.go"
|
||||||
|
application:
|
||||||
|
- name: builder
|
||||||
|
source: asset:serverBuilder
|
||||||
|
target: "{{ joinFilePath .Target .ServerPackage }}"
|
||||||
|
file_name: "spec_handlers.go"
|
||||||
37
api/config/main.yml
Normal file
37
api/config/main.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# This is our base swagger file and the primary mixin target.
|
||||||
|
# Everything in definitions|paths/*.yml will be mixed through
|
||||||
|
# and finally flattened into the actual swagger.yml in this dir.
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
description: API documentation
|
||||||
|
paths: {}
|
||||||
|
securityDefinitions:
|
||||||
|
Bearer:
|
||||||
|
type: apiKey
|
||||||
|
name: Authorization
|
||||||
|
in: header
|
||||||
|
description: |-
|
||||||
|
Access token for application access, **must** include "Bearer " prefix.
|
||||||
|
Example: `Bearer b4a94a42-3ea2-4af3-9699-8bcbfee6e6d2`
|
||||||
|
x-keyPrefix: "Bearer "
|
||||||
|
Management:
|
||||||
|
type: apiKey
|
||||||
|
in: query
|
||||||
|
description: Management secret, used for monitoring and infrastructure related calls
|
||||||
|
name: mgmt-secret
|
||||||
|
definitions:
|
||||||
|
# Any definitions that are not yet used within paths/*.yml are automatically removed from the resulting swagger.yml.
|
||||||
|
# You may reference some definitions that you *always* want to be included here.
|
||||||
|
# --
|
||||||
|
# Always include orderDir so we can test binding it to db queries directly.
|
||||||
|
orderDir:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- asc
|
||||||
|
- desc
|
||||||
122
api/config/swagger-ui-local-translator.js
Normal file
122
api/config/swagger-ui-local-translator.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// https://github.com/swagger-api/swagger-ui/blob/master/docker/configurator/translator.js
|
||||||
|
// Based on commit in 02758b8125dbf38763cfd5d4f91c7c803e9bd0ad
|
||||||
|
// Nov 08, 2018
|
||||||
|
// see "AAA change" for applied changes
|
||||||
|
|
||||||
|
// Converts an object of environment variables into a Swagger UI config object
|
||||||
|
const configSchema = require("./variables")
|
||||||
|
|
||||||
|
const defaultBaseConfig = {
|
||||||
|
url: {
|
||||||
|
value: "https://petstore.swagger.io/v2/swagger.json",
|
||||||
|
schema: {
|
||||||
|
type: "string",
|
||||||
|
base: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dom_id: {
|
||||||
|
value: "#swagger-ui",
|
||||||
|
schema: {
|
||||||
|
type: "string",
|
||||||
|
base: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deepLinking: {
|
||||||
|
value: "true",
|
||||||
|
schema: {
|
||||||
|
type: "boolean",
|
||||||
|
base: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
presets: {
|
||||||
|
value: `[\n SwaggerUIBundle.presets.apis,\n SwaggerUIStandalonePreset\n]`,
|
||||||
|
schema: {
|
||||||
|
type: "array",
|
||||||
|
base: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
value: `[\n SwaggerUIBundle.plugins.DownloadUrl\n]`,
|
||||||
|
schema: {
|
||||||
|
type: "array",
|
||||||
|
base: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
value: "StandaloneLayout",
|
||||||
|
schema: {
|
||||||
|
type: "string",
|
||||||
|
base: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// AAA change START: interspect requests to 8081 and instead pass them to 8080 where our server runs
|
||||||
|
requestInterceptor: {
|
||||||
|
value: `function (request) {
|
||||||
|
request.url = request.url.split(":8081/").join(":8080/");
|
||||||
|
console.log("[requestInterceptor] :8080", request);
|
||||||
|
return request;
|
||||||
|
}`,
|
||||||
|
schema: {
|
||||||
|
type: "function",
|
||||||
|
base: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// AAA change END
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectToKeyValueString(env, { injectBaseConfig = false, schema = configSchema, baseConfig = defaultBaseConfig } = {}) {
|
||||||
|
let valueStorage = injectBaseConfig ? Object.assign({}, baseConfig) : {}
|
||||||
|
const keys = Object.keys(env)
|
||||||
|
|
||||||
|
// Compute an intermediate representation that holds candidate values and schemas.
|
||||||
|
//
|
||||||
|
// This is useful for deduping between multiple env keys that set the same
|
||||||
|
// config variable.
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
const varSchema = schema[key]
|
||||||
|
const value = env[key]
|
||||||
|
|
||||||
|
if (!varSchema) return
|
||||||
|
|
||||||
|
if (varSchema.onFound) {
|
||||||
|
varSchema.onFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageContents = valueStorage[varSchema.name]
|
||||||
|
|
||||||
|
if (storageContents) {
|
||||||
|
if (varSchema.legacy === true && !storageContents.schema.base) {
|
||||||
|
// If we're looking at a legacy var, it should lose out to any already-set value
|
||||||
|
// except for base values
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete valueStorage[varSchema.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
valueStorage[varSchema.name] = {
|
||||||
|
value,
|
||||||
|
schema: varSchema
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Compute a key:value string based on valueStorage's contents.
|
||||||
|
|
||||||
|
let result = ""
|
||||||
|
|
||||||
|
Object.keys(valueStorage).forEach(key => {
|
||||||
|
const value = valueStorage[key]
|
||||||
|
|
||||||
|
const escapedName = /[^a-zA-Z0-9]/.test(key) ? `"${key}"` : key
|
||||||
|
|
||||||
|
if (value.schema.type === "string") {
|
||||||
|
result += `${escapedName}: "${value.value}",\n`
|
||||||
|
} else {
|
||||||
|
result += `${escapedName}: ${value.value === "" ? `undefined` : value.value},\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = objectToKeyValueString
|
||||||
195
api/definitions/auth.yml
Normal file
195
api/definitions/auth.yml
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths: {}
|
||||||
|
definitions:
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
description: ID of user
|
||||||
|
example: 891d37d3-c74f-493e-aea8-af73efd92016
|
||||||
|
GetUserInfoResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- sub
|
||||||
|
- updated_at
|
||||||
|
properties:
|
||||||
|
sub:
|
||||||
|
type: string
|
||||||
|
description: ID of user
|
||||||
|
example: 82ebdfad-c586-4407-a873-4cc1c33d56fc
|
||||||
|
updated_at:
|
||||||
|
type: integer
|
||||||
|
description: Unix timestamp the user's info was last updated at
|
||||||
|
example: 1591960808
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
description: Email address of user, if available
|
||||||
|
maxLength: 255
|
||||||
|
example: user@example.com
|
||||||
|
scopes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "app"
|
||||||
|
- "cms"
|
||||||
|
description: Auth-Scopes of the user, if available
|
||||||
|
example: ["app"]
|
||||||
|
PostChangePasswordPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- currentPassword
|
||||||
|
- newPassword
|
||||||
|
properties:
|
||||||
|
currentPassword:
|
||||||
|
description: Current password of user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
newPassword:
|
||||||
|
description: New password to set for user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
PostForgotPasswordCompletePayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- token
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
password:
|
||||||
|
description: New password to set for user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
token:
|
||||||
|
description: Password reset token sent via email
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: ec16f032-3c44-4148-bbcc-45557466fa74
|
||||||
|
PostForgotPasswordPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
description: Username to initiate password reset for
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
example: user@example.com
|
||||||
|
PostLoginPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
password:
|
||||||
|
description: Password of user to authenticate as
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
username:
|
||||||
|
description: Username of user to authenticate as
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
example: user@example.com
|
||||||
|
PostLoginResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- access_token
|
||||||
|
- token_type
|
||||||
|
- expires_in
|
||||||
|
- refresh_token
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
description: Access token required for accessing protected API endpoints
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: c1247d8d-0d65-41c4-bc86-ec041d2ac437
|
||||||
|
expires_in:
|
||||||
|
description: Access token expiry in seconds
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
example: 86400
|
||||||
|
refresh_token:
|
||||||
|
description: Refresh token for refreshing the access token once it expires
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: 1dadb3bd-50d8-485d-83a3-6111392568f0
|
||||||
|
token_type:
|
||||||
|
description: "Type of access token, will always be `bearer`"
|
||||||
|
type: string
|
||||||
|
example: bearer
|
||||||
|
RegisterResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- requiresConfirmation
|
||||||
|
properties:
|
||||||
|
requiresConfirmation:
|
||||||
|
description: Indicates whether the registration process requires email confirmation
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
PostLogoutPayload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
refresh_token:
|
||||||
|
description: Optional refresh token to delete while logging out
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: 700ebed3-40f7-4211-bc83-a89b22b9875e
|
||||||
|
PostRefreshPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- refresh_token
|
||||||
|
properties:
|
||||||
|
refresh_token:
|
||||||
|
description: Refresh token to use for retrieving new token set
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: 7503cd8a-c921-4368-a32d-6c1d01d86da9
|
||||||
|
PostRegisterPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
password:
|
||||||
|
description: Password to register with
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
username:
|
||||||
|
description: Username to register with
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
example: user@example.com
|
||||||
|
DeleteUserAccountPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- currentPassword
|
||||||
|
properties:
|
||||||
|
currentPassword:
|
||||||
|
description: Current password of user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
47
api/definitions/common.yml
Normal file
47
api/definitions/common.yml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths: {}
|
||||||
|
definitions:
|
||||||
|
Paginatable:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- limit
|
||||||
|
- offset
|
||||||
|
- total
|
||||||
|
properties:
|
||||||
|
limit:
|
||||||
|
type: integer
|
||||||
|
description: Actual limit applied to request
|
||||||
|
offset:
|
||||||
|
type: integer
|
||||||
|
description: Actual offset applied to request
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
description: Total number of records available
|
||||||
|
parameters:
|
||||||
|
offsetParam:
|
||||||
|
type: integer
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
description: Offset used for pagination, number of records to skip
|
||||||
|
default: 0
|
||||||
|
minimum: 0
|
||||||
|
limitParam:
|
||||||
|
type: integer
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
description: Limit used for pagination, number of records to retrieve
|
||||||
|
default: 50
|
||||||
|
minimum: 1
|
||||||
|
maximum: 500
|
||||||
|
orderDirParam:
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
name: orderDir
|
||||||
|
description: Direction of order applied, defaults to `asc` if omitted. `asc` will sort `NULL` values at the end of the list.
|
||||||
|
enum:
|
||||||
|
- asc
|
||||||
|
- desc
|
||||||
|
default: asc
|
||||||
79
api/definitions/errors.yml
Normal file
79
api/definitions/errors.yml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths: {}
|
||||||
|
definitions:
|
||||||
|
PublicHTTPErrorType:
|
||||||
|
type: string
|
||||||
|
description: Type of error returned, should be used for client-side error handling
|
||||||
|
enum:
|
||||||
|
- generic
|
||||||
|
# push
|
||||||
|
- PUSH_TOKEN_ALREADY_EXISTS
|
||||||
|
- OLD_PUSH_TOKEN_NOT_FOUND
|
||||||
|
# files
|
||||||
|
- ZERO_FILE_SIZE
|
||||||
|
# auth
|
||||||
|
- USER_DEACTIVATED
|
||||||
|
- INVALID_PASSWORD
|
||||||
|
- NOT_LOCAL_USER
|
||||||
|
- TOKEN_NOT_FOUND
|
||||||
|
- TOKEN_EXPIRED
|
||||||
|
- USER_ALREADY_EXISTS
|
||||||
|
- MALFORMED_TOKEN
|
||||||
|
- LAST_AUTHENTICATED_AT_EXCEEDED
|
||||||
|
- MISSING_SCOPES
|
||||||
|
PublicHTTPError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
- title
|
||||||
|
properties:
|
||||||
|
detail:
|
||||||
|
description: "More detailed, human-readable, optional explanation of the error"
|
||||||
|
type: string
|
||||||
|
example: User is lacking permission to access this resource
|
||||||
|
status:
|
||||||
|
description: HTTP status code returned for the error
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
maximum: 599
|
||||||
|
minimum: 100
|
||||||
|
x-go-name: Code
|
||||||
|
example: 403
|
||||||
|
title:
|
||||||
|
description: "Short, human-readable description of the error"
|
||||||
|
type: string
|
||||||
|
example: Forbidden
|
||||||
|
type:
|
||||||
|
$ref: "#/definitions/PublicHTTPErrorType"
|
||||||
|
PublicHTTPValidationError:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/definitions/PublicHTTPError"
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- validationErrors
|
||||||
|
properties:
|
||||||
|
validationErrors:
|
||||||
|
description: List of errors received while validating payload against schema
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/HTTPValidationErrorDetail"
|
||||||
|
HTTPValidationErrorDetail:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- key
|
||||||
|
- in
|
||||||
|
- error
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
description: Error describing field validation failure
|
||||||
|
type: string
|
||||||
|
in:
|
||||||
|
description: Indicates how the invalid field was provided
|
||||||
|
type: string
|
||||||
|
key:
|
||||||
|
description: Key of field failing validation
|
||||||
|
type: string
|
||||||
28
api/definitions/push.yml
Normal file
28
api/definitions/push.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths: {}
|
||||||
|
definitions:
|
||||||
|
PutUpdatePushTokenPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- newToken
|
||||||
|
- provider
|
||||||
|
properties:
|
||||||
|
newToken:
|
||||||
|
description: New push token for given provider.
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
example: 1c91e550-8167-439c-8021-dee7de2f7e96
|
||||||
|
oldToken:
|
||||||
|
description: Old token that can be deleted if present.
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
example: 495179de-b771-48f0-aab2-8d23701b0f02
|
||||||
|
x-nullable: true
|
||||||
|
provider:
|
||||||
|
description: Identifier of the provider the token is for (eg. "fcm", "apn"). Currently only "fcm" is supported.
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
example: fcm
|
||||||
289
api/paths/auth.yml
Normal file
289
api/paths/auth.yml
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
responses:
|
||||||
|
InvalidPasswordResponse:
|
||||||
|
description: "PublicHTTPValidationError, type `INVALID_PASSWORD`"
|
||||||
|
schema:
|
||||||
|
$ref: ../definitions/errors.yml#/definitions/PublicHTTPValidationError
|
||||||
|
AuthUnauthorizedResponse:
|
||||||
|
description: PublicHTTPError
|
||||||
|
schema:
|
||||||
|
$ref: ../definitions/errors.yml#/definitions/PublicHTTPError
|
||||||
|
AuthForbiddenResponse:
|
||||||
|
description: "PublicHTTPError, type `USER_DEACTIVATED`/`NOT_LOCAL_USER`"
|
||||||
|
schema:
|
||||||
|
$ref: ../definitions/errors.yml#/definitions/PublicHTTPError
|
||||||
|
ValidationError:
|
||||||
|
description: PublicHTTPValidationError
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/errors.yml#/definitions/PublicHTTPValidationError"
|
||||||
|
parameters:
|
||||||
|
registrationTokenParam:
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
in: path
|
||||||
|
name: registrationToken
|
||||||
|
description: Registration token to complete the registration process
|
||||||
|
required: true
|
||||||
|
paths:
|
||||||
|
/api/v1/auth/change-password:
|
||||||
|
post:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: |-
|
||||||
|
After successful password change, all current access and refresh tokens are
|
||||||
|
invalidated and a new set of auth tokens is returned
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Change local user's password
|
||||||
|
operationId: PostChangePasswordRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: ../definitions/auth.yml#/definitions/PostChangePasswordPayload
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: ../definitions/auth.yml#/definitions/PostLoginResponse
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/InvalidPasswordResponse"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/AuthUnauthorizedResponse"
|
||||||
|
"403":
|
||||||
|
$ref: "#/responses/AuthForbiddenResponse"
|
||||||
|
|
||||||
|
/api/v1/auth/forgot-password:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Initiates a password reset for a local user, sending an email with a password
|
||||||
|
reset link to the provided email address if a user account exists. Will always
|
||||||
|
succeed, even if no user was found in order to prevent user enumeration
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Initiate password reset for local user
|
||||||
|
operationId: PostForgotPasswordRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostForgotPasswordPayload"
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Success
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/ValidationError"
|
||||||
|
/api/v1/auth/forgot-password/complete:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Completes a password reset for a local user, using the password reset token sent via email
|
||||||
|
to confirm user access, setting the new password if successful. All current access and refresh
|
||||||
|
tokens are invalidated and a new set of auth tokens is returned
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Completes password reset for local user
|
||||||
|
operationId: PostForgotPasswordCompleteRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostForgotPasswordCompletePayload"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostLoginResponse"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/InvalidPasswordResponse"
|
||||||
|
"403":
|
||||||
|
$ref: "#/responses/AuthForbiddenResponse"
|
||||||
|
"404":
|
||||||
|
description: "PublicHTTPError, type `TOKEN_NOT_FOUND`"
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/errors.yml#/definitions/PublicHTTPError"
|
||||||
|
"409":
|
||||||
|
description: "PublicHTTPError, type `TOKEN_EXPIRED`"
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/errors.yml#/definitions/PublicHTTPError"
|
||||||
|
/api/v1/auth/login:
|
||||||
|
post:
|
||||||
|
description: Returns an access and refresh token on successful authentication
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Login with local user
|
||||||
|
operationId: PostLoginRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostLoginPayload"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostLoginResponse"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/ValidationError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/AuthUnauthorizedResponse"
|
||||||
|
"403":
|
||||||
|
description: "PublicHTTPError, type `USER_DEACTIVATED`"
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/errors.yml#/definitions/PublicHTTPError"
|
||||||
|
/api/v1/auth/logout:
|
||||||
|
post:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: |-
|
||||||
|
Logs the local user out, destroying the provided access token.
|
||||||
|
A refresh token can optionally be provided, destroying it as well if found.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Log out local user
|
||||||
|
operationId: PostLogoutRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostLogoutPayload"
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Success
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/ValidationError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/AuthUnauthorizedResponse"
|
||||||
|
/api/v1/auth/refresh:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Returns a fresh set of access and refresh tokens if a valid refresh token was provided.
|
||||||
|
The old refresh token used to authenticate the request will be invalidated.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Refresh tokens
|
||||||
|
operationId: PostRefreshRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostRefreshPayload"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostLoginResponse"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/ValidationError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/AuthUnauthorizedResponse"
|
||||||
|
"403":
|
||||||
|
description: "PublicHTTPError, type `USER_DEACTIVATED`"
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/errors.yml#/definitions/PublicHTTPError"
|
||||||
|
/api/v1/auth/register:
|
||||||
|
post:
|
||||||
|
description: |
|
||||||
|
Registers a local user. If the registration process requires a confirmation
|
||||||
|
the status code `202` is returned without auth tokens. Afterwards the registration needs to be confirmed
|
||||||
|
using the `POST /api/v1/auth/register/{registrationToken}` endpoint.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Registers a local user
|
||||||
|
operationId: PostRegisterRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostRegisterPayload"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostLoginResponse"
|
||||||
|
"202":
|
||||||
|
description: RegisterResponse
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/RegisterResponse"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/InvalidPasswordResponse"
|
||||||
|
"409":
|
||||||
|
description: "PublicHTTPError, type `USER_ALREADY_EXISTS`"
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/errors.yml#/definitions/PublicHTTPError"
|
||||||
|
get:
|
||||||
|
summary: Page to complete registration
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
operationId: GetCompleteRegisterRoute
|
||||||
|
parameters:
|
||||||
|
- name: token
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
description: Registration token to complete the registration process
|
||||||
|
required: true
|
||||||
|
produces:
|
||||||
|
- text/html
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Page to complete registration
|
||||||
|
/api/v1/auth/register/{registrationToken}:
|
||||||
|
post:
|
||||||
|
summary: Complete registration
|
||||||
|
description: |-
|
||||||
|
Completes registration for a local user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
operationId: PostCompleteRegisterRoute
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/parameters/registrationTokenParam"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/PostLoginResponse"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
/api/v1/auth/userinfo:
|
||||||
|
get:
|
||||||
|
summary: Get user info
|
||||||
|
description: |-
|
||||||
|
Returns user information compatible with the OpenID Connect Core 1.0 specification.
|
||||||
|
Information returned depends on the requesting user as some data is only available if an app user profile exists.
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
operationId: GetUserInfoRoute
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: GetUserInfoResponse
|
||||||
|
schema:
|
||||||
|
$ref: "../definitions/auth.yml#/definitions/GetUserInfoResponse"
|
||||||
|
/api/v1/auth/account:
|
||||||
|
delete:
|
||||||
|
summary: Delete user account
|
||||||
|
description: |-
|
||||||
|
Completely delete the user account. This action is irreversible and therefore requires additional password authentication.
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
operationId: DeleteUserAccountRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: ../definitions/auth.yml#/definitions/DeleteUserAccountPayload
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: NoContent
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/InvalidPasswordResponse"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/AuthUnauthorizedResponse"
|
||||||
|
"403":
|
||||||
|
$ref: "#/responses/AuthForbiddenResponse"
|
||||||
71
api/paths/common.yml
Normal file
71
api/paths/common.yml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths:
|
||||||
|
/swagger.yml:
|
||||||
|
get:
|
||||||
|
summary: Get swagger.yml
|
||||||
|
operationId: GetSwaggerRoute
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
description: |-
|
||||||
|
OpenAPI Specification ver. 2 (fka Swagger)
|
||||||
|
Returns our handcrafted and validated `swagger.yml`.
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
/-/ready:
|
||||||
|
get:
|
||||||
|
summary: Get ready (readiness probe)
|
||||||
|
operationId: GetReadyRoute
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
description: |-
|
||||||
|
This endpoint returns 200 when the service is ready to serve traffic.
|
||||||
|
Does read-only probes apart from the general server ready state.
|
||||||
|
Note that /-/ready is typically public (and not shielded by a mgmt-secret), we thus prevent information leakage here and only return `"Ready."`.
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Ready.
|
||||||
|
"521":
|
||||||
|
description: Not ready.
|
||||||
|
/-/healthy:
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- Management: []
|
||||||
|
summary: Get healthy (liveness probe)
|
||||||
|
operationId: GetHealthyRoute
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
description: |-
|
||||||
|
This endpoint returns 200 when the service is healthy.
|
||||||
|
Returns an human readable string about the current service status.
|
||||||
|
In addition to readiness probes, it performs actual write probes.
|
||||||
|
Note that /-/healthy is private (shielded by the mgmt-secret) as it may expose sensitive information about your service.
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Ready.
|
||||||
|
"521":
|
||||||
|
description: Not ready.
|
||||||
|
/-/version:
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- Management: []
|
||||||
|
summary: Get version
|
||||||
|
operationId: GetVersionRoute
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
description: |-
|
||||||
|
This endpoint returns the module name, commit and build-date baked into the app binary.
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "ModuleName @ Commit (BuildDate)"
|
||||||
33
api/paths/push.yml
Normal file
33
api/paths/push.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths:
|
||||||
|
/api/v1/push/token:
|
||||||
|
put:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: |-
|
||||||
|
Adds a push token for the given provider to the current user.
|
||||||
|
If the oldToken is present it will be deleted.
|
||||||
|
Currently only the provider 'fcm' is supported.
|
||||||
|
tags:
|
||||||
|
- push
|
||||||
|
summary: Adds a push token to the user
|
||||||
|
operationId: PutUpdatePushTokenRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
"$ref": "../definitions/push.yml#/definitions/PutUpdatePushTokenPayload"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
"404":
|
||||||
|
description: PublicHTTPError, type `OLD_PUSH_TOKEN_NOT_FOUND`
|
||||||
|
schema:
|
||||||
|
"$ref": "../definitions/errors.yml#/definitions/PublicHTTPError"
|
||||||
|
"409":
|
||||||
|
description: PublicHTTPError, type `PUSH_TOKEN_ALREADY_EXISTS`
|
||||||
|
schema:
|
||||||
|
"$ref": "../definitions/errors.yml#/definitions/PublicHTTPError"
|
||||||
27
api/paths/well_known.yml
Normal file
27
api/paths/well_known.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths:
|
||||||
|
/.well-known/apple-app-site-association:
|
||||||
|
get:
|
||||||
|
summary: Apple App Site Association
|
||||||
|
description: |-
|
||||||
|
Returns the Apple App Site Association file.
|
||||||
|
operationId: GetAppleAppSiteAssociationRoute
|
||||||
|
tags:
|
||||||
|
- well-known
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Apple App Site Association
|
||||||
|
/.well-known/assetlinks.json:
|
||||||
|
get:
|
||||||
|
summary: Android Digital Asset Links
|
||||||
|
description: |-
|
||||||
|
Returns the Android Digital Asset Links file.
|
||||||
|
operationId: GetAndroidDigitalAssetLinksRoute
|
||||||
|
tags:
|
||||||
|
- well-known
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Android Digital Asset Links
|
||||||
738
api/swagger.yml
Normal file
738
api/swagger.yml
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
# // Code generated by "make swagger"; DO NOT EDIT.
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
description: API documentation
|
||||||
|
title: allaboutapps.dev/aw/go-starter
|
||||||
|
version: 0.1.0
|
||||||
|
paths:
|
||||||
|
/-/healthy:
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- Management: []
|
||||||
|
description: |-
|
||||||
|
This endpoint returns 200 when the service is healthy.
|
||||||
|
Returns an human readable string about the current service status.
|
||||||
|
In addition to readiness probes, it performs actual write probes.
|
||||||
|
Note that /-/healthy is private (shielded by the mgmt-secret) as it may expose sensitive information about your service.
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
summary: Get healthy (liveness probe)
|
||||||
|
operationId: GetHealthyRoute
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Ready.
|
||||||
|
"521":
|
||||||
|
description: Not ready.
|
||||||
|
/-/ready:
|
||||||
|
get:
|
||||||
|
description: |-
|
||||||
|
This endpoint returns 200 when the service is ready to serve traffic.
|
||||||
|
Does read-only probes apart from the general server ready state.
|
||||||
|
Note that /-/ready is typically public (and not shielded by a mgmt-secret), we thus prevent information leakage here and only return `"Ready."`.
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
summary: Get ready (readiness probe)
|
||||||
|
operationId: GetReadyRoute
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Ready.
|
||||||
|
"521":
|
||||||
|
description: Not ready.
|
||||||
|
/-/version:
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- Management: []
|
||||||
|
description: This endpoint returns the module name, commit and build-date baked
|
||||||
|
into the app binary.
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
summary: Get version
|
||||||
|
operationId: GetVersionRoute
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ModuleName @ Commit (BuildDate)
|
||||||
|
/.well-known/apple-app-site-association:
|
||||||
|
get:
|
||||||
|
description: Returns the Apple App Site Association file.
|
||||||
|
tags:
|
||||||
|
- well-known
|
||||||
|
summary: Apple App Site Association
|
||||||
|
operationId: GetAppleAppSiteAssociationRoute
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Apple App Site Association
|
||||||
|
/.well-known/assetlinks.json:
|
||||||
|
get:
|
||||||
|
description: Returns the Android Digital Asset Links file.
|
||||||
|
tags:
|
||||||
|
- well-known
|
||||||
|
summary: Android Digital Asset Links
|
||||||
|
operationId: GetAndroidDigitalAssetLinksRoute
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Android Digital Asset Links
|
||||||
|
/api/v1/auth/account:
|
||||||
|
delete:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: Completely delete the user account. This action is irreversible
|
||||||
|
and therefore requires additional password authentication.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Delete user account
|
||||||
|
operationId: DeleteUserAccountRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/deleteUserAccountPayload'
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: NoContent
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError, type `INVALID_PASSWORD`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
"401":
|
||||||
|
description: PublicHTTPError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
"403":
|
||||||
|
description: PublicHTTPError, type `USER_DEACTIVATED`/`NOT_LOCAL_USER`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/api/v1/auth/change-password:
|
||||||
|
post:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: |-
|
||||||
|
After successful password change, all current access and refresh tokens are
|
||||||
|
invalidated and a new set of auth tokens is returned
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Change local user's password
|
||||||
|
operationId: PostChangePasswordRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postChangePasswordPayload'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLoginResponse'
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError, type `INVALID_PASSWORD`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
"401":
|
||||||
|
description: PublicHTTPError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
"403":
|
||||||
|
description: PublicHTTPError, type `USER_DEACTIVATED`/`NOT_LOCAL_USER`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/api/v1/auth/forgot-password:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Initiates a password reset for a local user, sending an email with a password
|
||||||
|
reset link to the provided email address if a user account exists. Will always
|
||||||
|
succeed, even if no user was found in order to prevent user enumeration
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Initiate password reset for local user
|
||||||
|
operationId: PostForgotPasswordRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postForgotPasswordPayload'
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Success
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
/api/v1/auth/forgot-password/complete:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Completes a password reset for a local user, using the password reset token sent via email
|
||||||
|
to confirm user access, setting the new password if successful. All current access and refresh
|
||||||
|
tokens are invalidated and a new set of auth tokens is returned
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Completes password reset for local user
|
||||||
|
operationId: PostForgotPasswordCompleteRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postForgotPasswordCompletePayload'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLoginResponse'
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError, type `INVALID_PASSWORD`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
"403":
|
||||||
|
description: PublicHTTPError, type `USER_DEACTIVATED`/`NOT_LOCAL_USER`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
"404":
|
||||||
|
description: PublicHTTPError, type `TOKEN_NOT_FOUND`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
"409":
|
||||||
|
description: PublicHTTPError, type `TOKEN_EXPIRED`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/api/v1/auth/login:
|
||||||
|
post:
|
||||||
|
description: Returns an access and refresh token on successful authentication
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Login with local user
|
||||||
|
operationId: PostLoginRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLoginPayload'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLoginResponse'
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
"401":
|
||||||
|
description: PublicHTTPError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
"403":
|
||||||
|
description: PublicHTTPError, type `USER_DEACTIVATED`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/api/v1/auth/logout:
|
||||||
|
post:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: |-
|
||||||
|
Logs the local user out, destroying the provided access token.
|
||||||
|
A refresh token can optionally be provided, destroying it as well if found.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Log out local user
|
||||||
|
operationId: PostLogoutRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLogoutPayload'
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Success
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
"401":
|
||||||
|
description: PublicHTTPError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/api/v1/auth/refresh:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Returns a fresh set of access and refresh tokens if a valid refresh token was provided.
|
||||||
|
The old refresh token used to authenticate the request will be invalidated.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Refresh tokens
|
||||||
|
operationId: PostRefreshRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postRefreshPayload'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLoginResponse'
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
"401":
|
||||||
|
description: PublicHTTPError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
"403":
|
||||||
|
description: PublicHTTPError, type `USER_DEACTIVATED`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/api/v1/auth/register:
|
||||||
|
get:
|
||||||
|
produces:
|
||||||
|
- text/html
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Page to complete registration
|
||||||
|
operationId: GetCompleteRegisterRoute
|
||||||
|
parameters:
|
||||||
|
- type: string
|
||||||
|
format: uuid4
|
||||||
|
description: Registration token to complete the registration process
|
||||||
|
name: token
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Page to complete registration
|
||||||
|
post:
|
||||||
|
description: |
|
||||||
|
Registers a local user. If the registration process requires a confirmation
|
||||||
|
the status code `202` is returned without auth tokens. Afterwards the registration needs to be confirmed
|
||||||
|
using the `POST /api/v1/auth/register/{registrationToken}` endpoint.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Registers a local user
|
||||||
|
operationId: PostRegisterRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postRegisterPayload'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLoginResponse'
|
||||||
|
"202":
|
||||||
|
description: RegisterResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/registerResponse'
|
||||||
|
"400":
|
||||||
|
description: PublicHTTPValidationError, type `INVALID_PASSWORD`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
"409":
|
||||||
|
description: PublicHTTPError, type `USER_ALREADY_EXISTS`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/api/v1/auth/register/{registrationToken}:
|
||||||
|
post:
|
||||||
|
description: Completes registration for a local user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Complete registration
|
||||||
|
operationId: PostCompleteRegisterRoute
|
||||||
|
parameters:
|
||||||
|
- type: string
|
||||||
|
format: uuid4
|
||||||
|
description: Registration token to complete the registration process
|
||||||
|
name: registrationToken
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: PostLoginResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/postLoginResponse'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
/api/v1/auth/userinfo:
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: |-
|
||||||
|
Returns user information compatible with the OpenID Connect Core 1.0 specification.
|
||||||
|
Information returned depends on the requesting user as some data is only available if an app user profile exists.
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
summary: Get user info
|
||||||
|
operationId: GetUserInfoRoute
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: GetUserInfoResponse
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/getUserInfoResponse'
|
||||||
|
/api/v1/push/token:
|
||||||
|
put:
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
description: |-
|
||||||
|
Adds a push token for the given provider to the current user.
|
||||||
|
If the oldToken is present it will be deleted.
|
||||||
|
Currently only the provider 'fcm' is supported.
|
||||||
|
tags:
|
||||||
|
- push
|
||||||
|
summary: Adds a push token to the user
|
||||||
|
operationId: PutUpdatePushTokenRoute
|
||||||
|
parameters:
|
||||||
|
- name: Payload
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/putUpdatePushTokenPayload'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
"404":
|
||||||
|
description: PublicHTTPError, type `OLD_PUSH_TOKEN_NOT_FOUND`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
"409":
|
||||||
|
description: PublicHTTPError, type `PUSH_TOKEN_ALREADY_EXISTS`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
/swagger.yml:
|
||||||
|
get:
|
||||||
|
description: |-
|
||||||
|
OpenAPI Specification ver. 2 (fka Swagger)
|
||||||
|
Returns our handcrafted and validated `swagger.yml`.
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
tags:
|
||||||
|
- common
|
||||||
|
summary: Get swagger.yml
|
||||||
|
operationId: GetSwaggerRoute
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
definitions:
|
||||||
|
deleteUserAccountPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- currentPassword
|
||||||
|
properties:
|
||||||
|
currentPassword:
|
||||||
|
description: Current password of user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
getUserInfoResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- sub
|
||||||
|
- updated_at
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
description: Email address of user, if available
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 255
|
||||||
|
example: user@example.com
|
||||||
|
scopes:
|
||||||
|
description: Auth-Scopes of the user, if available
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- app
|
||||||
|
- cms
|
||||||
|
example:
|
||||||
|
- app
|
||||||
|
sub:
|
||||||
|
description: ID of user
|
||||||
|
type: string
|
||||||
|
example: 82ebdfad-c586-4407-a873-4cc1c33d56fc
|
||||||
|
updated_at:
|
||||||
|
description: Unix timestamp the user's info was last updated at
|
||||||
|
type: integer
|
||||||
|
example: 1591960808
|
||||||
|
httpValidationErrorDetail:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- key
|
||||||
|
- in
|
||||||
|
- error
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
description: Error describing field validation failure
|
||||||
|
type: string
|
||||||
|
in:
|
||||||
|
description: Indicates how the invalid field was provided
|
||||||
|
type: string
|
||||||
|
key:
|
||||||
|
description: Key of field failing validation
|
||||||
|
type: string
|
||||||
|
orderDir:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- asc
|
||||||
|
- desc
|
||||||
|
postChangePasswordPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- currentPassword
|
||||||
|
- newPassword
|
||||||
|
properties:
|
||||||
|
currentPassword:
|
||||||
|
description: Current password of user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
newPassword:
|
||||||
|
description: New password to set for user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
postForgotPasswordCompletePayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- token
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
password:
|
||||||
|
description: New password to set for user
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
token:
|
||||||
|
description: Password reset token sent via email
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: ec16f032-3c44-4148-bbcc-45557466fa74
|
||||||
|
postForgotPasswordPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
description: Username to initiate password reset for
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
example: user@example.com
|
||||||
|
postLoginPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
password:
|
||||||
|
description: Password of user to authenticate as
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
username:
|
||||||
|
description: Username of user to authenticate as
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
example: user@example.com
|
||||||
|
postLoginResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- access_token
|
||||||
|
- token_type
|
||||||
|
- expires_in
|
||||||
|
- refresh_token
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
description: Access token required for accessing protected API endpoints
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: c1247d8d-0d65-41c4-bc86-ec041d2ac437
|
||||||
|
expires_in:
|
||||||
|
description: Access token expiry in seconds
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
example: 86400
|
||||||
|
refresh_token:
|
||||||
|
description: Refresh token for refreshing the access token once it expires
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: 1dadb3bd-50d8-485d-83a3-6111392568f0
|
||||||
|
token_type:
|
||||||
|
description: Type of access token, will always be `bearer`
|
||||||
|
type: string
|
||||||
|
example: bearer
|
||||||
|
postLogoutPayload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
refresh_token:
|
||||||
|
description: Optional refresh token to delete while logging out
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: 700ebed3-40f7-4211-bc83-a89b22b9875e
|
||||||
|
postRefreshPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- refresh_token
|
||||||
|
properties:
|
||||||
|
refresh_token:
|
||||||
|
description: Refresh token to use for retrieving new token set
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
example: 7503cd8a-c921-4368-a32d-6c1d01d86da9
|
||||||
|
postRegisterPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
password:
|
||||||
|
description: Password to register with
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
minLength: 1
|
||||||
|
example: correct horse battery staple
|
||||||
|
username:
|
||||||
|
description: Username to register with
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
example: user@example.com
|
||||||
|
publicHttpError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
- title
|
||||||
|
properties:
|
||||||
|
detail:
|
||||||
|
description: More detailed, human-readable, optional explanation of the error
|
||||||
|
type: string
|
||||||
|
example: User is lacking permission to access this resource
|
||||||
|
status:
|
||||||
|
description: HTTP status code returned for the error
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
maximum: 599
|
||||||
|
minimum: 100
|
||||||
|
x-go-name: Code
|
||||||
|
example: 403
|
||||||
|
title:
|
||||||
|
description: Short, human-readable description of the error
|
||||||
|
type: string
|
||||||
|
example: Forbidden
|
||||||
|
type:
|
||||||
|
$ref: '#/definitions/publicHttpErrorType'
|
||||||
|
publicHttpErrorType:
|
||||||
|
description: Type of error returned, should be used for client-side error handling
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- generic
|
||||||
|
- PUSH_TOKEN_ALREADY_EXISTS
|
||||||
|
- OLD_PUSH_TOKEN_NOT_FOUND
|
||||||
|
- ZERO_FILE_SIZE
|
||||||
|
- USER_DEACTIVATED
|
||||||
|
- INVALID_PASSWORD
|
||||||
|
- NOT_LOCAL_USER
|
||||||
|
- TOKEN_NOT_FOUND
|
||||||
|
- TOKEN_EXPIRED
|
||||||
|
- USER_ALREADY_EXISTS
|
||||||
|
- MALFORMED_TOKEN
|
||||||
|
- LAST_AUTHENTICATED_AT_EXCEEDED
|
||||||
|
- MISSING_SCOPES
|
||||||
|
publicHttpValidationError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- validationErrors
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/publicHttpError'
|
||||||
|
properties:
|
||||||
|
validationErrors:
|
||||||
|
description: List of errors received while validating payload against schema
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/httpValidationErrorDetail'
|
||||||
|
putUpdatePushTokenPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- newToken
|
||||||
|
- provider
|
||||||
|
properties:
|
||||||
|
newToken:
|
||||||
|
description: New push token for given provider.
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
example: 1c91e550-8167-439c-8021-dee7de2f7e96
|
||||||
|
oldToken:
|
||||||
|
description: Old token that can be deleted if present.
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
x-nullable: true
|
||||||
|
example: 495179de-b771-48f0-aab2-8d23701b0f02
|
||||||
|
provider:
|
||||||
|
description: Identifier of the provider the token is for (eg. "fcm", "apn").
|
||||||
|
Currently only "fcm" is supported.
|
||||||
|
type: string
|
||||||
|
maxLength: 500
|
||||||
|
example: fcm
|
||||||
|
registerResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- requiresConfirmation
|
||||||
|
properties:
|
||||||
|
requiresConfirmation:
|
||||||
|
description: Indicates whether the registration process requires email confirmation
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
parameters:
|
||||||
|
registrationTokenParam:
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
|
description: Registration token to complete the registration process
|
||||||
|
name: registrationToken
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
AuthForbiddenResponse:
|
||||||
|
description: PublicHTTPError, type `USER_DEACTIVATED`/`NOT_LOCAL_USER`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
AuthUnauthorizedResponse:
|
||||||
|
description: PublicHTTPError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpError'
|
||||||
|
InvalidPasswordResponse:
|
||||||
|
description: PublicHTTPValidationError, type `INVALID_PASSWORD`
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
ValidationError:
|
||||||
|
description: PublicHTTPValidationError
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/publicHttpValidationError'
|
||||||
|
securityDefinitions:
|
||||||
|
Bearer:
|
||||||
|
description: |-
|
||||||
|
Access token for application access, **must** include "Bearer " prefix.
|
||||||
|
Example: `Bearer b4a94a42-3ea2-4af3-9699-8bcbfee6e6d2`
|
||||||
|
type: apiKey
|
||||||
|
name: Authorization
|
||||||
|
in: header
|
||||||
|
x-keyPrefix: 'Bearer '
|
||||||
|
Management:
|
||||||
|
description: Management secret, used for monitoring and infrastructure related
|
||||||
|
calls
|
||||||
|
type: apiKey
|
||||||
|
name: mgmt-secret
|
||||||
|
in: query
|
||||||
5
api/templates/server/builder.README
Normal file
5
api/templates/server/builder.README
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
https://raw.githubusercontent.com/go-swagger/go-swagger/master/generator/templates/server/builder.gotmpl
|
||||||
|
Based on commit in 78f49e7e72c09ff0e39251f002bba729b861e756
|
||||||
|
Feb 24, 2020
|
||||||
|
|
||||||
|
Note that our `builder.gotmpl` is minimal and only used for validating if any routes without a swagger spec exist.
|
||||||
49
api/templates/server/builder.gotmpl
Normal file
49
api/templates/server/builder.gotmpl
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Code generated by go-swagger; DO NOT EDIT.
|
||||||
|
|
||||||
|
|
||||||
|
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
package types
|
||||||
|
{{ $package := "types" }}
|
||||||
|
|
||||||
|
// This file was generated by the swagger tool.
|
||||||
|
// Editing this file might prove futile when you re-run the swagger generate command
|
||||||
|
|
||||||
|
// NewSwaggerSpec creates a new SwaggerSpec instance
|
||||||
|
func NewSwaggerSpec() *SwaggerSpec {
|
||||||
|
|
||||||
|
spec := &SwaggerSpec{
|
||||||
|
Handlers: make(map[string]map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(spec.Handlers) == 0 {
|
||||||
|
spec.initHandlerCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
/*SwaggerSpec {{ if .Info }}{{ if .Info.Description }}{{.Info.Description}}{{ else }}the {{ humanize .Name }} API{{ end }}{{ end }} */
|
||||||
|
type SwaggerSpec struct {
|
||||||
|
Handlers map[string]map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ({{.ReceiverName}} *SwaggerSpec) initHandlerCache() {
|
||||||
|
{{- if .Operations }}
|
||||||
|
{{.ReceiverName}}.Handlers = make(map[string]map[string]bool)
|
||||||
|
|
||||||
|
// https://swagger.io/specification/v2/ fixed fields: GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH
|
||||||
|
{{.ReceiverName}}.Handlers["GET"] = make(map[string]bool)
|
||||||
|
{{.ReceiverName}}.Handlers["PUT"] = make(map[string]bool)
|
||||||
|
{{.ReceiverName}}.Handlers["POST"] = make(map[string]bool)
|
||||||
|
{{.ReceiverName}}.Handlers["DELETE"] = make(map[string]bool)
|
||||||
|
{{.ReceiverName}}.Handlers["OPTIONS"] = make(map[string]bool)
|
||||||
|
{{.ReceiverName}}.Handlers["HEAD"] = make(map[string]bool)
|
||||||
|
{{.ReceiverName}}.Handlers["PATCH"] = make(map[string]bool)
|
||||||
|
{{ range .Operations }}
|
||||||
|
{{.ReceiverName}}.Handlers[{{ printf "%q" (upper .Method) }}][{{ if eq .Path "/" }}""{{ else }}{{ printf "%q" (cleanPath .Path) }}{{ end }}] = true
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
|
||||||
3
api/templates/server/parameter.README
Normal file
3
api/templates/server/parameter.README
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
https://github.com/go-swagger/go-swagger/blob/master/generator/templates/server/parameter.gotmpl
|
||||||
|
Based on commit in b1ca570cfcd2e281d445e0049506f18fa1c2c2cc
|
||||||
|
Jun 13, 2020
|
||||||
759
api/templates/server/parameter.gotmpl
Normal file
759
api/templates/server/parameter.gotmpl
Normal file
@@ -0,0 +1,759 @@
|
|||||||
|
{{ define "bindprimitiveparam" }}{{/* an empty test definition to test template repo dependencies resolution - DO NOT CHANGE THIS */}}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "bodyvalidator" }}
|
||||||
|
{{- if .HasModelBodyParams }}
|
||||||
|
// validate body object{{/* delegate validation to model object */}}
|
||||||
|
if err := body.Validate(route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) == 0 {
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
}
|
||||||
|
{{- else if and .HasSimpleBodyParams .HasModelBodyItems }}
|
||||||
|
|
||||||
|
{{- if or .Schema.HasSliceValidations .Schema.Items.HasValidations }}
|
||||||
|
// validate array of body objects
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Schema.HasSliceValidations }}
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
{{ template "sliceparamvalidator" . }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if and .Schema.Items.HasValidations (not (or .Schema.Items.IsInterface .Schema.Items.IsStream)) }}
|
||||||
|
for {{ .IndexVar }} := range body {
|
||||||
|
{{- if .Schema.Items.IsNullable }}
|
||||||
|
if body[{{ .IndexVar }}] == nil {
|
||||||
|
{{- if .Schema.Items.Required }}
|
||||||
|
res = append(res, errors.Required({{ .Child.Path }}, {{ printf "%q" .Child.Location }}, body[{{ .IndexVar }}]))
|
||||||
|
break
|
||||||
|
{{- else }}
|
||||||
|
continue
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
if err := body[{{ .IndexVar }}].Validate(route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- if not .Schema.HasSliceValidations }}
|
||||||
|
if len(res) == 0 {
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
// no validation for items in this slice
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- else if and .HasSimpleBodyParams .HasModelBodyMap }}
|
||||||
|
|
||||||
|
{{- if and .Schema.HasValidations (not (or .Schema.AdditionalProperties.IsInterface .Schema.AdditionalProperties.IsStream)) }}
|
||||||
|
// validate map of body objects
|
||||||
|
for {{ .KeyVar }} := range body {
|
||||||
|
{{- if .Schema.AdditionalProperties.Required }}
|
||||||
|
if err := validate.Required({{ if .Child.Path }}{{ .Child.Path }}{{ else }}""{{ end }}, {{ printf "%q" .Child.Location }}, {{ if not .IsAnonymous }}{{ .Schema.GoType }}({{ end }}body[{{ .KeyVar }}]{{ if not .IsAnonymous }}){{ end }}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .Schema.AdditionalProperties.IsNullable (not .IsMapNullOverride) }}
|
||||||
|
if body[{{ .KeyVar }}] == nil {
|
||||||
|
{{- if .Schema.AdditionalProperties.Required }}
|
||||||
|
res = append(res, errors.Required({{ .Path }}, {{ printf "%q" .Location }}, body[{{ .KeyVar }}]))
|
||||||
|
break
|
||||||
|
{{- else }}
|
||||||
|
continue
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
if val , ok :=body[{{ .KeyVar }}]; ok {
|
||||||
|
{{- if and .IsNullable (not .IsMapNullOverride) }}
|
||||||
|
if val != nil {
|
||||||
|
{{- end }}
|
||||||
|
if err := val.Validate(route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
{{- if and .IsNullable (not .IsMapNullOverride) }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) == 0 {
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
// no validation for this map
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
{{- end }}
|
||||||
|
{{- else if .HasSimpleBodyParams }}
|
||||||
|
{{- if and (not .IsArray) (not .IsMap) .Schema.HasValidations }}
|
||||||
|
// validate inline body
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}Body(route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{- else if and (or .IsArray .IsMap) .Schema.HasValidations }}
|
||||||
|
// validate inline body {{ if .IsArray }}array{{ else }}map{{ end }}
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}Body(route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
// no validation required on inline body
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
{{- end}}
|
||||||
|
{{- else }}
|
||||||
|
{{- if .IsInterface }}
|
||||||
|
// no validation on generic interface
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "sliceparamvalidator"}}
|
||||||
|
{{ if or .MinItems .MaxItems }}
|
||||||
|
{{ camelize .Name }}Size := int64(len({{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{ if and .Child (not (hasPrefix .ValueExpression "o.")) }}{{ .Child.ValueExpression }}C{{ else }}{{ .ValueExpression }}{{ end }}))
|
||||||
|
{{ end }}
|
||||||
|
{{ if .MinItems }}
|
||||||
|
// {{ .ItemsDepth }}minItems: {{ .MinItems }}
|
||||||
|
if err := validate.MinItems({{ .Path }}, {{ printf "%q" .Location }}, {{ camelize .Name }}Size, {{ .MinItems }}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .MaxItems }}
|
||||||
|
// {{ .ItemsDepth }}maxItems: {{ .MaxItems }}
|
||||||
|
if err := validate.MaxItems({{ .Path }}, {{ printf "%q" .Location }}, {{ camelize .Name }}Size, {{.MaxItems}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .UniqueItems }}
|
||||||
|
// {{ .ItemsDepth }}uniqueItems: true
|
||||||
|
if err := validate.UniqueItems({{ .Path }}, {{ printf "%q" .Location }}, {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{ if and .Child (not ( hasPrefix .ValueExpression "o." )) }}{{ .Child.ValueExpression }}C{{ else }}{{ .ValueExpression }}{{ end }}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Enum }}
|
||||||
|
// {{ .ItemsDepth }}Enum: {{ .Enum }}
|
||||||
|
if err := validate.EnumCase(
|
||||||
|
{{- .Path }}, {{ printf "%q" .Location }},
|
||||||
|
{{- if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end -}}
|
||||||
|
{{- if .Child -}}
|
||||||
|
{{- if not ( hasPrefix .ValueExpression "o." ) -}}
|
||||||
|
{{- .Child.ValueExpression }}C{{- if .IsCustomFormatter }}.String(){{ end -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- .ValueExpression -}}{{- if .Child.IsCustomFormatter }}.String(){{ end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}},
|
||||||
|
{{- printf "%#v" .Enum -}}, {{ if .IsEnumCI }}false{{ else }}true{{ end }}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{/* BEGIN CUSTOM BY AAA */}}
|
||||||
|
{{- if .IsArray }}{{/* slice validations */}}
|
||||||
|
for {{ if .Child.NeedsIndex }}{{ .IndexVar }}{{ else }}_{{ end }}, {{ varname .Child.ValueExpression }}V := range {{ varname .Child.ValueExpression }}C {
|
||||||
|
{{- if .Child.IsArray }}{{/* recursive resolution of arrays in params */}}
|
||||||
|
{{- if not .Child.SkipParse }}
|
||||||
|
// {{ .Child.ItemsDepth }}CollectionFormat: {{ .Child.CollectionFormat }}
|
||||||
|
{{- end }}
|
||||||
|
{{ .Child.Child.ValueExpression }}C := {{ if .Child.SkipParse }}{{ varname .Child.ValueExpression }}V{{ else }}swag.SplitByFormat({{ varname .Child.ValueExpression }}V, {{ printf "%q" .Child.CollectionFormat }}){{ end }}
|
||||||
|
{{- if .Child.HasSliceValidations }}
|
||||||
|
{{- template "sliceparamvalidator" .Child }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if .Child.IsMap }}{{/* simple map in items (possible with body params)*/}}
|
||||||
|
{{ .Child.Child.ValueExpression }}C := {{ varname .Child.ValueExpression }}V
|
||||||
|
{{- template "mapparamvalidator" .Child }}
|
||||||
|
{{- else }}{{/* non-array && non-map type in items */}}
|
||||||
|
{{- if and .Child.IsNullable (not .IsMapNullOverride) }}
|
||||||
|
if {{ varname .Child.ValueExpression }}V == nil {
|
||||||
|
{{- if .Child.Required }}
|
||||||
|
return errors.Required({{ .Child.Path }}, {{ printf "%q" .Child.Location }}, {{ varname .Child.ValueExpression }}V)
|
||||||
|
{{- else }}
|
||||||
|
continue
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- template "childvalidator" .Child }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{/* END CUSTOM BY AAA */}}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{- define "childvalidator" }}
|
||||||
|
{{- if .Converter }}
|
||||||
|
{{- if ne .SwaggerFormat "" }}
|
||||||
|
// {{ .ItemsDepth }}Format: {{ printf "%q" .SwaggerFormat }}
|
||||||
|
{{- end }}
|
||||||
|
{{ varname .ValueExpression }}, err := {{ .Converter }}({{ varname .ValueExpression }}V)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, "{{ .GoType }}", {{ varname .ValueExpression }})
|
||||||
|
}
|
||||||
|
{{- else if and .IsCustomFormatter (not .SkipParse) }}{{/* parsing is skipped for simple body items */}}
|
||||||
|
// {{ .ItemsDepth }}Format: {{ printf "%q" .SwaggerFormat }}
|
||||||
|
value, err := formats.Parse({{ printf "%q" .SwaggerFormat }},{{ varname .ValueExpression }}V)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, "{{ .GoType }}", value)
|
||||||
|
}
|
||||||
|
{{ varname .ValueExpression }} := *(value.(*{{.GoType}}))
|
||||||
|
{{- else if and .IsComplexObject .HasValidations }}{{/* dedicated to nested body params */}}
|
||||||
|
{{ varname .ValueExpression }} := {{ varname .ValueExpression }}V
|
||||||
|
if err := {{ .ValueExpression }}.Validate(formats) ; err != nil {
|
||||||
|
if ve, ok := err.(*errors.Validation); ok {
|
||||||
|
return ve.ValidateName({{ .Path }})
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
{{ varname .ValueExpression }} := {{ varname .ValueExpression }}V
|
||||||
|
{{- end }}
|
||||||
|
{{ template "propertyparamvalidator" . }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "mapparamvalidator" }}
|
||||||
|
{{- if and .Child.HasValidations (not (or .Child.IsInterface .Child.IsStream)) }}
|
||||||
|
|
||||||
|
// validations for map
|
||||||
|
{{- else }}
|
||||||
|
|
||||||
|
// map has no validations: copying all elements
|
||||||
|
{{- end }}
|
||||||
|
{{ varname .Child.ValueExpression }}R := make({{ .GoType }},len({{ .Child.ValueExpression }}C))
|
||||||
|
for {{ .KeyVar }}, {{ .Child.ValueExpression }}V := range {{ .Child.ValueExpression}}C {
|
||||||
|
{{- if .Child.IsArray }}
|
||||||
|
{{ .Child.Child.ValueExpression }}C := {{ varname .Child.ValueExpression }}V
|
||||||
|
{{- if .Child.HasSliceValidations }}
|
||||||
|
{{- template "sliceparamvalidator" .Child }}
|
||||||
|
{{- end }}
|
||||||
|
{{- template "sliceparambinder" .Child }}
|
||||||
|
{{- else if .Child.IsMap }}
|
||||||
|
{{ .Child.Child.ValueExpression }}C := {{ varname .Child.ValueExpression }}V
|
||||||
|
{{ template "mapparamvalidator" .Child }}
|
||||||
|
{{- else }}
|
||||||
|
{{- if and .Child.IsNullable }}
|
||||||
|
if {{ varname .Child.ValueExpression }}V == nil {
|
||||||
|
{{- if .Child.Required }}
|
||||||
|
return errors.Required({{ .Child.Path }}, {{ printf "%q" .Child.Location }}, {{ varname .Child.ValueExpression }}V)
|
||||||
|
{{- else }}
|
||||||
|
continue
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- template "childvalidator" .Child }}
|
||||||
|
{{- end }}
|
||||||
|
{{ varname .Child.ValueExpression }}R[{{.KeyVar}}] = {{ varname .Child.ValueExpression }}{{ if or .Child.IsArray .Child.IsMap}}IR{{end}}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "propertyparamvalidator" }}
|
||||||
|
{{/* BEGIN CUSTOM BY AAA */}}
|
||||||
|
{{- if .IsNullable }}
|
||||||
|
// Required: {{ if .Required }}true{{ else }}false{{ end }}
|
||||||
|
if {{.ValueExpression}} == nil {
|
||||||
|
{{- if .Required }}
|
||||||
|
return errors.Required({{ printf "%q" (camelize .Name) }}, {{ printf "%q" .Location }}, "")
|
||||||
|
{{- else }}
|
||||||
|
return nil
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .IsPrimitive }}
|
||||||
|
{{ template "validationPrimitive" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .IsCustomFormatter (not .IsStream) (not .IsBase64) }}
|
||||||
|
if err := validate.FormatOf({{.Path}}, "{{.Location}}", "{{.SwaggerFormat}}", {{ if .IsNullable }}(*{{ end }}{{.ValueExpression}}{{ if .IsNullable }}){{ end }}{{ if .IsCustomFormatter }}.String(){{ end }}, formats); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .IsArray }}{{/* slice validations */}}
|
||||||
|
{{ varname .Child.ValueExpression }}C := {{ .ValueExpression }}
|
||||||
|
{{/* END CUSTOM BY AAA */}}
|
||||||
|
{{ template "sliceparamvalidator" . }}
|
||||||
|
{{- else if .IsMap }}
|
||||||
|
{{ .Child.ValueExpression }}C := {{ varname .Child.ValueExpression }}V
|
||||||
|
{{ template "mapparamvalidator" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "sliceparambinder" }}
|
||||||
|
var {{ varname .Child.ValueExpression }}R {{ .GoType }}
|
||||||
|
for {{ if .Child.NeedsIndex }}{{ .IndexVar }}{{ else }}_{{ end }}, {{ varname .Child.ValueExpression }}V := range {{ varname .Child.ValueExpression }}C {
|
||||||
|
{{- if .Child.IsArray }}{{/* recursive resolution of arrays in params */}}
|
||||||
|
{{- if not .Child.SkipParse }}
|
||||||
|
// {{ .Child.ItemsDepth }}CollectionFormat: {{ .Child.CollectionFormat }}
|
||||||
|
{{- end }}
|
||||||
|
{{ .Child.Child.ValueExpression }}C := {{ if .Child.SkipParse }}{{ varname .Child.ValueExpression }}V{{ else }}swag.SplitByFormat({{ varname .Child.ValueExpression }}V, {{ printf "%q" .Child.CollectionFormat }}){{ end }}
|
||||||
|
{{- if .Child.HasSliceValidations }}
|
||||||
|
{{- template "sliceparamvalidator" .Child }}
|
||||||
|
{{- end }}
|
||||||
|
if len({{ varname .Child.Child.ValueExpression }}C) > 0 {
|
||||||
|
{{ template "sliceparambinder" .Child }}
|
||||||
|
{{ varname .Child.ValueExpression }}R = append({{ varname .Child.ValueExpression }}R, {{ varname .Child.ValueExpression }}{{ if or .Child.IsArray .Child.IsMap }}IR{{end}})
|
||||||
|
}
|
||||||
|
{{- else if .Child.IsMap }}{{/* simple map in items (possible with body params)*/}}
|
||||||
|
{{ .Child.Child.ValueExpression }}C := {{ varname .Child.ValueExpression }}V
|
||||||
|
{{- template "mapparamvalidator" .Child }}
|
||||||
|
{{ varname .Child.ValueExpression }}R = append({{ varname .Child.ValueExpression }}R, {{ varname .Child.ValueExpression }}{{ if or .Child.IsArray .Child.IsMap }}IR{{end}})
|
||||||
|
{{- else }}{{/* non-array && non-map type in items */}}
|
||||||
|
{{- if and .Child.IsNullable (not .IsMapNullOverride) }}
|
||||||
|
if {{ varname .Child.ValueExpression }}V == nil {
|
||||||
|
{{- if .Child.Required }}
|
||||||
|
return errors.Required({{ .Child.Path }}, {{ printf "%q" .Child.Location }}, {{ varname .Child.ValueExpression }}V)
|
||||||
|
{{- else }}
|
||||||
|
continue
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- template "childvalidator" .Child }}
|
||||||
|
{{ varname .Child.ValueExpression }}R = append({{ varname .Child.ValueExpression }}R, {{ varname .Child.ValueExpression }}{{ if or .Child.IsArray .Child.IsMap }}IR{{end}})
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{/* BEGIN CUSTOM BY AAA */}}
|
||||||
|
{{- define "customsliceparamvalidator" }}
|
||||||
|
{{- template "sliceparamvalidator" . -}}
|
||||||
|
for _, {{ varname .Child.ValueExpression }}V := range {{ varname .Child.ValueExpression }}C {
|
||||||
|
{{- if .Child.IsArray }}{{/* recursive resolution of arrays in params */}}
|
||||||
|
{{ varname .Child.ValueExpression }}IC := {{ varname .Child.ValueExpression }}V
|
||||||
|
{{- template "customsliceparamvalidator" .Child }}
|
||||||
|
{{- else if .Child.IsCustomFormatter }}
|
||||||
|
// Format: {{ .Child.SwaggerFormat }}
|
||||||
|
if err := validate.FormatOf({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .SwaggerFormat }}, {{ varname .Child.ValueExpression }}V.String(), formats); err != nil {
|
||||||
|
res = append(res, errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .GoType }}, {{ varname .Child.ValueExpression }}V))
|
||||||
|
}
|
||||||
|
{{- end -}}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{/* END CUSTOM BY AAA */}}
|
||||||
|
// Code generated by go-swagger; DO NOT EDIT.
|
||||||
|
|
||||||
|
|
||||||
|
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
package {{ .Package }}
|
||||||
|
|
||||||
|
// This file was generated by the swagger tool.
|
||||||
|
// Editing this file might prove futile when you re-run the swagger generate command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-openapi/errors"
|
||||||
|
"github.com/go-openapi/runtime"
|
||||||
|
"github.com/go-openapi/runtime/security"
|
||||||
|
"github.com/go-openapi/runtime/middleware"
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/go-openapi/validate"
|
||||||
|
|
||||||
|
{{ imports .DefaultImports }}
|
||||||
|
{{ imports .Imports }}
|
||||||
|
)
|
||||||
|
|
||||||
|
// New{{ pascalize .Name }}Params creates a new {{ pascalize .Name }}Params object
|
||||||
|
{{ if .Params.HasSomeDefaults -}}
|
||||||
|
// with the default values initialized.{{ else -}}
|
||||||
|
// no default values defined in spec.
|
||||||
|
{{- end }}
|
||||||
|
func New{{ pascalize .Name }}Params() {{ pascalize .Name }}Params {
|
||||||
|
{{ if .Params.HasSomeDefaults }}
|
||||||
|
var (
|
||||||
|
// initialize parameters with default values
|
||||||
|
{{ range .Params }}
|
||||||
|
{{ if .HasDefault -}}
|
||||||
|
{{ if not .IsFileParam }}{{ varname .ID}}Default =
|
||||||
|
{{- if and .IsPrimitive .IsCustomFormatter (not (stringContains .Zero "(\"" )) }}{{ .Zero }}{{/* strfmt type initializer requires UnmarshalText(), e.g. Date, Datetime, Duration */}}
|
||||||
|
{{- else if and .IsPrimitive .IsCustomFormatter (stringContains .Zero "(\"" ) }}{{.GoType}}({{- printf "%#v" .Default }}){{/* strfmt type initializer takes string */}}
|
||||||
|
{{- else if and .IsPrimitive (not .IsCustomFormatter) -}}{{.GoType}}({{- printf "%#v" .Default }}){{/* regular go primitive type initializer */}}
|
||||||
|
{{- else if .IsArray -}}{{- /* Do not initialize from possible defaults in nested arrays */ -}}
|
||||||
|
{{- if and .Child.IsPrimitive .Child.IsCustomFormatter }}{{ .Zero }}{{/* initialization strategy with UnmarshalText() */}}
|
||||||
|
{{- else if .Child.IsArray -}}{{ .Zero }}{{/* initialization strategy with json.Unmarshal() */}}
|
||||||
|
{{- else if and .Child.IsPrimitive (not .Child.IsCustomFormatter) -}}{{.GoType}}{{- arrayInitializer .Default }}{{/* regular go primitive type initializer: simple slice initializer */}}
|
||||||
|
{{- else }}{{ printf "%#v" .Default }}{{/* all other cases (e.g. schema) [should not occur] */}}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}{{ printf "%#v" .Default }}{{/* case .Schema */}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
)
|
||||||
|
|
||||||
|
{{ range .Params }}{{ if .HasDefault -}}{{- /* carry on UnmarshalText initialization strategy */ -}}
|
||||||
|
{{ if and .IsPrimitive .IsCustomFormatter (not (stringContains .Zero "(\"")) }}{{ varname .ID}}Default.UnmarshalText([]byte({{ printf "%q" .Default }}))
|
||||||
|
{{ else if .IsArray -}}
|
||||||
|
{{ if or ( and .Child.IsPrimitive .Child.IsCustomFormatter ) .Child.IsArray -}}
|
||||||
|
if err := json.Unmarshal([]byte(`{{printf "%s" (json .Default)}}`), &{{ varname .ID }}Default); err != nil {
|
||||||
|
// panics if specification is invalid
|
||||||
|
msg := fmt.Sprintf("invalid default value for parameter {{ varname .ID }}: %v",err)
|
||||||
|
panic(msg)
|
||||||
|
}
|
||||||
|
{{ end -}}
|
||||||
|
{{- end }}
|
||||||
|
{{ end -}}
|
||||||
|
{{- end }}
|
||||||
|
{{ end }}
|
||||||
|
return {{ pascalize .Name }}Params{ {{ range .Params }}{{ if .HasDefault }}
|
||||||
|
{{ pascalize .ID}}: {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}&{{ end }}{{ varname .ID }}Default,
|
||||||
|
{{ end }}{{ end }} }
|
||||||
|
}
|
||||||
|
|
||||||
|
// {{ pascalize .Name }}Params contains all the bound params for the {{ humanize .Name }} operation
|
||||||
|
// typically these are obtained from a http.Request
|
||||||
|
//
|
||||||
|
// swagger:parameters {{ .Name }}
|
||||||
|
type {{ pascalize .Name }}Params struct {
|
||||||
|
|
||||||
|
// HTTP Request Object
|
||||||
|
HTTPRequest *http.Request `json:"-"`
|
||||||
|
|
||||||
|
{{ range .Params }}/*{{ if .Description }}{{ blockcomment .Description }}{{ end }}{{ if .Required }}
|
||||||
|
Required: true{{ end }}{{ if .Maximum }}
|
||||||
|
Maximum: {{ if .ExclusiveMaximum }}< {{ end }}{{ .Maximum }}{{ end }}{{ if .Minimum }}
|
||||||
|
Minimum: {{ if .ExclusiveMinimum }}> {{ end }}{{ .Minimum }}{{ end }}{{ if .MultipleOf }}
|
||||||
|
Multiple Of: {{ .MultipleOf }}{{ end }}{{ if .MaxLength }}
|
||||||
|
Max Length: {{ .MaxLength }}{{ end }}{{ if .MinLength }}
|
||||||
|
Min Length: {{ .MinLength }}{{ end }}{{ if .Pattern }}
|
||||||
|
Pattern: {{ .Pattern }}{{ end }}{{ if .MaxItems }}
|
||||||
|
Max Items: {{ .MaxItems }}{{ end }}{{ if .MinItems }}
|
||||||
|
Min Items: {{ .MinItems }}{{ end }}{{ if .UniqueItems }}
|
||||||
|
Unique: true{{ end }}{{ if .Location }}
|
||||||
|
In: {{ .Location }}{{ end }}{{ if .CollectionFormat }}
|
||||||
|
Collection Format: {{ .CollectionFormat }}{{ end }}{{ if .HasDefault }}
|
||||||
|
Default: {{ printf "%#v" .Default }}{{ end }}
|
||||||
|
*/
|
||||||
|
{{ if not .Schema }}{{ pascalize .ID }} {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{.GoType}}{{ else }}{{ pascalize .Name }} {{ if and (not .Schema.IsBaseType) .IsNullable (not .Schema.IsStream) }}*{{ end }}{{.GoType}}{{ end }}{{/* BEGIN CUSTOM BY AAA */}}{{ if .IsQueryParam }}`query:"{{ .Name }}"`{{ else if .IsPathParam }}`param:"{{ .Name }}"`{{ else if .IsFormParam }}`form:"{{ .Name }}"`{{ end }}{{/* END CUSTOM BY AAA */}}
|
||||||
|
{{ end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
|
||||||
|
// for simple values it will use straight method calls.
|
||||||
|
//
|
||||||
|
// To ensure default values, the struct must have been initialized with New{{ pascalize .Name }}Params() beforehand.
|
||||||
|
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
|
||||||
|
var res []error
|
||||||
|
|
||||||
|
{{ .ReceiverName }}.HTTPRequest = r
|
||||||
|
|
||||||
|
{{ if .HasQueryParams }}qs := runtime.Values(r.URL.Query()){{ end }}
|
||||||
|
|
||||||
|
{{ if .HasFormParams }}if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||||
|
if err != http.ErrNotMultipart {
|
||||||
|
return errors.New(400,"%v",err)
|
||||||
|
} else if err := r.ParseForm(); err != nil {
|
||||||
|
return errors.New(400,"%v",err)
|
||||||
|
}
|
||||||
|
}{{ if .HasFormValueParams }}
|
||||||
|
fds := runtime.Values(r.Form)
|
||||||
|
{{ end }}{{ end }}
|
||||||
|
|
||||||
|
{{ range .Params }}
|
||||||
|
{{ if not .IsArray -}}
|
||||||
|
{{ if .IsQueryParam }}q{{ pascalize .Name }}, qhk{{ pascalize .Name }}, _ := qs.GetOK({{ .Path }})
|
||||||
|
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(q{{ pascalize .Name }}, qhk{{ pascalize .Name }}, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ else if .IsPathParam }}r{{ pascalize .Name }}, rhk{{ pascalize .Name }}, _ := route.Params.GetOK({{ .Path }})
|
||||||
|
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(r{{ pascalize .Name }}, rhk{{ pascalize .Name }}, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ else if .IsHeaderParam }}if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(r.Header[http.CanonicalHeaderKey({{ .Path }})], true, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ else if .IsFormParam -}}
|
||||||
|
{{if .IsFileParam }}{{ camelize .Name }}, {{ camelize .Name }}Header, err := r.FormFile({{ .Path }})
|
||||||
|
if err != nil {{ if .IsNullable }}&& err != http.ErrMissingFile{{ end }}{
|
||||||
|
res = append(res, errors.New(400, "reading file %q failed: %v", {{ printf "%q" (camelize .Name) }}, err))
|
||||||
|
{{ if .IsNullable }}} else if err == http.ErrMissingFile {
|
||||||
|
// no-op for missing but optional file parameter
|
||||||
|
{{ end }}} else if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}({{ camelize .Name }}, {{ camelize .Name }}Header); err != nil {
|
||||||
|
{{ if .Required }}// Required: true{{ printf "\n" }}{{end }}res = append(res, err)
|
||||||
|
} else {
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = &runtime.File{Data: {{ camelize .Name }}, Header: {{ camelize .Name }}Header}
|
||||||
|
}
|
||||||
|
{{ else }}fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}, _ := fds.GetOK({{ .Path }})
|
||||||
|
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ end }}{{/* end .FileParam */}}
|
||||||
|
{{ end }}{{/* end not .Array */}}
|
||||||
|
{{ else if .IsArray -}}
|
||||||
|
{{ if .IsQueryParam }}q{{ pascalize .Name }}, qhk{{ pascalize .Name }}, _ := qs.GetOK({{ .Path }})
|
||||||
|
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(q{{ pascalize .Name }}, qhk{{ pascalize .Name }}, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ else if .IsPathParam }}r{{ pascalize .Name }}, rhk{{ pascalize .Name }}, _ := route.Params.GetOK({{ .Path }})
|
||||||
|
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(r{{ pascalize .Name }}, rhk{{ pascalize .Name }}, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ else if .IsHeaderParam }}if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(r.Header[http.CanonicalHeaderKey({{ .Path }})], true, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ else if and .IsFormParam }}fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}, _ := fds.GetOK({{ .Path }})
|
||||||
|
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{ end }}{{ end }}
|
||||||
|
{{ if and .IsBodyParam .Schema -}}
|
||||||
|
if runtime.HasBody(r) {
|
||||||
|
{{- if .Schema.IsStream }}
|
||||||
|
{{ .ReceiverName }}.{{ pascalize .Name }} = r.Body
|
||||||
|
{{- else }}
|
||||||
|
defer r.Body.Close()
|
||||||
|
{{- if and .Schema.IsBaseType .Schema.IsExported }}
|
||||||
|
body, err := {{ toPackageName .ModelsPackage }}.Unmarshal{{ dropPackage .GoType }}{{ if .IsArray }}Slice{{ end }}(r.Body, route.Consumer)
|
||||||
|
if err != nil {
|
||||||
|
{{- if .Required }}
|
||||||
|
if err == io.EOF {
|
||||||
|
err = errors.Required({{ .Path }}, {{ printf "%q" .Location }}, "")
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
res = append(res, err)
|
||||||
|
{{- else }}
|
||||||
|
var body {{ .GoType }}
|
||||||
|
if err := route.Consumer.Consume(r.Body, &body); err != nil {
|
||||||
|
{{- if .Required }}
|
||||||
|
if err == io.EOF {
|
||||||
|
res = append(res, errors.Required({{ printf "%q" (camelize .Name) }}, {{ printf "%q" .Location }}, ""))
|
||||||
|
} else {
|
||||||
|
{{- end }}
|
||||||
|
res = append(res, errors.NewParseError({{ printf "%q" (camelize .Name) }}, {{ printf "%q" .Location }}, "", err))
|
||||||
|
{{- if .Required }}
|
||||||
|
}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end }}
|
||||||
|
} else {
|
||||||
|
{{- template "bodyvalidator" . }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- if .Required }} else {
|
||||||
|
res = append(res, errors.Required({{ printf "%q" (camelize .Name) }}, {{ printf "%q" .Location }}, ""))
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
if len(res) > 0 {
|
||||||
|
return errors.CompositeValidationError(res...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/* BEGIN CUSTOM BY AAA */}}
|
||||||
|
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) Validate(formats strfmt.Registry) error {
|
||||||
|
var res []error
|
||||||
|
{{ range .Params }}
|
||||||
|
// {{ .Name }}
|
||||||
|
// Required: {{ .Required }}
|
||||||
|
{{ if .IsQueryParam }}// AllowEmptyValue: {{ .AllowEmptyValue }}{{ end -}}{{ if .IsPathParam }}// Parameter is provided by construction from the route{{ end -}}
|
||||||
|
{{/* file validations */}}
|
||||||
|
{{- if .IsFileParam }}
|
||||||
|
{{- if or .MinLength .MaxLength }}
|
||||||
|
size, _ := {{ .ReceiverName }}.{{ .ID }}.Seek(0, io.SeekEnd)
|
||||||
|
file.Seek(0, io.SeekStart)
|
||||||
|
{{- end }}
|
||||||
|
{{- if .MinLength}}
|
||||||
|
if size < {{.MinLength}} {
|
||||||
|
res = append(res, errors.ExceedsMinimum({{ .Path }}, {{ printf "%q" .Location }}, {{ .MinLength }}, false))
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .MaxLength}}
|
||||||
|
if size > {{.MaxLength}} {
|
||||||
|
res = append(res, errors.ExceedsMaximum({{ .Path }}, {{ printf "%q" .Location }}, {{ .MaxLength }}, false))
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{/* body validations, currently not used */}}
|
||||||
|
{{- else if .IsBodyParam }}
|
||||||
|
// body is validated in endpoint
|
||||||
|
//if err := {{ .ReceiverName }}.{{ .ID }}.Validate(formats); err != nil {
|
||||||
|
// res = append(res, err)
|
||||||
|
//}
|
||||||
|
{{/* validate for arrays */}}
|
||||||
|
{{- else if .IsArray }}
|
||||||
|
{{- if .HasSliceValidations }}
|
||||||
|
if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}(formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{- else if or .Child.IsCustomFormatter .Child.IsArray }}
|
||||||
|
{{ varname .Child.ValueExpression }}C := {{ .ValueExpression }}
|
||||||
|
{{- template "customsliceparamvalidator" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{/* path and query validaionts */}}
|
||||||
|
{{- else if not .IsBodyParam }}
|
||||||
|
{{/* if required check if exists */}}
|
||||||
|
{{- if and (not .IsPathParam) .Required (not .AllowEmptyValue) -}}
|
||||||
|
if err := validate.Required({{ .Path }}, {{ printf "%q" .Location }}, {{ .ReceiverName }}.{{ .ID }}); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{/* if a validation method exists for the property exists, call it */}}
|
||||||
|
{{- if .HasValidations }}
|
||||||
|
if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}(formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
{{- else if .IsCustomFormatter }}// Format: {{ .SwaggerFormat }}
|
||||||
|
if err := validate.FormatOf({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .SwaggerFormat }}, {{ .ReceiverName }}.{{ .ID }}, formats); err != nil {
|
||||||
|
res = append(res, errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .GoType }}, {{ .ReceiverName }}.{{ .ID }}))
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{/* in case of missing validation a TODO comment is added */}}
|
||||||
|
{{- else }}
|
||||||
|
// TODO add case in template
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
if len(res) > 0 {
|
||||||
|
return errors.CompositeValidationError(res...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{/* END CUSTOM BY AAA */}}
|
||||||
|
|
||||||
|
{{- $className := (pascalize .Name) }}
|
||||||
|
{{ range .Params }}
|
||||||
|
{{- if .IsFileParam }}
|
||||||
|
// bind{{ pascalize .ID }} binds file parameter {{ .ID }}.
|
||||||
|
//
|
||||||
|
// The only supported validations on files are MinLength and MaxLength
|
||||||
|
func ({{ .ReceiverName }} *{{ $className }}Params) bind{{ pascalize .ID }}(file multipart.File, header *multipart.FileHeader) error {
|
||||||
|
{{- if or .MinLength .MaxLength }}
|
||||||
|
size, _ := file.Seek(0, io.SeekEnd)
|
||||||
|
file.Seek(0, io.SeekStart)
|
||||||
|
{{- end }}
|
||||||
|
{{- if .MinLength}}
|
||||||
|
if size < {{.MinLength}} {
|
||||||
|
return errors.ExceedsMinimum({{ .Path }}, {{ printf "%q" .Location }}, {{ .MinLength }}, false)
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .MaxLength}}
|
||||||
|
if size > {{.MaxLength}} {
|
||||||
|
return errors.ExceedsMaximum({{ .Path }}, {{ printf "%q" .Location }}, {{ .MaxLength }}, false)
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{- else if not .IsBodyParam }}
|
||||||
|
{{- if or .IsPrimitive .IsCustomFormatter }}
|
||||||
|
// bind{{ pascalize .ID }} binds and validates parameter {{ .ID }} from {{ .Location }}.
|
||||||
|
func ({{ .ReceiverName }} *{{ $className }}Params) bind{{ pascalize .ID }}(rawData []string, hasKey bool, formats strfmt.Registry) error {
|
||||||
|
{{- if and (not .IsPathParam) .Required }}
|
||||||
|
if !hasKey {
|
||||||
|
return errors.Required({{ .Path }}, {{ printf "%q" .Location }}, rawData)
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
var raw string
|
||||||
|
if len(rawData) > 0 {
|
||||||
|
raw = rawData[len(rawData)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required: {{ .Required }}
|
||||||
|
{{ if .IsQueryParam }}// AllowEmptyValue: {{ .AllowEmptyValue }}{{ end }}{{ if .IsPathParam }}// Parameter is provided by construction from the route{{ end }}
|
||||||
|
{{ if and (not .IsPathParam) .Required (not .AllowEmptyValue) -}}
|
||||||
|
if err := validate.RequiredString({{ .Path }}, {{ printf "%q" .Location }}, raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{ else if and ( not .IsPathParam ) (or (not .Required) .AllowEmptyValue) -}}
|
||||||
|
if raw == "" { // empty values pass all other validations{{ if .HasDefault }}
|
||||||
|
// Default values have been previously initialized by New{{ $className }}Params(){{ end }}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Converter }}value, err := {{ .Converter }}(raw)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .GoType }}, raw)
|
||||||
|
}
|
||||||
|
{{ .ValueExpression }} = {{ if .IsNullable }}&{{ end }}value
|
||||||
|
{{ else if .IsCustomFormatter }}// Format: {{ .SwaggerFormat }}
|
||||||
|
value, err := formats.Parse({{ printf "%q" .SwaggerFormat }}, raw)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .GoType }}, raw)
|
||||||
|
}
|
||||||
|
{{ .ValueExpression }} = {{ if or .IsArray .HasDiscriminator .IsFileParam .IsStream (not .IsNullable) }}*{{ end }}(value.(*{{ .GoType }}))
|
||||||
|
{{else}}{{ .ValueExpression }} = {{ if .IsNullable }}&{{ end }}raw
|
||||||
|
{{ end }}
|
||||||
|
{{if .HasValidations }}if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}(formats); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{- else if .IsArray }}
|
||||||
|
// bind{{ pascalize .ID }} binds and validates array parameter {{ .ID }} from {{ .Location }}.
|
||||||
|
//
|
||||||
|
// Arrays are parsed according to CollectionFormat: "{{ .CollectionFormat }}" (defaults to "csv" when empty).
|
||||||
|
func ({{ .ReceiverName }} *{{ $className }}Params) bind{{ pascalize .ID }}(rawData []string, hasKey bool, formats strfmt.Registry) error {
|
||||||
|
{{if .Required }}if !hasKey {
|
||||||
|
return errors.Required({{ .Path }}, {{ printf "%q" .Location }}, rawData)
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ if eq .CollectionFormat "multi" -}}
|
||||||
|
// CollectionFormat: {{ .CollectionFormat }}
|
||||||
|
{{ varname .Child.ValueExpression }}C := rawData
|
||||||
|
{{ else -}}
|
||||||
|
var qv{{ pascalize .Name }} string
|
||||||
|
if len(rawData) > 0 {
|
||||||
|
qv{{ pascalize .Name }} = rawData[len(rawData) - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionFormat: {{ .CollectionFormat }}
|
||||||
|
{{ varname .Child.ValueExpression }}C := swag.SplitByFormat(qv{{ pascalize .Name }}, {{ printf "%q" .CollectionFormat }}){{ end }}
|
||||||
|
{{if and .Required (not .AllowEmptyValue) }}
|
||||||
|
if len({{ varname .Child.ValueExpression }}C) == 0 {
|
||||||
|
return errors.Required({{ .Path }}, {{ printf "%q" .Location }}, {{ varname .Child.ValueExpression }}C)
|
||||||
|
}
|
||||||
|
{{ else }}if len({{ varname .Child.ValueExpression }}C) == 0 { {{ if .HasDefault }}
|
||||||
|
// Default values have been previously initialized by New{{ $className }}Params(){{ end }}
|
||||||
|
return nil
|
||||||
|
} {{- end }}
|
||||||
|
{{ template "sliceparambinder" . }}
|
||||||
|
{{ .ValueExpression }} = {{ varname .Child.ValueExpression }}R
|
||||||
|
{{- if .HasSliceValidations }}
|
||||||
|
if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}(formats); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ if or (and (not .IsArray) .HasValidations) (and .IsArray .HasSliceValidations) }}
|
||||||
|
// validate{{ pascalize .ID }} carries on validations for parameter {{ .ID }}
|
||||||
|
func ({{ .ReceiverName }} *{{ $className }}Params) validate{{ pascalize .ID }}(formats strfmt.Registry) error {
|
||||||
|
{{ template "propertyparamvalidator" . }}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{- else if .IsBodyParam }}{{/* validation method for inline body parameters with validations */}}
|
||||||
|
{{- if and .HasSimpleBodyParams (not .HasModelBodyItems) (not .HasModelBodyMap) }}
|
||||||
|
{{- if .Schema.HasValidations }}
|
||||||
|
// validate{{ pascalize .ID }}Body validates an inline body parameter
|
||||||
|
func ({{ .ReceiverName }} *{{ $className }}Params) validate{{ pascalize .ID }}Body(formats strfmt.Registry) error {
|
||||||
|
{{- if .IsArray }}
|
||||||
|
{{- if .HasSliceValidations }}
|
||||||
|
{{- template "sliceparamvalidator" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Child.HasValidations }}
|
||||||
|
{{ varname .Child.ValueExpression }}C := {{ .ValueExpression }}
|
||||||
|
{{ template "sliceparambinder" . }}
|
||||||
|
{{ .ValueExpression }} = {{ varname .Child.ValueExpression }}R
|
||||||
|
{{- end }}
|
||||||
|
{{- else if .IsMap }}
|
||||||
|
{{ varname .Child.ValueExpression }}C := {{ .ValueExpression }}
|
||||||
|
{{ template "mapparamvalidator" . }}
|
||||||
|
{{ .ValueExpression }} = {{ varname .Child.ValueExpression }}R
|
||||||
|
{{- else }}
|
||||||
|
{{ template "propertyparamvalidator" . }}
|
||||||
|
{{- end }}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{ end }}
|
||||||
13
assets/README.md
Normal file
13
assets/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# `/assets`
|
||||||
|
|
||||||
|
Other assets to go along with your repository (images, logos, etc).
|
||||||
|
|
||||||
|
https://github.com/golang-standards/project-layout/tree/master/assets
|
||||||
|
|
||||||
|
### all about apps specific requirement regarding `/assets/mnt`
|
||||||
|
|
||||||
|
`/assets/mnt` is special, as this is typically our PV rw mountpoint in our infrastucture.
|
||||||
|
|
||||||
|
**Do not check-in files there** (it's also `.gitignore`d)!
|
||||||
|
|
||||||
|
Instead, use this path as your default place to write user generated content that needs to be persisted. This folder will get **shadowed** by the PV mount in our infrastructure and will be available by all replicas through NFS.
|
||||||
0
assets/mnt/.gitkeep
Normal file
0
assets/mnt/.gitkeep
Normal file
11
cmd/README.md
Normal file
11
cmd/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# `/cmd`
|
||||||
|
|
||||||
|
Main applications for this project.
|
||||||
|
|
||||||
|
Don't put a lot of code in the application directory. If you think the code can be imported and used in other projects, then it should live in the `/pkg` directory. If the code is not reusable or if you don't want others to reuse it, put that code in the `/internal` directory. You'll be surprised what others will do, so be explicit about your intentions!
|
||||||
|
|
||||||
|
We manage our applications via cobra ([`cobra-cli`](https://github.com/spf13/cobra-cli) is installed within the `Dockerfile` within the development stage, `/cmd` consumes the core [`cobra`](https://github.com/spf13/cobra) library), see:
|
||||||
|
* https://github.com/spf13/cobra#getting-started
|
||||||
|
* https://github.com/spf13/cobra-cli/blob/main/README.md#add-commands-to-a-project
|
||||||
|
|
||||||
|
Also see https://github.com/golang-standards/project-layout/tree/master/cmd
|
||||||
13
cmd/db/db.go
Normal file
13
cmd/db/db.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *cobra.Command {
|
||||||
|
return command.NewSubcommandGroup("db",
|
||||||
|
newMigrate(),
|
||||||
|
newSeed(),
|
||||||
|
)
|
||||||
|
}
|
||||||
97
cmd/db/migrate.go
Normal file
97
cmd/db/migrate.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
migrate "github.com/rubenv/sql-migrate"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newMigrate() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "migrate",
|
||||||
|
Short: "Executes all migrations which are not yet applied.",
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
migrateCmdFunc()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateCmdFunc() {
|
||||||
|
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
n, err := ApplyMigrations(ctx, s.Config)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Error while applying migrations")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Int("appliedMigrationsCount", n).Msg("Successfully applied migrations")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to apply migrations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyMigrations(ctx context.Context, serviceConfig config.Server) (int, error) {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
// pin migrate to use the globally defined `migrations` table identifier
|
||||||
|
migrate.SetTable(config.DatabaseMigrationTable)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", serviceConfig.Database.ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to open the database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to ping the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case an old default sql-migrate migration table (named "gorp_migrations") still exists we rename it to the new name equivalent
|
||||||
|
// in sync with the settings in dbconfig.yml and config.DatabaseMigrationTable.
|
||||||
|
if _, err := db.ExecContext(ctx, fmt.Sprintf("ALTER TABLE IF EXISTS gorp_migrations RENAME TO %s;", config.DatabaseMigrationTable)); err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to rename migrations table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations := &migrate.FileMigrationSource{
|
||||||
|
Dir: config.DatabaseMigrationFolder,
|
||||||
|
}
|
||||||
|
|
||||||
|
missingMigrations, _, err := migrate.PlanMigration(db, "postgres", migrations, migrate.Up, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Error while planning migrations")
|
||||||
|
return 0, fmt.Errorf("failed to plan migrations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var appliedMigrationsCount int
|
||||||
|
for i := 0; i < len(missingMigrations); i++ {
|
||||||
|
log.Info().Str("migrationId", missingMigrations[i].Id).Msg("Applying migration")
|
||||||
|
|
||||||
|
n, err := migrate.ExecMax(db, "postgres", migrations, migrate.Up, 1)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Error while applying migration")
|
||||||
|
return 0, fmt.Errorf("failed to apply migration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Int("appliedMigrationsCount", n).Msg("Applied migration")
|
||||||
|
|
||||||
|
appliedMigrationsCount += n
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appliedMigrationsCount, nil
|
||||||
|
}
|
||||||
77
cmd/db/seed.go
Normal file
77
cmd/db/seed.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
data "allaboutapps.dev/aw/go-starter/internal/data/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||||
|
dbutil "allaboutapps.dev/aw/go-starter/internal/util/db"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/boil"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSeed() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "seed",
|
||||||
|
Short: "Inserts or updates fixtures to the database.",
|
||||||
|
Long: `Uses upsert to add test data to the current environment.`,
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
seedCmdFunc()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedCmdFunc() {
|
||||||
|
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
err := ApplySeedFixtures(ctx, s.Config)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Error while applying seed fixtures")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("Successfully applied seed fixtures")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to apply migrations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplySeedFixtures(ctx context.Context, config config.Server) error {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", config.Database.ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to ping the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert fixtures in an auto-managed db transaction
|
||||||
|
return dbutil.WithTransaction(ctx, db, func(tx boil.ContextExecutor) error {
|
||||||
|
fixtures := data.Upserts()
|
||||||
|
|
||||||
|
for _, fixture := range fixtures {
|
||||||
|
if err := fixture.Upsert(ctx, tx, true, nil, boil.Infer(), boil.Infer()); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to upsert fixture")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Int("fixturesCount", len(fixtures)).Msg("Successfully upserted fixtures")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
38
cmd/env/env.go
vendored
Normal file
38
cmd/env/env.go
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "env",
|
||||||
|
Short: "Prints the env",
|
||||||
|
Long: `Prints the currently applied env
|
||||||
|
|
||||||
|
You may use this cmd to get an overview about how
|
||||||
|
your ENV_VARS are bound by the server config.
|
||||||
|
Please note that certain secrets are automatically
|
||||||
|
removed from this output.`,
|
||||||
|
Run: func(_ *cobra.Command /* cmd */, _ []string /* args */) {
|
||||||
|
runEnv()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runEnv() {
|
||||||
|
config := config.DefaultServiceConfigFromEnv()
|
||||||
|
|
||||||
|
result, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to marshal the env")
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:forbidigo
|
||||||
|
fmt.Println(string(result))
|
||||||
|
}
|
||||||
89
cmd/probe/liveness.go
Normal file
89
cmd/probe/liveness.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package probe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/handlers/common"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LivenessFlags struct {
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLiveness() *cobra.Command {
|
||||||
|
var flags LivenessFlags
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "liveness",
|
||||||
|
Short: "Runs liveness probes",
|
||||||
|
Long: `Runs connection livenesss probes
|
||||||
|
|
||||||
|
This command triggers the same livenesss probes as in
|
||||||
|
/-/healthy (apart from the actual server.ready
|
||||||
|
probe) and prints the results to stdout. Fails with
|
||||||
|
non zero exitcode on encountered errors.
|
||||||
|
|
||||||
|
A typical usecase of this command are liveness probes
|
||||||
|
to take action if dependant services (e.g. DB, NFS
|
||||||
|
mounts) become unstable. You may also use this to
|
||||||
|
ensure all requirements are fulfilled before starting
|
||||||
|
the app server.`,
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
livenessCmdFunc(flags)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&flags.Verbose, verboseFlag, "v", false, "Show verbose output.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func livenessCmdFunc(flags LivenessFlags) {
|
||||||
|
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
errs, err := runLiveness(ctx, s.Config, flags)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to run liveness probes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Fatal().Errs("errs", errs).Msg("Unhealthy.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to run liveness probes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLiveness(ctx context.Context, config config.Server, flags LivenessFlags) ([]error, error) {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", config.Database.ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to open the database")
|
||||||
|
return nil, fmt.Errorf("failed to open the database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
livenessCtx, cancel := context.WithTimeout(context.Background(), config.Management.LivenessTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
str, errs := common.ProbeLiveness(livenessCtx, db, config.Management.ProbeWriteablePathsAbs, config.Management.ProbeWriteableTouchfile)
|
||||||
|
|
||||||
|
if flags.Verbose {
|
||||||
|
log.Info().Msg(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs, nil
|
||||||
|
}
|
||||||
17
cmd/probe/probe.go
Normal file
17
cmd/probe/probe.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package probe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
verboseFlag string = "verbose"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *cobra.Command {
|
||||||
|
return command.NewSubcommandGroup("probe",
|
||||||
|
newLiveness(),
|
||||||
|
newReadiness(),
|
||||||
|
)
|
||||||
|
}
|
||||||
89
cmd/probe/readiness.go
Normal file
89
cmd/probe/readiness.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package probe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/handlers/common"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReadinessFlags struct {
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReadiness() *cobra.Command {
|
||||||
|
var flags ReadinessFlags
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "readiness",
|
||||||
|
Short: "Runs readiness probes",
|
||||||
|
Long: `Runs connection readinesss probes
|
||||||
|
|
||||||
|
This command triggers the same readinesss probes as in
|
||||||
|
/-/ready (apart from the actual server.ready
|
||||||
|
probe) and prints the results to stdout. Fails with
|
||||||
|
non zero exitcode on encountered errors.
|
||||||
|
|
||||||
|
A typical usecase of this command are readiness probes
|
||||||
|
to take action if dependant services (e.g. DB, NFS
|
||||||
|
mounts) become unstable. You may also use this to
|
||||||
|
ensure all requirements are fulfilled before starting
|
||||||
|
the app server.`,
|
||||||
|
Run: func(_ *cobra.Command, _ []string /* args */) {
|
||||||
|
readinessCmdFunc(flags)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&flags.Verbose, verboseFlag, "v", false, "Show verbose output.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func readinessCmdFunc(flags ReadinessFlags) {
|
||||||
|
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
errs, err := RunReadiness(ctx, s.Config, flags)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to run readiness probes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Fatal().Errs("errs", errs).Msg("Unhealthy.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to run readiness probes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunReadiness(ctx context.Context, config config.Server, flags ReadinessFlags) ([]error, error) {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", config.Database.ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to open the database")
|
||||||
|
return nil, fmt.Errorf("failed to open the database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
readinessCtx, cancel := context.WithTimeout(context.Background(), config.Management.ReadinessTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
str, errs := common.ProbeReadiness(readinessCtx, db, config.Management.ProbeWriteablePathsAbs)
|
||||||
|
|
||||||
|
if flags.Verbose {
|
||||||
|
log.Info().Msg(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs, nil
|
||||||
|
}
|
||||||
44
cmd/root.go
Normal file
44
cmd/root.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/cmd/db"
|
||||||
|
"allaboutapps.dev/aw/go-starter/cmd/env"
|
||||||
|
"allaboutapps.dev/aw/go-starter/cmd/probe"
|
||||||
|
"allaboutapps.dev/aw/go-starter/cmd/server"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rootCmd represents the base command when called without any subcommands
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Version: config.GetFormattedBuildArgs(),
|
||||||
|
Use: "app",
|
||||||
|
Short: config.ModuleName,
|
||||||
|
Long: fmt.Sprintf(`%v
|
||||||
|
|
||||||
|
A stateless RESTful JSON service written in Go.
|
||||||
|
Requires configuration through ENV.`, config.ModuleName),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
rootCmd.SetVersionTemplate(`{{printf "%s\n" .Version}}`)
|
||||||
|
|
||||||
|
// attach the subcommands
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
db.New(),
|
||||||
|
env.New(),
|
||||||
|
probe.New(),
|
||||||
|
server.New(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to execute root command")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
105
cmd/server/server.go
Normal file
105
cmd/server/server.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/cmd/db"
|
||||||
|
"allaboutapps.dev/aw/go-starter/cmd/probe"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/router"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Flags struct {
|
||||||
|
ProbeReadiness bool
|
||||||
|
ApplyMigrations bool
|
||||||
|
SeedFixtures bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *cobra.Command {
|
||||||
|
var flags Flags
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "server",
|
||||||
|
Short: "Starts the server",
|
||||||
|
Long: `Starts the stateless RESTful JSON server
|
||||||
|
|
||||||
|
Requires configuration through ENV and
|
||||||
|
a fully migrated PostgreSQL database.`,
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
runServer(flags)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&flags.ProbeReadiness, "probe", "p", false, "Probe readiness before startup.")
|
||||||
|
cmd.Flags().BoolVarP(&flags.ApplyMigrations, "migrate", "m", false, "Apply migrations before startup.")
|
||||||
|
cmd.Flags().BoolVarP(&flags.SeedFixtures, "seed", "s", false, "Seed fixtures into database before startup.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServer(flags Flags) {
|
||||||
|
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
if flags.ProbeReadiness {
|
||||||
|
errs, err := probe.RunReadiness(ctx, s.Config, probe.ReadinessFlags{
|
||||||
|
Verbose: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to run readiness probes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Fatal().Errs("errs", errs).Msg("Unhealthy.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.ApplyMigrations {
|
||||||
|
_, err := db.ApplyMigrations(ctx, s.Config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Error while applying migrations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.SeedFixtures {
|
||||||
|
err := db.ApplySeedFixtures(ctx, s.Config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Error while applying seed fixtures")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := router.Init(s)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to initialize router")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.Start(); err != nil {
|
||||||
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Info().Msg("Server closed")
|
||||||
|
} else {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to start server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to start server")
|
||||||
|
}
|
||||||
|
}
|
||||||
15
dbconfig.yml
Normal file
15
dbconfig.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Uses PG* env vars
|
||||||
|
# the live database
|
||||||
|
development:
|
||||||
|
dialect: postgres
|
||||||
|
datasource: host=${PGHOST} dbname=${PGDATABASE} user=${PGUSER} password=${PGPASSWORD} port=${PGPORT} sslmode=${PGSSLMODE}
|
||||||
|
dir: migrations
|
||||||
|
table: migrations
|
||||||
|
|
||||||
|
# Uses PSQL_* env vars
|
||||||
|
# the spec database (used for introspection purposes e.g. generate the sqlboiler models)
|
||||||
|
spec:
|
||||||
|
dialect: postgres
|
||||||
|
datasource: host=${PSQL_HOST} dbname=${PSQL_DBNAME} user=${PSQL_USER} password=${PSQL_PASS} port=${PSQL_PORT} sslmode=${PSQL_SSLMODE}
|
||||||
|
dir: migrations
|
||||||
|
table: migrations
|
||||||
168
docker-compose.yml
Normal file
168
docker-compose.yml
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
services:
|
||||||
|
service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: development
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
working_dir: &PROJECT_ROOT_DIR /app
|
||||||
|
# linux permissions / vscode support: we must explicitly run as the development user
|
||||||
|
user: development
|
||||||
|
volumes:
|
||||||
|
# mount working directory
|
||||||
|
# https://code.visualstudio.com/docs/remote/containers-advanced#_update-the-mount-consistency-to-delegated-for-macos
|
||||||
|
# https://docs.docker.com/docker-for-mac/osxfs-caching/#delegated
|
||||||
|
# the container’s view is authoritative (permit delays before updates on the container appear in the host)
|
||||||
|
- .:/app:delegated
|
||||||
|
|
||||||
|
# mount cached go pkg downloads
|
||||||
|
- go-pkg:/go/pkg
|
||||||
|
|
||||||
|
# speed up tmp dirs in working directory by using separate volumes (not the host's filesystem)
|
||||||
|
- workdir-api-tmp:/app/api/tmp
|
||||||
|
- workdir-bin:/app/bin
|
||||||
|
- workdir-tmp:/app/tmp
|
||||||
|
|
||||||
|
# mount cached vscode container extensions
|
||||||
|
# https://code.visualstudio.com/docs/remote/containers-advanced#_avoiding-extension-reinstalls-on-container-rebuild
|
||||||
|
- vscode-extensions:/home/development/.vscode-server/extensions
|
||||||
|
- vscode-extensions-insiders:/home/development/.vscode-server-insiders/extensions
|
||||||
|
|
||||||
|
# https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history
|
||||||
|
# keep user development .bash_history between container restarts
|
||||||
|
- bash-history:/home/development/commandhistory
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- integresql
|
||||||
|
environment:
|
||||||
|
# required: env for main working database, service
|
||||||
|
# default for sql-migrate (target development) and psql cli tool
|
||||||
|
PGDATABASE: &PGDATABASE "development"
|
||||||
|
PGUSER: &PGUSER "dbuser"
|
||||||
|
PGPASSWORD: &PGPASSWORD "dbpass"
|
||||||
|
PGHOST: &PGHOST "postgres"
|
||||||
|
PGPORT: &PGPORT "5432"
|
||||||
|
PGSSLMODE: &PGSSLMODE "disable"
|
||||||
|
|
||||||
|
# optional: env for sql-boiler (ability to generate models out of a "spec" database)
|
||||||
|
# sql-boiler should operate on a "spec" database only
|
||||||
|
PSQL_DBNAME: "spec"
|
||||||
|
PSQL_USER: *PGUSER
|
||||||
|
PSQL_PASS: *PGPASSWORD
|
||||||
|
PSQL_HOST: *PGHOST
|
||||||
|
PSQL_PORT: *PGPORT
|
||||||
|
PSQL_SSLMODE: *PGSSLMODE
|
||||||
|
|
||||||
|
# optional: project root directory, used for relative path resolution (e.g. fixtures)
|
||||||
|
PROJECT_ROOT_DIR: *PROJECT_ROOT_DIR
|
||||||
|
|
||||||
|
# optional: env for integresql client testing
|
||||||
|
# INTEGRESQL_CLIENT_BASE_URL: "http://integresql:5000/api"
|
||||||
|
|
||||||
|
# optional: enable pretty print of log output
|
||||||
|
# intended use is for development and debugging purposes only
|
||||||
|
# not recommended to enable on production systems due to performance penalty and loss of parsing ability
|
||||||
|
SERVER_LOGGER_PRETTY_PRINT_CONSOLE: "true"
|
||||||
|
|
||||||
|
# optional: static management secret to easily call http://localhost:8080/-/healthy?mgmt-secret=mgmtpass
|
||||||
|
SERVER_MANAGEMENT_SECRET: "mgmtpass"
|
||||||
|
|
||||||
|
# path to the changie config
|
||||||
|
CHANGIE_CONFIG_PATH: "/app/.changie-go-starter.yaml"
|
||||||
|
|
||||||
|
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
||||||
|
cap_add:
|
||||||
|
- SYS_PTRACE
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
|
||||||
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
sudo chown -R development:development /app/api/tmp
|
||||||
|
sudo chown -R development:development /app/bin
|
||||||
|
sudo chown -R development:development /app/tmp
|
||||||
|
chmod +x /app/rksh
|
||||||
|
git config --global --add safe.directory /app
|
||||||
|
while sleep 1000; do :; done
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:17.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live
|
||||||
|
# ATTENTION
|
||||||
|
# fsync=off, synchronous_commit=off and full_page_writes=off
|
||||||
|
# gives us a major speed up during local development and testing (~30%),
|
||||||
|
# however you should NEVER use these settings in PRODUCTION unless
|
||||||
|
# you want to have CORRUPTED data.
|
||||||
|
# DO NOT COPY/PASTE THIS BLINDLY.
|
||||||
|
# YOU HAVE BEEN WARNED.
|
||||||
|
# Apply some performance improvements to pg as these guarantees are not needed while running locally
|
||||||
|
command: "postgres -c 'shared_buffers=128MB' -c 'fsync=off' -c 'synchronous_commit=off' -c 'full_page_writes=off' -c 'max_connections=100' -c 'client_min_messages=warning'"
|
||||||
|
expose:
|
||||||
|
- "5432"
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: *PGDATABASE
|
||||||
|
POSTGRES_USER: *PGUSER
|
||||||
|
POSTGRES_PASSWORD: *PGPASSWORD
|
||||||
|
volumes:
|
||||||
|
- pgvolume:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
integresql:
|
||||||
|
image: ghcr.io/allaboutapps/integresql:v1.1.0
|
||||||
|
expose:
|
||||||
|
- "5000"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
PGHOST: *PGHOST
|
||||||
|
PGUSER: *PGUSER
|
||||||
|
PGPASSWORD: *PGPASSWORD
|
||||||
|
|
||||||
|
mailhog:
|
||||||
|
image: mailhog/mailhog
|
||||||
|
expose:
|
||||||
|
- "1025"
|
||||||
|
ports:
|
||||||
|
- "8025:8025"
|
||||||
|
|
||||||
|
swaggerui:
|
||||||
|
image: swaggerapi/swagger-ui:v3.46.0
|
||||||
|
environment:
|
||||||
|
SWAGGER_JSON: "/api/swagger.yml"
|
||||||
|
volumes:
|
||||||
|
# mount our local main swagger.yml file (refresh your browser to see changes)
|
||||||
|
- ./api:/api:ro,consistent
|
||||||
|
# mount overwritten translator.js (intercept requests port 8081 to our local service on port 8080)
|
||||||
|
- ./api/config/swagger-ui-local-translator.js:/usr/share/nginx/configurator/translator.js:ro,delegated
|
||||||
|
|
||||||
|
swaggerui-browser-sync:
|
||||||
|
image: allaboutapps/browser-sync:v2.26.14
|
||||||
|
command: start --proxy 'swaggerui:8080' --port 8081 --files "/api/*.yml"
|
||||||
|
volumes:
|
||||||
|
- ./api:/api:ro,consistent
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# postgresql: declare a named volume to persist DB data
|
||||||
|
pgvolume:
|
||||||
|
|
||||||
|
# go: go mod cached downloads
|
||||||
|
go-pkg:
|
||||||
|
|
||||||
|
# tmp dirs in workdir
|
||||||
|
workdir-api-tmp:
|
||||||
|
workdir-bin:
|
||||||
|
workdir-tmp:
|
||||||
|
|
||||||
|
# vscode: Avoiding extension reinstalls on container rebuild
|
||||||
|
# https://code.visualstudio.com/docs/remote/containers-advanced#_avoiding-extension-reinstalls-on-container-rebuild
|
||||||
|
vscode-extensions:
|
||||||
|
vscode-extensions-insiders:
|
||||||
|
|
||||||
|
# https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history
|
||||||
|
bash-history:
|
||||||
22
docker-helper.sh
Executable file
22
docker-helper.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$1" = "--up" ]; then
|
||||||
|
docker compose up --no-start
|
||||||
|
docker compose start # ensure we are started, handle also allowed to be consumed by vscode
|
||||||
|
docker compose exec service bash
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = "--halt" ]; then
|
||||||
|
docker compose stop
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = "--rebuild" ]; then
|
||||||
|
docker compose up -d --force-recreate --no-deps --build service
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = "--destroy" ]; then
|
||||||
|
docker compose down --rmi local -v --remove-orphans
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$1" -a \( "$1" = "--up" -o "$1" = "--halt" -o "$1" = "--rebuild" -o "$1" = "--destroy" \) ] \
|
||||||
|
|| { echo "usage: $0 --up | --halt | --rebuild | --destroy" >&2; exit 1; }
|
||||||
11
docs/README.md
Normal file
11
docs/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# `/docs`
|
||||||
|
|
||||||
|
Design and user documents (in addition to your godoc generated documentation).
|
||||||
|
|
||||||
|
https://github.com/golang-standards/project-layout/tree/master/docs
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* https://github.com/gohugoio/hugo/tree/master/docs
|
||||||
|
* https://github.com/openshift/origin/tree/master/docs
|
||||||
|
* https://github.com/dapr/dapr/tree/master/docs
|
||||||
1
docs/schemacrawler/.gitignore
vendored
Normal file
1
docs/schemacrawler/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
schema.*
|
||||||
31
docs/schemacrawler/README.md
Normal file
31
docs/schemacrawler/README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# `/docs/schemacrawler`
|
||||||
|
|
||||||
|
To locally (re-)generate a schemacrawler diagramm, execute any of the following commands from your **host** machine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Note that the project must be already running within docker-compose (and the "spec" database should already be migrated via "make sql" or "make all").
|
||||||
|
# First find out under which docker network the "allaboutapps.dev/aw/go-starter" project is available (as started via ./docker-helper.sh --up).
|
||||||
|
# Typically it's "<dir_name>_default".
|
||||||
|
docker network ls
|
||||||
|
# [...]
|
||||||
|
# go-starter_default
|
||||||
|
|
||||||
|
# Ensure you are within the /docs/schemacrawler directory
|
||||||
|
cd docs/schemacrawler
|
||||||
|
pwd
|
||||||
|
# [...]/docs/schemacrawler
|
||||||
|
|
||||||
|
# Generate a png (exchange --network="..." with your docker network before executing this command)
|
||||||
|
docker run --network=go-starter_default -v $(pwd):/home/schcrwlr/share -v $(pwd)/schemacrawler.config.properties:/opt/schemacrawler/config/schemacrawler.config.properties --entrypoint=/opt/schemacrawler/bin/schemacrawler.sh schemacrawler/schemacrawler --server=postgresql --host=postgres --port=5432 --database=spec --schemas=public --user=dbuser --password=dbpass --info-level=standard --command=schema --portable-names --title "allaboutapps.dev/aw/go-starter" --output-format=png --output-file=/home/schcrwlr/share/schema.png
|
||||||
|
|
||||||
|
# Generate a pdf (exchange --network="..." with your docker network before executing this command)
|
||||||
|
docker run --network=go-starter_default -v $(pwd):/home/schcrwlr/share -v $(pwd)/schemacrawler.config.properties:/opt/schemacrawler/config/schemacrawler.config.properties --entrypoint=/opt/schemacrawler/bin/schemacrawler.sh schemacrawler/schemacrawler --server=postgresql --host=postgres --port=5432 --database=spec --schemas=public --user=dbuser --password=dbpass --info-level=standard --command=schema --portable-names --title "allaboutapps.dev/aw/go-starter" --output-format=pdf --output-file=/home/schcrwlr/share/schema.pdf
|
||||||
|
|
||||||
|
# Feel free to override schemacrawler configuration settings in "./schemacrawler.config.properties".
|
||||||
|
```
|
||||||
|
|
||||||
|
For further information see:
|
||||||
|
- Latest [Dockerfile](https://github.com/schemacrawler/SchemaCrawler/blob/master/schemacrawler-docker/Dockerfile) of schemacrawler
|
||||||
|
- [SchemaCrawler Database Diagramming](https://www.schemacrawler.com/diagramming.html) (intro to most diagramming options)
|
||||||
|
- [Docker Image for SchemaCrawler](https://www.schemacrawler.com/docker-image.html) (about running schemacrawler in Docker)
|
||||||
|
- [DockerHub `schemacrawler/schemacrawler`](https://hub.docker.com/r/schemacrawler/schemacrawler/) (available version of this Docker image)
|
||||||
181
docs/schemacrawler/schemacrawler.config.properties
Normal file
181
docs/schemacrawler/schemacrawler.config.properties
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----=
|
||||||
|
# - SchemaCrawler: Configuration Options
|
||||||
|
# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----=
|
||||||
|
#
|
||||||
|
# - Metadata Retrieval Options
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# - Override the metadata retrieval strategy
|
||||||
|
# - This can affect speed, so they are commented out in order to use database
|
||||||
|
# - specific defaults
|
||||||
|
# - Default: Hard-coded into each database plugin, otherwise metadata
|
||||||
|
# - Possible values for each property are metadata or data_dictionary_all
|
||||||
|
#schemacrawler.schema.retrieval.strategy.typeinfo=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.tables=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.tablecolumns=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.primarykeys=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.indexes=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.foreignkeys=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.procedures=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.procedurecolumns=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.functions=metadata
|
||||||
|
#schemacrawler.schema.retrieval.strategy.functioncolumns=metadata
|
||||||
|
#
|
||||||
|
# - Limit Options - inclusion rules for database objects
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# - Regular expression schema pattern to filter
|
||||||
|
# - schema names
|
||||||
|
# - Default: .* for include, <none> for exclude
|
||||||
|
# - IMPORTANT: Please uncomment the follow patterns only for
|
||||||
|
# - database that support schemas. SQLite for example does
|
||||||
|
# - not support schemas
|
||||||
|
#schemacrawler.schema.pattern.include=.*
|
||||||
|
#schemacrawler.schema.pattern.exclude=
|
||||||
|
# - Regular expression table and column name pattern to filter table
|
||||||
|
# - and column names
|
||||||
|
# - Column regular expression to match fully qualified column names,
|
||||||
|
# - in the form "CATALOGNAME.SCHEMANAME.TABLENAME.COLUMNNAME"
|
||||||
|
# - Default: .* for include, <none> for exclude
|
||||||
|
#schemacrawler.table.pattern.include=.*
|
||||||
|
#schemacrawler.table.pattern.exclude=
|
||||||
|
#schemacrawler.column.pattern.include=.*
|
||||||
|
#schemacrawler.column.pattern.exclude=
|
||||||
|
# - Regular expression routine and routine parameter name pattern to filter
|
||||||
|
# - routine and routine parameter names
|
||||||
|
# - Default: .* for include, <none> for exclude
|
||||||
|
#schemacrawler.routine.pattern.include=
|
||||||
|
#schemacrawler.routine.pattern.exclude=.*
|
||||||
|
#schemacrawler.routine.inout.pattern.include=.*
|
||||||
|
#schemacrawler.routine.inout.pattern.exclude=
|
||||||
|
# - Regular expression synonym pattern to filter
|
||||||
|
# - synonym names
|
||||||
|
# - Default: <none> for include, .* for exclude
|
||||||
|
#schemacrawler.synonym.pattern.include=
|
||||||
|
#schemacrawler.synonym.pattern.exclude=.*
|
||||||
|
# - Regular expression sequence pattern to filter
|
||||||
|
# - sequence names
|
||||||
|
# - Default: <none> for include, .* for exclude
|
||||||
|
#schemacrawler.sequence.pattern.include=
|
||||||
|
#schemacrawler.sequence.pattern.exclude=.*
|
||||||
|
#
|
||||||
|
# - Grep Options - inclusion rules
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# - Include patterns for table columns
|
||||||
|
# - Default: .* for include, <none> for exclude
|
||||||
|
#schemacrawler.grep.column.pattern.include=.*
|
||||||
|
#schemacrawler.grep.column.pattern.exclude=
|
||||||
|
# - Include patterns for routine parameters
|
||||||
|
# - Default: .* for include, <none> for exclude
|
||||||
|
#schemacrawler.grep.routine.inout.pattern.include=.*
|
||||||
|
#schemacrawler.grep.routine.inout.pattern.exclude=
|
||||||
|
# - Include patterns for table and routine definitions
|
||||||
|
# - Default: .* for include, <none> for exclude
|
||||||
|
#schemacrawler.grep.definition.pattern.include=.*
|
||||||
|
#schemacrawler.grep.definition.pattern.exclude=
|
||||||
|
#
|
||||||
|
# - Sorting Options
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# - Sort orders for objects
|
||||||
|
#schemacrawler.format.sort_alphabetically.tables=true
|
||||||
|
#schemacrawler.format.sort_alphabetically.table_columns=false
|
||||||
|
#schemacrawler.format.sort_alphabetically.table_foreignkeys=false
|
||||||
|
#schemacrawler.format.sort_alphabetically.table_indexes=false
|
||||||
|
#schemacrawler.format.sort_alphabetically.routines=true
|
||||||
|
#schemacrawler.format.sort_alphabetically.routine_columns=false
|
||||||
|
#
|
||||||
|
# - Show Options - text output formatting
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# - Controls generation of the SchemaCrawler header and footer in output
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.no_header=false
|
||||||
|
#schemacrawler.format.no_footer=false
|
||||||
|
schemacrawler.format.no_schemacrawler_info=true
|
||||||
|
schemacrawler.format.show_database_info=true
|
||||||
|
#schemacrawler.format.show_jdbc_driver_info=false
|
||||||
|
# - Controls display of remarks for tables and columns in output
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.hide_remarks=false
|
||||||
|
# - Shows all object names with the catalog and schema names, for easier comparison
|
||||||
|
# - across different schemas
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.show_unqualified_names=false
|
||||||
|
# - Shows standard column names instead of database specific column names
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.show_standard_column_type_names=false
|
||||||
|
# - Shows ordinal numbers for columns
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.show_ordinal_numbers=false
|
||||||
|
# - Shows table row counts - use with --info-level=maximum
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.show_row_counts=false
|
||||||
|
# - If foreign key names, constraint names, trigger names,
|
||||||
|
# - specific names for routines, or index and primary key names
|
||||||
|
# - are not explicitly provided while creating a schema, most
|
||||||
|
# - database systems assign default names. These names can show
|
||||||
|
# - up as spurious diffs in SchemaCrawler output.
|
||||||
|
# - All of these are hidden with the --portable-names
|
||||||
|
# - command-line option. For more control, use the following
|
||||||
|
# - options.
|
||||||
|
# - Hides foreign key names, constraint names, trigger names,
|
||||||
|
# - specific names for routines, index and primary key names
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.hide_primarykey_names=false
|
||||||
|
#schemacrawler.format.hide_foreignkey_names=false
|
||||||
|
#schemacrawler.format.hide_index_names=false
|
||||||
|
#schemacrawler.format.hide_trigger_names=false
|
||||||
|
#schemacrawler.format.hide_routine_specific_names=false
|
||||||
|
#schemacrawler.format.hide_constraint_names=false
|
||||||
|
#schemacrawler.format.show_weak_associations=false
|
||||||
|
# Specifies how to quote (delimit) database object names in text output
|
||||||
|
# Options are
|
||||||
|
# - quote_none - Do not quote any database object names
|
||||||
|
# - quote_all - Always quote database object names
|
||||||
|
# - quote_if_special_characters - Only quote database object names
|
||||||
|
# if they contain special characters
|
||||||
|
# - quote_if_special_characters_and_reserved_words - Quote database object names
|
||||||
|
# if they contain special characters or SQL 2003 reserved words
|
||||||
|
# - Default: quote_if_special_characters_and_reserved_words
|
||||||
|
#schemacrawler.format.identifier_quoting_strategy=quote_if_special_characters_and_reserved_words
|
||||||
|
# - Does not color-code catalog and schema names.
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.format.no_schema_colors=false
|
||||||
|
# - Encoding of input files, such as Apache Velocity templates
|
||||||
|
# - Default: UTF-8
|
||||||
|
#schemacrawler.encoding.input=UTF-8
|
||||||
|
# - Encoding of SchemaCrawler output files
|
||||||
|
# - Default: UTF-8
|
||||||
|
#schemacrawler.encoding.output=UTF-8
|
||||||
|
#
|
||||||
|
# - Graphing Options
|
||||||
|
# - (some graphing options may be controlled by text formatting options)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# - Show a crow's foot symbol to indicate cardinality
|
||||||
|
# - Default: true
|
||||||
|
#schemacrawler.graph.show.primarykey.cardinality=true
|
||||||
|
#schemacrawler.graph.show.foreignkey.cardinality=true
|
||||||
|
#
|
||||||
|
# - Graph attributes for Graphviz, supporting graph, node and edge
|
||||||
|
# - See https://www.graphviz.org/doc/info/attrs.html
|
||||||
|
schemacrawler.graph.graphviz.graph.rankdir=RL
|
||||||
|
schemacrawler.graph.graphviz.graph.labeljust=r
|
||||||
|
schemacrawler.graph.graphviz.graph.fontname=Helvetica
|
||||||
|
schemacrawler.graph.graphviz.node.fontname=Helvetica
|
||||||
|
schemacrawler.graph.graphviz.node.shape=none
|
||||||
|
schemacrawler.graph.graphviz.edge.fontname=Helvetica
|
||||||
|
|
||||||
|
# schemacrawler.graph.graphviz.graph.splines=ortho
|
||||||
|
|
||||||
|
# - Additional options for Graphviz, to control diagram generation
|
||||||
|
# - See https://www.graphviz.org/doc/info/command.html
|
||||||
|
# schemacrawler.graph.graphviz_opts=-Gdpi=150
|
||||||
|
# - Data Output Options
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# - Whether to show data from CLOB and BLOB objects
|
||||||
|
# - Default: false
|
||||||
|
#schemacrawler.data.show_lobs=false
|
||||||
|
# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----=
|
||||||
|
# Queries
|
||||||
|
# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----=
|
||||||
|
# Define your own named queries, which then become SchemaCrawler command
|
||||||
|
hsqldb.tables=SELECT * FROM INFORMATION_SCHEMA.SYSTEM_TABLES
|
||||||
|
tables.select=SELECT ${columns} FROM ${table} ORDER BY ${columns}
|
||||||
|
tables.drop=DROP ${tabletype} ${table}
|
||||||
106
docs/server-initialization.md
Normal file
106
docs/server-initialization.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Server Initialization
|
||||||
|
|
||||||
|
As our projects have grown, we have gradually added more and more dependent components. Consequently, fully initializing the top-level `Server` struct has become a rather complex task, especially considering the lack of reliable protection against accidental usage of an uninitialized `Server` instance or attached services.
|
||||||
|
|
||||||
|
To simplify our workflow, we made the decision to integrate the **wire** code generation tool into our project. This tool effectively resolves the dependency graph and ensures that the server components are always initialized in the correct order.
|
||||||
|
|
||||||
|
> Wire is a code generation tool that automates connecting components using dependency injection.
|
||||||
|
|
||||||
|
https://pkg.go.dev/github.com/google/wire.
|
||||||
|
|
||||||
|
To accomplish this, we had to introduce certain changes, which may be potentially breaking. We acknowledge that the downstream projects will require some effort in adapting to these changes. Nevertheless, we strongly believe that the long-term benefits will justify the initial investment.
|
||||||
|
|
||||||
|
Please read the following instructions carefully.
|
||||||
|
|
||||||
|
## 1. Define providers
|
||||||
|
**BEFORE**: server components initialized within the methods defined on the `Server` struct.
|
||||||
|
For example:
|
||||||
|
```go
|
||||||
|
func (s *Server) InitPush() error {
|
||||||
|
```
|
||||||
|
|
||||||
|
**AFTER**: providers defined according to the wire guideline: https://github.com/google/wire/blob/main/docs/guide.md#defining-providers.
|
||||||
|
|
||||||
|
Providers are just ordinary functions getting the necessary dependencies as input parameters and returning an initialized component.
|
||||||
|
For example:
|
||||||
|
```go
|
||||||
|
func NewPush(cfg config.Server, db *sql.DB) (*push.Service, error) {
|
||||||
|
```
|
||||||
|
|
||||||
|
### REQUIRED ACTION
|
||||||
|
Convert all `func (s *Server) Init*` methods into providers conforming to the wire guidelines.
|
||||||
|
|
||||||
|
If for any reason a provider function can't live in it's dedicated package, you can place it in `providers.go`.
|
||||||
|
|
||||||
|
Please refer to `internal/api/wire.go` to check currently used wire providers. They are listed as params to the `wire.Build()` function.
|
||||||
|
|
||||||
|
## 2. Define injectors
|
||||||
|
Injectors are declared in `internal/api/wire.go`. They instruct wire which providers should be used to satisfy the dependencies of a top level component.
|
||||||
|
|
||||||
|
### REQUIRED ACTION
|
||||||
|
Add the providers defined in the previous step to the `InitNewServer*` functions in `internal/api/wire.go`. More injectors might be added if needed.
|
||||||
|
|
||||||
|
### Some hints:
|
||||||
|
- Components that should be skipped by wire should be labeled with `wire:"-"` (although this is not recommended).
|
||||||
|
- The order of `wire.Build()` arguments doesn't matter.
|
||||||
|
- If any provider is missing, wire generation will fail. Also, if any provider failes to create a dependent component in runtime, `InitNewServer*` returns an error.
|
||||||
|
- If there are two or more components of the same type, declare a custom type for each of them to let wire identify the right provider to be used: https://github.com/google/wire/blob/main/docs/best-practices.md#distinguishing-types. A newly created type should be returned by a corresponding provider.
|
||||||
|
- If a `Server`'s member is an interface and the corresponding provider returns a pointer to a struct, use the `Bind` function: https://pkg.go.dev/github.com/google/wire#Bind.
|
||||||
|
- Providers commonly used together might be grouped into sets: https://pkg.go.dev/github.com/google/wire#ProviderSet.
|
||||||
|
|
||||||
|
## 3. Generate
|
||||||
|
Functions declared in wire.go are just recepies for code generation and are excluded from build with the `+build wireinject` directive.
|
||||||
|
To generate the code, wire command has to be invoked in the directory where wire.go resides.
|
||||||
|
|
||||||
|
Generated code can appears in `wire_gen.go` and looks like:
|
||||||
|
```go
|
||||||
|
func InitNewServer(cfg config.Server) (*Server, error) {
|
||||||
|
db, err := NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
service, err := NewPush(cfg, db)
|
||||||
|
```
|
||||||
|
|
||||||
|
Because `wire.go` is excluded from build, function signatures can be duplicated in `wire_gen.go`. On any `wire.go` change, `wire_gen.go` needs to be updated.
|
||||||
|
|
||||||
|
Wire generation has been added to Makefile step `go-generate`.
|
||||||
|
|
||||||
|
### REQUIRED ACTION
|
||||||
|
#### Wire tool installation:
|
||||||
|
```sh
|
||||||
|
make init
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Wire generation:
|
||||||
|
```sh
|
||||||
|
wire gen ./...
|
||||||
|
```
|
||||||
|
or via `make`:
|
||||||
|
```sh
|
||||||
|
make go-generate
|
||||||
|
```
|
||||||
|
|
||||||
|
On errors you can simply remove `wire_gen.go` and try again - sometimes it helps.
|
||||||
|
Otherwire, see [this section](#some-hints) or get familiar with the wire documentation.
|
||||||
|
|
||||||
|
## 4. Integrate into existing code
|
||||||
|
|
||||||
|
After successful generation, `func (s *Server) Init*` methods are no longer needed. We can remove them and update each place where they have been used previously to use newly generated functions.
|
||||||
|
|
||||||
|
### REQUIRED ACTION
|
||||||
|
Remove all `func (s *Server) Init*` methods and run
|
||||||
|
```sh
|
||||||
|
make go-build
|
||||||
|
```
|
||||||
|
At each place where the compilation fails do the following:
|
||||||
|
- remove the usage of these methods.
|
||||||
|
- replace `NewServer(cfg)` or any other previously used `Server` provider with the corresponding function generated by wire, for example `InitNewServer`.
|
||||||
|
|
||||||
|
Sometimes having a fully initialized server is not required (see `scripts/internal/handlers/check.go`). In such cases, components we want to use need a manual initialization - the providers have to be invoked in the right order and the resulting components have to be assigned to the `Server`.
|
||||||
|
|
||||||
|
When you're done, verify the sweet fruits of your labor with:
|
||||||
|
```sh
|
||||||
|
make all
|
||||||
|
make test
|
||||||
|
```
|
||||||
150
go.mod
Normal file
150
go.mod
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
module allaboutapps.dev/aw/go-starter
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.5.0
|
||||||
|
github.com/aarondl/null/v8 v8.1.3
|
||||||
|
github.com/aarondl/randomize v0.0.2
|
||||||
|
github.com/aarondl/sqlboiler/v4 v4.19.5
|
||||||
|
github.com/aarondl/strmangle v0.0.9
|
||||||
|
github.com/allaboutapps/integresql-client-go v1.0.0
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||||
|
github.com/friendsofgo/errors v0.9.2
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9
|
||||||
|
github.com/go-openapi/errors v0.22.2
|
||||||
|
github.com/go-openapi/runtime v0.28.0
|
||||||
|
github.com/go-openapi/strfmt v0.23.0
|
||||||
|
github.com/go-openapi/swag v0.23.1
|
||||||
|
github.com/go-openapi/validate v0.24.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/google/wire v0.6.0
|
||||||
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||||
|
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12
|
||||||
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.4.1
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||||
|
github.com/prometheus/client_golang v1.23.0
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/rubenv/sql-migrate v1.8.0
|
||||||
|
github.com/spf13/cobra v1.9.1
|
||||||
|
github.com/spf13/viper v1.20.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/subosito/gotenv v1.6.0
|
||||||
|
golang.org/x/crypto v0.41.0
|
||||||
|
golang.org/x/mod v0.26.0
|
||||||
|
golang.org/x/sys v0.35.0
|
||||||
|
golang.org/x/text v0.28.0
|
||||||
|
google.golang.org/api v0.247.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aarondl/inflect v0.0.2 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/google/subcommands v1.2.0 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.65.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/auth v0.16.4 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||||
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
|
github.com/armon/go-radix v1.0.0 // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
|
github.com/bgentry/speakeasy v0.2.0 // indirect
|
||||||
|
github.com/denisenkom/go-mssqldb v0.12.3 // indirect
|
||||||
|
github.com/dlmiddlecote/sqlstats v1.0.2
|
||||||
|
github.com/dropbox/godropbox v0.0.0-20230623171840-436d2007a9fd
|
||||||
|
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/loads v0.22.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/godror/godror v0.46.0 // indirect
|
||||||
|
github.com/godror/knownpb v0.2.0 // indirect
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/labstack/echo-contrib v0.17.4
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-oci8 v0.1.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
|
github.com/mitchellh/cli v1.1.5 // indirect
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
|
github.com/posener/complete v1.2.3 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.10.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.7 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||||
|
google.golang.org/grpc v1.74.2 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.7 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
tool (
|
||||||
|
github.com/aarondl/sqlboiler/v4
|
||||||
|
github.com/aarondl/sqlboiler/v4/drivers/sqlboiler-psql
|
||||||
|
github.com/google/wire/cmd/wire
|
||||||
|
github.com/rubenv/sql-migrate/sql-migrate
|
||||||
|
)
|
||||||
7
go.not
Normal file
7
go.not
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Specifies go modules that should *never* be embedded in the final app executable (for testing only)
|
||||||
|
github.com/allaboutapps/integresql-client-go
|
||||||
|
github.com/davecgh/go-spew
|
||||||
|
github.com/pmezard/go-difflib
|
||||||
|
github.com/stretchr/testify
|
||||||
|
github.com/rogpeppe/go-internal
|
||||||
|
github.com/kat-co/vala
|
||||||
489
go.sum
Normal file
489
go.sum
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8=
|
||||||
|
cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M=
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
|
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
|
||||||
|
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
||||||
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||||
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||||
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
|
github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4=
|
||||||
|
github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc=
|
||||||
|
github.com/aarondl/inflect v0.0.2 h1:XvH8K5g1wKS921tMmDOUsZ3zS1Eo8WwK5RHC0IGGT2s=
|
||||||
|
github.com/aarondl/inflect v0.0.2/go.mod h1:zjmCfdXHUDQ9jFOV6SeHknpo0Au6rQhV8GchS4Vzv/0=
|
||||||
|
github.com/aarondl/null/v8 v8.1.3 h1:ZJcvvj34BkXAguqU7xzDqEmzG86cSBgM8HYxcqeK0+8=
|
||||||
|
github.com/aarondl/null/v8 v8.1.3/go.mod h1:t30s8PEiGWof1orkBNQ6WKpxjoP8UZHJr7D0AHX3G/A=
|
||||||
|
github.com/aarondl/randomize v0.0.2 h1:JP+3DMqbIMI/ndNFD3GojA8GXi3aRdN39wZL7EIw+HE=
|
||||||
|
github.com/aarondl/randomize v0.0.2/go.mod h1:/4icd0VTMi5WGrfWGK/YY8UsHghSck8EWSfi2AFVbUM=
|
||||||
|
github.com/aarondl/sqlboiler/v4 v4.19.5 h1:/UW1qvOA+ytXjhDg85E7fDW6iqIGP9xDdqFbtqZ3xL8=
|
||||||
|
github.com/aarondl/sqlboiler/v4 v4.19.5/go.mod h1:PqsFMK0K44NPrqcO24fnft2ePqK2avLvbqxWqsTXXHk=
|
||||||
|
github.com/aarondl/strmangle v0.0.9 h1:VCT+O1FqRSE9DTK3qR0zRHtB384fdRzuyKfx2ux2xms=
|
||||||
|
github.com/aarondl/strmangle v0.0.9/go.mod h1:ezNIwvvnuVGuKedP5qt2T+wvzPD8yuOoMzamifXNMlk=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/allaboutapps/integresql-client-go v1.0.0 h1:sVsV2Z78BR5E9la+8TJ4fkzP832z+uHtfDOt7Mo3SKI=
|
||||||
|
github.com/allaboutapps/integresql-client-go v1.0.0/go.mod h1:C5fz9y+Nnjidhj7Mc9h4qKXSmJanIXMa4nFeEOva0oA=
|
||||||
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||||
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
|
||||||
|
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
|
||||||
|
github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI=
|
||||||
|
github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY=
|
||||||
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
|
github.com/dropbox/godropbox v0.0.0-20230623171840-436d2007a9fd h1:s2vYw+2c+7GR1ccOaDuDcKsmNB/4RIxyu5liBm1VRbs=
|
||||||
|
github.com/dropbox/godropbox v0.0.0-20230623171840-436d2007a9fd/go.mod h1:Vr/Q4p40Kce7JAHDITjDhiy/zk07W4tqD5YVi5FD0PA=
|
||||||
|
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo=
|
||||||
|
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
|
||||||
|
github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
|
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||||
|
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
||||||
|
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
|
||||||
|
github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=
|
||||||
|
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
|
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
|
||||||
|
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
|
||||||
|
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
|
||||||
|
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
|
||||||
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
|
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||||
|
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||||
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
|
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||||
|
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/godror/godror v0.46.0 h1:/43db84UcoxlooASIsasH8TvZ7E1huwJ64yDtZ2504k=
|
||||||
|
github.com/godror/godror v0.46.0/go.mod h1:44hxVDzvFSwc+yGyRM+riCLNAY5SwZkUfLzVTh5MXCg=
|
||||||
|
github.com/godror/knownpb v0.2.0 h1:RJLntksFiKUHoUz3wCCJ8+DBjxSLYHYDNl1xRz0/gXI=
|
||||||
|
github.com/godror/knownpb v0.2.0/go.mod h1:kRahRJBwqTenpVPleymQ4k433Xz2Wuy7dOeFSuEpmkI=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
|
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||||
|
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||||
|
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||||
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 h1:DQVOxR9qdYEybJUr/c7ku34r3PfajaMYXZwgDM7KuSk=
|
||||||
|
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12/go.mod h1:u9MdXq/QageOOSGp7qG4XAQsYUMP+V5zEel/Vrl6OOc=
|
||||||
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
|
||||||
|
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
|
||||||
|
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||||
|
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM=
|
||||||
|
github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
|
||||||
|
github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
|
||||||
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.4.1 h1:zwzjtX4uYyiaU02K5Ia3zSkpJZrByARkRB4V3YPrr0g=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.4.1/go.mod h1:++Pl70FR6Cki7hdzZRnEEqdc2dJt+SAGotyFg/SvZMk=
|
||||||
|
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
|
||||||
|
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||||
|
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||||
|
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
|
||||||
|
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||||
|
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||||
|
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||||
|
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||||
|
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
|
||||||
|
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
|
||||||
|
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
|
||||||
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||||
|
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||||
|
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 h1:QVxbx5l/0pzciWYOynixQMtUhPYC3YKD6EcUlOsgGqw=
|
||||||
|
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09/go.mod h1:Uy/Rnv5WKuOO+PuDhuYLEpUiiKIZtss3z519uk67aF0=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||||
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||||
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
|
google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
|
||||||
|
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
|
||||||
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||||
|
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||||
|
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||||
|
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||||
|
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
55
internal/README.md
Normal file
55
internal/README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# `/internal`
|
||||||
|
|
||||||
|
Private application and library code. This is the code you don't want others importing in their applications or libraries. Note that this layout pattern is enforced by the Go compiler itself. See the Go 1.4 [`release notes`](https://golang.org/doc/go1.4#internalpackages) for more details. Note that you are not limited to the top level `internal` directory. You can have more than one `internal` directory at any level of your project tree.
|
||||||
|
|
||||||
|
You can optionally add a bit of extra structure to your internal packages to separate your shared and non-shared internal code. It's not required (especially for smaller projects), but it's nice to have visual clues showing the intended package use. Your actual application code can go in the `/internal/app` directory (e.g., `/internal/app/myapp`) and the code shared by those apps in the `/internal/pkg` directory (e.g., `/internal/pkg/myprivlib`).
|
||||||
|
|
||||||
|
https://github.com/golang-standards/project-layout/tree/master/internal
|
||||||
|
|
||||||
|
### `/internal/api`
|
||||||
|
|
||||||
|
Holds API implementations (`/internal/api/handlers`) and general server, router and middleware setup.
|
||||||
|
|
||||||
|
### `/internal/config`
|
||||||
|
|
||||||
|
Holds configuration of this project (translation of `ENV` vars into something useable).
|
||||||
|
|
||||||
|
### `/internal/data`
|
||||||
|
|
||||||
|
Anything data related (e.g. mappers, DAOs, DTOs, ...), may be especially relevant if you do 3rd party data integrations. Use this package to write **reuseable** functions for your `/internal/api/handlers/*`.
|
||||||
|
|
||||||
|
May hold live db fixture data (for `app db seed`).
|
||||||
|
|
||||||
|
### `/internal/i18n`
|
||||||
|
|
||||||
|
Our implementation for i18n/l10n, as available via `api.Server.I18n`. Your own localized i18n translation bundles should live within **`/web/i18n`**.
|
||||||
|
|
||||||
|
### `/internal/mailer`
|
||||||
|
|
||||||
|
Email handling sub-service.
|
||||||
|
|
||||||
|
### `/internal/models`
|
||||||
|
|
||||||
|
> **Autogenerated** [SQLBoiler](https://github.com/volatiletech/sqlboiler#getting-started) models. Do *not* put your own files in here.
|
||||||
|
|
||||||
|
These are based on your current database in `../migrations/*.sql` and updated while running `make`.
|
||||||
|
|
||||||
|
### `/internal/push`
|
||||||
|
|
||||||
|
Push notifications sub-service.
|
||||||
|
|
||||||
|
### `/internal/test`
|
||||||
|
|
||||||
|
Test-Setup related code and general testing utility functions.
|
||||||
|
|
||||||
|
Holds test db fixture data (`/internal/test/fixtures.go`).
|
||||||
|
|
||||||
|
### `/internal/types`
|
||||||
|
|
||||||
|
> **Autogenerated** [go-swagger](https://github.com/go-swagger/go-swagger) types and validations. Do *not* put your own files in here.
|
||||||
|
|
||||||
|
These are based on your Swagger OpenAPI specification in `../api/**/*.yml` and updated while running `make`.
|
||||||
|
|
||||||
|
### `/internal/util`
|
||||||
|
|
||||||
|
Utility functions.
|
||||||
41
internal/api/handlers/auth/delete_user_account.go
Normal file
41
internal/api/handlers/auth/delete_user_account.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteUserAccountRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.DELETE("/account", deleteUserAccountHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteUserAccountHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
user := auth.UserFromContext(ctx)
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var body types.DeleteUserAccountPayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Auth.DeleteUserAccount(ctx, dto.DeleteUserAccountRequest{
|
||||||
|
User: *user,
|
||||||
|
CurrentPassword: swag.StringValue(body.CurrentPassword),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to delete user")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
136
internal/api/handlers/auth/delete_user_account_test.go
Normal file
136
internal/api/handlers/auth/delete_user_account_test.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"github.com/aarondl/null/v8"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/boil"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertUserAndRelatedData(ctx context.Context, t *testing.T, s *api.Server, userID string, expectExists bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
userExists, err := models.Users(
|
||||||
|
models.UserWhere.ID.EQ(userID),
|
||||||
|
).Exists(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectExists, userExists)
|
||||||
|
|
||||||
|
appUserProfileExists, err := models.AppUserProfiles(
|
||||||
|
models.AppUserProfileWhere.UserID.EQ(userID),
|
||||||
|
).Exists(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectExists, appUserProfileExists)
|
||||||
|
|
||||||
|
accessTokenExists, err := models.AccessTokens(
|
||||||
|
models.AccessTokenWhere.UserID.EQ(userID),
|
||||||
|
).Exists(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectExists, accessTokenExists)
|
||||||
|
|
||||||
|
refreshTokenExists, err := models.RefreshTokens(
|
||||||
|
models.RefreshTokenWhere.UserID.EQ(userID),
|
||||||
|
).Exists(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectExists, refreshTokenExists)
|
||||||
|
|
||||||
|
pushTokenExists, err := models.PushTokens(
|
||||||
|
models.PushTokenWhere.UserID.EQ(userID),
|
||||||
|
).Exists(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectExists, pushTokenExists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUserAccount(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
// expect the user to have a app user profile and different kinds of tokens (access, refresh, push, password reset)
|
||||||
|
assertUserAndRelatedData(ctx, t, s, fix.User1.ID, true)
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "DELETE", "/api/v1/auth/account", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
require.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
// expect the user and all related data to be deleted
|
||||||
|
assertUserAndRelatedData(ctx, t, s, fix.User1.ID, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUserAccountCurrentPasswordWrong(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": "wrongpassword",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "DELETE", "/api/v1/auth/account", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUserAccountMissingCurrentPassword(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "DELETE", "/api/v1/auth/account", nil, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
require.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUserAccountNoAuth(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
res := test.PerformRequest(t, s, "DELETE", "/api/v1/auth/account", nil, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUserAccountUserNotActive(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
fix.User1.IsActive = false
|
||||||
|
_, err := fix.User1.Update(ctx, s.DB, boil.Whitelist(models.UserColumns.IsActive))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": "somepassword",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "DELETE", "/api/v1/auth/account", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenUserDeactivated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUserAccountUserNotLocal(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
fix.User1.Password = null.String{}
|
||||||
|
_, err := fix.User1.Update(ctx, s.DB, boil.Whitelist(models.UserColumns.Password))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": "wrongpassword",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "DELETE", "/api/v1/auth/account", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenNotLocalUser)
|
||||||
|
})
|
||||||
|
}
|
||||||
39
internal/api/handlers/auth/get_complete_register.go
Normal file
39
internal/api/handlers/auth/get_complete_register.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/router/templates"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/url"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCompleteRegisterRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.GET("/register", getCompleteRegisterHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCompleteRegisterHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
params := auth.NewGetCompleteRegisterRouteParams()
|
||||||
|
if err := util.BindAndValidatePathAndQueryParams(c, ¶ms); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmationRequestURL, err := url.ConfirmationRequestURL(s.Config, params.Token.String())
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to generate confirmation link")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, templates.ViewTemplateAccountConfirmation.String(), map[string]interface{}{
|
||||||
|
"confirmationRequestURL": confirmationRequestURL.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
27
internal/api/handlers/auth/get_complete_register_test.go
Normal file
27
internal/api/handlers/auth/get_complete_register_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCompleteRegister(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", fmt.Sprintf("/api/v1/auth/register?token=%s", fix.UserRequiresConfirmationConfirmationToken.Token), nil, nil)
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
response, err := io.ReadAll(res.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
test.Snapshoter.SaveString(t, string(response))
|
||||||
|
})
|
||||||
|
}
|
||||||
33
internal/api/handlers/auth/get_userinfo.go
Normal file
33
internal/api/handlers/auth/get_userinfo.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetUserInfoRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.GET("/userinfo", getUserInfoHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserInfoHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
user := auth.UserFromContext(ctx)
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
user.Profile, err = s.Auth.GetAppUserProfile(ctx, user.ID)
|
||||||
|
if err != nil && !errors.Is(err, auth.ErrNotFound) {
|
||||||
|
log.Debug().Err(err).Msg("Failed to get user profile")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusOK, user.ToTypes())
|
||||||
|
}
|
||||||
|
}
|
||||||
69
internal/api/handlers/auth/get_userinfo_test.go
Normal file
69
internal/api/handlers/auth/get_userinfo_test.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetUserInfo(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/api/v1/auth/userinfo", nil, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.GetUserInfoResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.Equal(t, fix.User1.ID, *response.Sub)
|
||||||
|
assert.Equal(t, strfmt.Email(fix.User1.Username.String), response.Email)
|
||||||
|
test.Snapshoter.Skip([]string{"UpdatedAt"}).Save(t, response)
|
||||||
|
|
||||||
|
for _, scope := range fix.User1.Scopes {
|
||||||
|
assert.Contains(t, response.Scopes, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
appUserProfile, err := models.FindAppUserProfile(ctx, s.DB, fix.User1.ID, models.AccessTokenColumns.UpdatedAt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, appUserProfile.UpdatedAt.Unix(), *response.UpdatedAt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserInfoMinimal(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
_, err := models.AppUserProfiles(models.AppUserProfileWhere.UserID.EQ(fix.User1.ID)).DeleteAll(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/api/v1/auth/userinfo", nil, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.GetUserInfoResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.Equal(t, fix.User1.ID, *response.Sub)
|
||||||
|
assert.Equal(t, strfmt.Email(fix.User1.Username.String), response.Email)
|
||||||
|
|
||||||
|
for _, scope := range fix.User1.Scopes {
|
||||||
|
assert.Contains(t, response.Scopes, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := models.FindUser(ctx, s.DB, fix.User1.ID, models.UserColumns.UpdatedAt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, user.UpdatedAt.Unix(), *response.UpdatedAt)
|
||||||
|
})
|
||||||
|
}
|
||||||
42
internal/api/handlers/auth/post_change_password.go
Normal file
42
internal/api/handlers/auth/post_change_password.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostChangePasswordRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST("/change-password", postChangePasswordHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postChangePasswordHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
user := auth.UserFromEchoContext(c)
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var body types.PostChangePasswordPayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.Auth.UpdatePassword(ctx, dto.UpdatePasswordRequest{
|
||||||
|
User: *user,
|
||||||
|
CurrentPassword: swag.StringValue(body.CurrentPassword),
|
||||||
|
NewPassword: swag.StringValue(body.NewPassword),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to update password")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusOK, result.ToTypes())
|
||||||
|
}
|
||||||
|
}
|
||||||
187
internal/api/handlers/auth/post_change_password_test.go
Normal file
187
internal/api/handlers/auth/post_change_password_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"github.com/aarondl/null/v8"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/boil"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
newPassword = "correct horse battery staple"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostChangePasswordSuccess(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": fixtures.PlainTestUserPassword,
|
||||||
|
"newPassword": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/change-password", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
assert.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEqual(t, fix.User1AccessToken1.Token, *response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
assert.NotEqual(t, fix.User1RefreshToken1.Token, *response.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
|
||||||
|
cnt, err := fix.User1.AccessTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
cnt, err = fix.User1.RefreshTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
err = fix.User1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, fixtures.HashedTestUserPassword, fix.User1.Password.String)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostChangePasswordInvalidPassword(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": "not my password",
|
||||||
|
"newPassword": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/change-password", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostChangePasswordDeactivatedUser(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": fixtures.PlainTestUserPassword,
|
||||||
|
"newPassword": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/change-password", payload, test.HeadersWithAuth(t, fix.UserDeactivatedAccessToken1.Token))
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenUserDeactivated)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostChangePasswordUserWithoutPassword(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"currentPassword": fixtures.PlainTestUserPassword,
|
||||||
|
"newPassword": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
fix.User2.Password = null.String{}
|
||||||
|
rowsAff, err := fix.User2.Update(t.Context(), s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), rowsAff)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/change-password", payload, test.HeadersWithAuth(t, fix.User2AccessToken1.Token))
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenNotLocalUser)
|
||||||
|
|
||||||
|
err = fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostChangePasswordBadRequest(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
payload test.GenericPayload
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "MissingCurrentPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"newPassword": newPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingNewPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"currentPassword": fixtures.PlainTestUserPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyCurrentPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"currentPassword": "",
|
||||||
|
"newPassword": newPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyNewPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"currentPassword": fixtures.PlainTestUserPassword,
|
||||||
|
"newPassword": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/change-password", tt.payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response httperrors.HTTPValidationError
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
test.Snapshoter.Save(t, response)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
40
internal/api/handlers/auth/post_complete_register.go
Normal file
40
internal/api/handlers/auth/post_complete_register.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/handlers/constants"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types/auth"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostCompleteRegisterRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST(fmt.Sprintf("/register/:%s", constants.RegistrationTokenParam), postCompleteRegisterHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postCompleteRegisterHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
params := auth.NewPostCompleteRegisterRouteParams()
|
||||||
|
if err := util.BindAndValidatePathAndQueryParams(c, ¶ms); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.Auth.CompleteRegister(ctx, dto.CompleteRegisterRequest{
|
||||||
|
ConfirmationToken: params.RegistrationToken.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to complete registration")
|
||||||
|
return echo.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusOK, result.ToTypes())
|
||||||
|
}
|
||||||
|
}
|
||||||
70
internal/api/handlers/auth/post_complete_register_test.go
Normal file
70
internal/api/handlers/auth/post_complete_register_test.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/boil"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostCompleteRegister(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", fmt.Sprintf("/api/v1/auth/register/%s", fix.UserRequiresConfirmationConfirmationToken.Token), nil, nil)
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
|
||||||
|
user, err := models.Users(
|
||||||
|
models.UserWhere.ID.EQ(fix.UserRequiresConfirmationConfirmationToken.UserID),
|
||||||
|
).One(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, user.IsActive)
|
||||||
|
assert.False(t, user.RequiresConfirmation)
|
||||||
|
|
||||||
|
// trying again with the same token should fail
|
||||||
|
{
|
||||||
|
res := test.PerformRequest(t, s, "POST", fmt.Sprintf("/api/v1/auth/register/%s", fix.UserRequiresConfirmationConfirmationToken.Token), nil, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCompleteRegisterTokenNotFound(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register/e45071b7-b9a0-4ed7-a5a0-16a16413d275", nil, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCompleteRegisterTokenExpired(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
fix.UserRequiresConfirmationConfirmationToken.ValidUntil = s.Clock.Now().Add(-1 * time.Second)
|
||||||
|
_, err := fix.UserRequiresConfirmationConfirmationToken.Update(ctx, s.DB, boil.Whitelist(models.ConfirmationTokenColumns.ValidUntil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", fmt.Sprintf("/api/v1/auth/register/%s", fix.UserRequiresConfirmationConfirmationToken.Token), nil, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
57
internal/api/handlers/auth/post_forgot_password.go
Normal file
57
internal/api/handlers/auth/post_forgot_password.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/url"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostForgotPasswordRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST("/forgot-password", postForgotPasswordHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postForgotPasswordHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
|
var body types.PostForgotPasswordPayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
username := dto.NewUsername(body.Username.String())
|
||||||
|
|
||||||
|
result, err := s.Auth.InitPasswordReset(ctx, dto.InitPasswordResetRequest{
|
||||||
|
Username: username,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to initiate password reset")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.ResetToken.IsZero() {
|
||||||
|
log.Debug().Msg("Failed to initiate password reset, no token returned")
|
||||||
|
// Return success status to prevent user enumeration
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLink, err := url.PasswordResetDeeplinkURL(s.Config, result.ResetToken.String)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to generate password reset link")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Mailer.SendPasswordReset(ctx, username.String(), resetLink.String()); err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to send password reset email")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
39
internal/api/handlers/auth/post_forgot_password_complete.go
Normal file
39
internal/api/handlers/auth/post_forgot_password_complete.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostForgotPasswordCompleteRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST("/forgot-password/complete", postForgotPasswordCompleteHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postForgotPasswordCompleteHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var body types.PostForgotPasswordCompletePayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.Auth.ResetPassword(ctx, dto.ResetPasswordRequest{
|
||||||
|
ResetToken: body.Token.String(),
|
||||||
|
NewPassword: swag.StringValue(body.Password),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to reset password")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusOK, result.ToTypes())
|
||||||
|
}
|
||||||
|
}
|
||||||
280
internal/api/handlers/auth/post_forgot_password_complete_test.go
Normal file
280
internal/api/handlers/auth/post_forgot_password_complete_test.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"github.com/aarondl/null/v8"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/boil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostForgotPasswordCompleteSuccess(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
passwordResetToken := models.PasswordResetToken{
|
||||||
|
UserID: fix.User1.ID,
|
||||||
|
ValidUntil: s.Clock.Now().Add(s.Config.Auth.PasswordResetTokenValidity),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := passwordResetToken.Insert(ctx, s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"token": passwordResetToken.Token,
|
||||||
|
"password": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEqual(t, fix.User1AccessToken1.Token, response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
assert.NotEqual(t, fix.User1RefreshToken1.Token, *response.RefreshToken)
|
||||||
|
test.Snapshoter.Skip([]string{"AccessToken", "RefreshToken"}).Save(t, response)
|
||||||
|
|
||||||
|
err = fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
|
||||||
|
cnt, err := fix.User1.AccessTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
cnt, err = fix.User1.RefreshTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
err = fix.User1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, fixtures.HashedTestUserPassword, fix.User1.Password.String)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordCompleteUnknownToken(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"token": "fd5c04ea-f39c-49e9-bb40-7f570ed1f66f",
|
||||||
|
"password": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrNotFoundTokenNotFound)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cnt, err := fix.User1.AccessTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
cnt, err = fix.User1.RefreshTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
err = fix.User1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, fixtures.HashedTestUserPassword, fix.User1.Password.String)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordCompleteExpiredToken(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
passwordResetToken := models.PasswordResetToken{
|
||||||
|
UserID: fix.User1.ID,
|
||||||
|
ValidUntil: s.Clock.Now().Add(time.Minute * -10),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := passwordResetToken.Insert(ctx, s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"token": passwordResetToken.Token,
|
||||||
|
"password": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrConflictTokenExpired)
|
||||||
|
|
||||||
|
err = fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cnt, err := fix.User1.AccessTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
cnt, err = fix.User1.RefreshTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
err = fix.User1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, fixtures.HashedTestUserPassword, fix.User1.Password.String)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordCompleteDeactivatedUser(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
passwordResetToken := models.PasswordResetToken{
|
||||||
|
UserID: fix.UserDeactivated.ID,
|
||||||
|
ValidUntil: s.Clock.Now().Add(s.Config.Auth.PasswordResetTokenValidity),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := passwordResetToken.Insert(ctx, s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"token": passwordResetToken.Token,
|
||||||
|
"password": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenUserDeactivated)
|
||||||
|
|
||||||
|
err = fix.UserDeactivatedAccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.UserDeactivatedRefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cnt, err := fix.UserDeactivated.AccessTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
cnt, err = fix.UserDeactivated.RefreshTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
err = fix.UserDeactivated.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, fixtures.HashedTestUserPassword, fix.UserDeactivated.Password.String)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordCompleteUserWithoutPassword(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
passwordResetToken := models.PasswordResetToken{
|
||||||
|
UserID: fix.User2.ID,
|
||||||
|
ValidUntil: s.Clock.Now().Add(s.Config.Auth.PasswordResetTokenValidity),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := passwordResetToken.Insert(ctx, s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"token": passwordResetToken.Token,
|
||||||
|
"password": newPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
fix.User2.Password = null.String{}
|
||||||
|
rowsAff, err := fix.User2.Update(t.Context(), s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), rowsAff)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenNotLocalUser)
|
||||||
|
|
||||||
|
err = fix.User2AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = fix.User2RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cnt, err := fix.User2.AccessTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
cnt, err = fix.User2.RefreshTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), cnt)
|
||||||
|
|
||||||
|
err = fix.User2.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, fix.User2.Password.Valid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordCompleteBadRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
payload test.GenericPayload
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "MissingToken",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"password": "correct horse battery stable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"token": "7b6e2366-7806-421f-bd56-ffcb39d7b1ee",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidToken",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"token": "definitelydoesnotexist",
|
||||||
|
"password": "correct horse battery stable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyToken",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"password": "correct horse battery stable",
|
||||||
|
"token": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"token": "42deb737-fa9c-4e9e-bdce-e33b829c72f7",
|
||||||
|
"password": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", tt.payload, nil)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response httperrors.HTTPValidationError
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
test.Snapshoter.Save(t, response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
256
internal/api/handlers/auth/post_forgot_password_test.go
Normal file
256
internal/api/handlers/auth/post_forgot_password_test.go
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/db"
|
||||||
|
"github.com/aarondl/null/v8"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/boil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostForgotPasswordSuccess(t *testing.T) {
|
||||||
|
config := config.DefaultServiceConfigFromEnv()
|
||||||
|
config.Auth.PasswordResetTokenReuseDuration = 120 * time.Second
|
||||||
|
config.Auth.PasswordResetTokenDebounceDuration = 60 * time.Second
|
||||||
|
|
||||||
|
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
require.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
passwordResetToken, err := fix.User1.PasswordResetTokens().One(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mail := test.GetLastSentMail(t, s.Mailer)
|
||||||
|
require.NotNil(t, mail)
|
||||||
|
assert.Contains(t, string(mail.HTML), fmt.Sprintf("http://localhost:3000/set-new-password?token=%s", passwordResetToken.Token))
|
||||||
|
|
||||||
|
// retrying should not send a new mail because of the debounce time
|
||||||
|
{
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
require.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
sentMails := test.GetSentMails(t, s.Mailer)
|
||||||
|
assert.Len(t, sentMails, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAt of token exceeds debounce time, retrying should send a new mail
|
||||||
|
// but with the same token as the reuse duration has not passed yet
|
||||||
|
{
|
||||||
|
passwordResetToken.CreatedAt = s.Clock.Now().Add(-s.Config.Auth.PasswordResetTokenDebounceDuration)
|
||||||
|
_, err = passwordResetToken.Update(ctx, s.DB, boil.Whitelist(models.PasswordResetTokenColumns.CreatedAt))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
require.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
sentMails := test.GetSentMails(t, s.Mailer)
|
||||||
|
require.Len(t, sentMails, 2)
|
||||||
|
|
||||||
|
passwordResetTokens, err := fix.User1.PasswordResetTokens().All(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, passwordResetTokens, 1)
|
||||||
|
for _, mail := range sentMails {
|
||||||
|
assert.Contains(t, string(mail.HTML), fmt.Sprintf("http://localhost:3000/set-new-password?token=%s", passwordResetTokens[0].Token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAt of token exceeds reuse time, retrying should send a new mail with a new token
|
||||||
|
{
|
||||||
|
passwordResetToken.CreatedAt = s.Clock.Now().Add(-s.Config.Auth.PasswordResetTokenReuseDuration)
|
||||||
|
_, err = passwordResetToken.Update(ctx, s.DB, boil.Whitelist(models.PasswordResetTokenColumns.CreatedAt))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
require.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
sentMails := test.GetSentMails(t, s.Mailer)
|
||||||
|
require.Len(t, sentMails, 3)
|
||||||
|
|
||||||
|
passwordResetTokens, err := fix.User1.PasswordResetTokens(
|
||||||
|
db.OrderBy(types.OrderDirDesc, models.PasswordResetTokenColumns.CreatedAt),
|
||||||
|
).All(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, passwordResetTokens, 2)
|
||||||
|
|
||||||
|
assert.Contains(t, string(sentMails[2].HTML), fmt.Sprintf("http://localhost:3000/set-new-password?token=%s", passwordResetTokens[0].Token))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token validity is expired, retrying should send a new mail with a new token
|
||||||
|
{
|
||||||
|
_, err = models.PasswordResetTokens().UpdateAll(ctx, s.DB, models.M{
|
||||||
|
models.PasswordResetTokenColumns.ValidUntil: s.Clock.Now().Add(-1 * time.Second),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
require.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
sentMails := test.GetSentMails(t, s.Mailer)
|
||||||
|
require.Len(t, sentMails, 4)
|
||||||
|
|
||||||
|
passwordResetTokens, err := fix.User1.PasswordResetTokens(
|
||||||
|
db.OrderBy(types.OrderDirDesc, models.PasswordResetTokenColumns.CreatedAt),
|
||||||
|
).All(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, passwordResetTokens, 3)
|
||||||
|
|
||||||
|
assert.Contains(t, string(sentMails[3].HTML), fmt.Sprintf("http://localhost:3000/set-new-password?token=%s", passwordResetTokens[0].Token))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordSuccessLowercaseTrimWhitespaces(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fmt.Sprintf(" %s ", strings.ToUpper(fix.User1.Username.String)),
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
passwordResetToken, err := fix.User1.PasswordResetTokens().One(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mail := test.GetLastSentMail(t, s.Mailer)
|
||||||
|
require.NotNil(t, mail)
|
||||||
|
assert.Contains(t, string(mail.HTML), fmt.Sprintf("http://localhost:3000/set-new-password?token=%s", passwordResetToken.Token))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordUnknownUser(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": "definitelydoesnotexist@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
cnt, err := models.PasswordResetTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), cnt)
|
||||||
|
|
||||||
|
mail := test.GetLastSentMail(t, s.Mailer)
|
||||||
|
assert.Nil(t, mail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordDeactivatedUser(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.UserDeactivated.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
cnt, err := models.PasswordResetTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), cnt)
|
||||||
|
|
||||||
|
mail := test.GetLastSentMail(t, s.Mailer)
|
||||||
|
assert.Nil(t, mail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordUserWithoutPassword(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.User2.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
fix.User2.Password = null.String{}
|
||||||
|
rowsAff, err := fix.User2.Update(t.Context(), s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), rowsAff)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
cnt, err := models.PasswordResetTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), cnt)
|
||||||
|
|
||||||
|
mail := test.GetLastSentMail(t, s.Mailer)
|
||||||
|
assert.Nil(t, mail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostForgotPasswordBadRequest(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
payload test.GenericPayload
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "MissingUsername",
|
||||||
|
payload: test.GenericPayload{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": "definitely not an email",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password", tt.payload, nil)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response httperrors.HTTPValidationError
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
test.Snapshoter.Save(t, response)
|
||||||
|
|
||||||
|
cnt, err := models.PasswordResetTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), cnt)
|
||||||
|
|
||||||
|
mail := test.GetLastSentMail(t, s.Mailer)
|
||||||
|
assert.Nil(t, mail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
39
internal/api/handlers/auth/post_login.go
Normal file
39
internal/api/handlers/auth/post_login.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostLoginRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST("/login", postLoginHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postLoginHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var body types.PostLoginPayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.Auth.Login(ctx, dto.LoginRequest{
|
||||||
|
Username: dto.NewUsername(body.Username.String()),
|
||||||
|
Password: swag.StringValue(body.Password),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to authenticate user")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusOK, result.ToTypes())
|
||||||
|
}
|
||||||
|
}
|
||||||
179
internal/api/handlers/auth/post_login_test.go
Normal file
179
internal/api/handlers/auth/post_login_test.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"github.com/aarondl/null/v8"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/boil"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostLoginSuccess(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEqual(t, fix.User1AccessToken1.Token, response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
assert.NotEqual(t, fix.User1RefreshToken1.Token, response.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLoginSuccessLowercaseTrimWhitespaces(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fmt.Sprintf(" %s ", strings.ToUpper(fix.User1.Username.String)),
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEqual(t, fix.User1AccessToken1.Token, response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
assert.NotEqual(t, fix.User1RefreshToken1.Token, response.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLoginInvalidCredentials(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
"password": "not my password",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLoginUnknownUser(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": "definitelydoesnotexist@example.com",
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLoginDeactivatedUser(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.UserDeactivated.Username,
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenUserDeactivated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLoginUserWithoutPassword(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.User2.Username,
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
fix.User2.Password = null.String{}
|
||||||
|
rowsAff, err := fix.User2.Update(t.Context(), s.DB, boil.Infer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), rowsAff)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLoginBadRequest(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
payload test.GenericPayload
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "InvalidUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": "definitely not an email",
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": "",
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
"password": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", tt.payload, nil)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response httperrors.HTTPValidationError
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
test.Snapshoter.Save(t, response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
45
internal/api/handlers/auth/post_logout.go
Normal file
45
internal/api/handlers/auth/post_logout.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/aarondl/null/v8"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostLogoutRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST("/logout", postLogoutHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postLogoutHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var body types.PostLogoutPayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
request := dto.LogoutRequest{
|
||||||
|
AccessToken: *auth.AccessTokenFromEchoContext(c),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body.RefreshToken.String()) > 0 {
|
||||||
|
request.RefreshToken = null.StringFrom(body.RefreshToken.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Auth.Logout(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to logout user")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
129
internal/api/handlers/auth/post_logout_test.go
Normal file
129
internal/api/handlers/auth/post_logout_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/middleware"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostLogoutSuccess(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", nil, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
assert.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLogoutSuccessWithRefreshToken(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"refresh_token": fix.User1RefreshToken1.Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
assert.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLogoutSuccessWithUnknownRefreshToken(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"refresh_token": "93d8ccd0-be30-4661-a428-cbe74e1a3ffe",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
assert.Equal(t, http.StatusNoContent, res.Result().StatusCode)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLogoutInvalidRefreshToken(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"refresh_token": "not my refresh token",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", payload, test.HeadersWithAuth(t, fix.User1AccessToken1.Token))
|
||||||
|
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response httperrors.HTTPValidationError
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
test.Snapshoter.Save(t, response)
|
||||||
|
|
||||||
|
err := fix.User1AccessToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostLogoutError(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expectedError *httperrors.HTTPError
|
||||||
|
headers http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "InvalidAuthToken",
|
||||||
|
expectedError: middleware.ErrBadRequestMalformedToken,
|
||||||
|
headers: test.HeadersWithAuth(t, "not my auth token"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UnknownAuthToken",
|
||||||
|
expectedError: httperrors.NewFromEcho(echo.ErrUnauthorized),
|
||||||
|
headers: test.HeadersWithAuth(t, "25e8630e-9a41-4f38-8339-373f0c203cef"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingAuthToken",
|
||||||
|
expectedError: httperrors.NewFromEcho(echo.ErrUnauthorized),
|
||||||
|
headers: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", nil, tt.headers)
|
||||||
|
test.RequireHTTPError(t, res, tt.expectedError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
37
internal/api/handlers/auth/post_refresh.go
Normal file
37
internal/api/handlers/auth/post_refresh.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostRefreshRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST("/refresh", postRefreshHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postRefreshHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var body types.PostRefreshPayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.Auth.Refresh(ctx, dto.RefreshRequest{
|
||||||
|
RefreshToken: body.RefreshToken.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to refresh tokens")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusOK, result.ToTypes())
|
||||||
|
}
|
||||||
|
}
|
||||||
108
internal/api/handlers/auth/post_refresh_test.go
Normal file
108
internal/api/handlers/auth/post_refresh_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostRefreshSuccess(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"refresh_token": fix.User1RefreshToken1.Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/refresh", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEqual(t, fix.User1AccessToken1.Token, response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
assert.NotEqual(t, fix.User1RefreshToken1.Token, response.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||||
|
|
||||||
|
err := fix.User1RefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRefreshUnknownToken(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"refresh_token": "c094e933-e5f0-4ece-9c10-914f3122cdb6",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/refresh", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.NewFromEcho(echo.ErrUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRefreshDeactivatedUser(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"refresh_token": fix.UserDeactivatedRefreshToken1.Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/refresh", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrForbiddenUserDeactivated)
|
||||||
|
|
||||||
|
err := fix.UserDeactivatedRefreshToken1.Reload(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRefreshBadRequest(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
payload test.GenericPayload
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "MissingRefreshToken",
|
||||||
|
payload: test.GenericPayload{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyRefreshToken",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"refresh_token": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidToken",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"refresh_token": "not a valid token",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/refresh", tt.payload, nil)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response httperrors.HTTPValidationError
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
test.Snapshoter.Save(t, response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
72
internal/api/handlers/auth/post_register.go
Normal file
72
internal/api/handlers/auth/post_register.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/url"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostRegisterRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.APIV1Auth.POST("/register", postRegisterHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postRegisterHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
log := util.LogFromContext(ctx)
|
||||||
|
|
||||||
|
var body types.PostRegisterPayload
|
||||||
|
if err := util.BindAndValidateBody(c, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
username := dto.NewUsername(body.Username.String())
|
||||||
|
|
||||||
|
result, err := s.Auth.Register(ctx, dto.RegisterRequest{
|
||||||
|
Username: username,
|
||||||
|
Password: swag.StringValue(body.Password),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to register user")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.RequiresConfirmation {
|
||||||
|
loginResult, err := s.Auth.Login(ctx, dto.LoginRequest{
|
||||||
|
Username: username,
|
||||||
|
Password: swag.StringValue(body.Password),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to authenticate user after registration")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusOK, loginResult.ToTypes())
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.ConfirmationToken.Valid {
|
||||||
|
confirmationLink, err := url.ConfirmationDeeplinkURL(s.Config, result.ConfirmationToken.String)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to generate confirmation link")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Mailer.SendAccountConfirmation(ctx, username.String(), dto.ConfirmatioNotificationPayload{
|
||||||
|
ConfirmationLink: confirmationLink.String(),
|
||||||
|
}); err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Failed to send confirmation email")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.ValidateAndReturn(c, http.StatusAccepted, &types.RegisterResponse{
|
||||||
|
RequiresConfirmation: swag.Bool(result.RequiresConfirmation),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
334
internal/api/handlers/auth/post_register_test.go
Normal file
334
internal/api/handlers/auth/post_register_test.go
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/auth"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test/fixtures"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/db"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/util/url"
|
||||||
|
"github.com/aarondl/null/v8"
|
||||||
|
"github.com/aarondl/sqlboiler/v4/queries/qm"
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostRegisterSuccess(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
now := time.Date(2025, 2, 5, 11, 42, 30, 0, time.UTC)
|
||||||
|
test.SetMockClock(t, s, now)
|
||||||
|
|
||||||
|
username := "usernew@example.com"
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": username,
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||||
|
|
||||||
|
user, err := models.Users(
|
||||||
|
models.UserWhere.Username.EQ(null.StringFrom(username)),
|
||||||
|
qm.Load(models.UserRels.AppUserProfile),
|
||||||
|
qm.Load(models.UserRels.AccessTokens),
|
||||||
|
qm.Load(models.UserRels.RefreshTokens),
|
||||||
|
).One(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, null.StringFrom(username), user.Username)
|
||||||
|
assert.True(t, user.LastAuthenticatedAt.Valid)
|
||||||
|
assert.Equal(t, now, user.LastAuthenticatedAt.Time)
|
||||||
|
assert.EqualValues(t, s.Config.Auth.DefaultUserScopes, user.Scopes)
|
||||||
|
|
||||||
|
assert.NotNil(t, user.R.AppUserProfile)
|
||||||
|
assert.False(t, user.R.AppUserProfile.LegalAcceptedAt.Valid)
|
||||||
|
|
||||||
|
assert.Len(t, user.R.AccessTokens, 1)
|
||||||
|
assert.Equal(t, strfmt.UUID4(user.R.AccessTokens[0].Token), *response.AccessToken)
|
||||||
|
assert.Len(t, user.R.RefreshTokens, 1)
|
||||||
|
assert.Equal(t, strfmt.UUID4(user.R.RefreshTokens[0].Token), *response.RefreshToken)
|
||||||
|
|
||||||
|
res2 := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusOK, res2.Result().StatusCode)
|
||||||
|
|
||||||
|
var response2 types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res2, &response2)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response2.AccessToken)
|
||||||
|
assert.NotEqual(t, response.AccessToken, *response2.AccessToken)
|
||||||
|
assert.NotEmpty(t, response2.RefreshToken)
|
||||||
|
assert.NotEqual(t, response.RefreshToken, *response2.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response2.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response2.TokenType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRegisterWithConfirmationSuccess(t *testing.T) {
|
||||||
|
config := config.DefaultServiceConfigFromEnv()
|
||||||
|
config.Auth.RegistrationRequiresConfirmation = true
|
||||||
|
|
||||||
|
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
username := "usernew-with-confirmation@example.com"
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": username,
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusAccepted, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.RegisterResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.True(t, swag.BoolValue(response.RequiresConfirmation))
|
||||||
|
|
||||||
|
// expect the user to be normally created
|
||||||
|
user, err := models.Users(
|
||||||
|
models.UserWhere.Username.EQ(null.StringFrom(username)),
|
||||||
|
qm.Load(models.UserRels.AppUserProfile),
|
||||||
|
qm.Load(models.UserRels.AccessTokens),
|
||||||
|
qm.Load(models.UserRels.RefreshTokens),
|
||||||
|
qm.Load(models.UserRels.ConfirmationTokens),
|
||||||
|
).One(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, null.StringFrom(username), user.Username)
|
||||||
|
assert.True(t, user.LastAuthenticatedAt.Valid)
|
||||||
|
assert.EqualValues(t, s.Config.Auth.DefaultUserScopes, user.Scopes)
|
||||||
|
assert.False(t, user.IsActive)
|
||||||
|
assert.True(t, user.RequiresConfirmation)
|
||||||
|
|
||||||
|
assert.NotNil(t, user.R.AppUserProfile)
|
||||||
|
assert.False(t, user.R.AppUserProfile.LegalAcceptedAt.Valid)
|
||||||
|
|
||||||
|
// expect the user to have no access or refresh tokens
|
||||||
|
assert.Empty(t, user.R.AccessTokens)
|
||||||
|
assert.Empty(t, user.R.RefreshTokens)
|
||||||
|
require.Len(t, user.R.ConfirmationTokens, 1)
|
||||||
|
confirmationToken := user.R.ConfirmationTokens[0]
|
||||||
|
|
||||||
|
// expect the login to fail
|
||||||
|
res2 := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res2, httperrors.ErrForbiddenUserDeactivated)
|
||||||
|
|
||||||
|
// expect the confirmation email to be sent
|
||||||
|
mails := test.GetSentMails(t, s.Mailer)
|
||||||
|
require.Len(t, mails, 1)
|
||||||
|
|
||||||
|
mail := mails[0]
|
||||||
|
expectedConfirmationLink, err := url.ConfirmationDeeplinkURL(s.Config, confirmationToken.Token)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, username, mail.To[0])
|
||||||
|
assert.Contains(t, string(mail.HTML), expectedConfirmationLink.String())
|
||||||
|
|
||||||
|
// directly register again should trigger the debounce
|
||||||
|
// and not create a new confirmation token
|
||||||
|
registerAgainRes := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||||
|
require.Equal(t, http.StatusAccepted, registerAgainRes.Result().StatusCode)
|
||||||
|
|
||||||
|
var registerAgainResponse types.RegisterResponse
|
||||||
|
test.ParseResponseAndValidate(t, registerAgainRes, ®isterAgainResponse)
|
||||||
|
|
||||||
|
// expect the confirmation to be required
|
||||||
|
assert.True(t, swag.BoolValue(registerAgainResponse.RequiresConfirmation))
|
||||||
|
|
||||||
|
confirmationTokenCount, err := user.ConfirmationTokens().Count(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 1, confirmationTokenCount)
|
||||||
|
|
||||||
|
// register later again
|
||||||
|
test.SetMockClock(t, s, s.Clock.Now().Add(config.Auth.ConfirmationTokenDebounceDuration+time.Second))
|
||||||
|
|
||||||
|
registerLaterAgainRes := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||||
|
require.Equal(t, http.StatusAccepted, registerLaterAgainRes.Result().StatusCode)
|
||||||
|
|
||||||
|
var registerLaterAgainResponse types.RegisterResponse
|
||||||
|
test.ParseResponseAndValidate(t, registerLaterAgainRes, ®isterLaterAgainResponse)
|
||||||
|
assert.True(t, swag.BoolValue(registerLaterAgainResponse.RequiresConfirmation))
|
||||||
|
|
||||||
|
confirmationTokens, err := models.ConfirmationTokens(
|
||||||
|
models.ConfirmationTokenWhere.UserID.EQ(user.ID),
|
||||||
|
db.OrderBy(types.OrderDirDesc, models.ConfirmationTokenColumns.CreatedAt),
|
||||||
|
).All(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, confirmationTokens, 2)
|
||||||
|
|
||||||
|
lastSentMail := test.GetLastSentMail(t, s.Mailer)
|
||||||
|
require.NotNil(t, lastSentMail)
|
||||||
|
|
||||||
|
expectedConfirmationLink, err = url.ConfirmationDeeplinkURL(s.Config, confirmationTokens[0].Token)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, username, lastSentMail.To[0])
|
||||||
|
assert.Contains(t, string(lastSentMail.HTML), expectedConfirmationLink.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRegisterSuccessLowercaseTrimWhitespaces(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
username := " USERNEW@example.com "
|
||||||
|
usernameLowerTrimmed := "usernew@example.com"
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": username,
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
"name": "Trim Whitespaces",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response.AccessToken)
|
||||||
|
assert.NotEmpty(t, response.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response.TokenType)
|
||||||
|
|
||||||
|
user, err := models.Users(
|
||||||
|
models.UserWhere.Username.EQ(null.StringFrom(usernameLowerTrimmed)),
|
||||||
|
qm.Load(models.UserRels.AppUserProfile),
|
||||||
|
qm.Load(models.UserRels.AccessTokens),
|
||||||
|
qm.Load(models.UserRels.RefreshTokens),
|
||||||
|
).One(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, null.StringFrom(usernameLowerTrimmed), user.Username)
|
||||||
|
assert.True(t, user.LastAuthenticatedAt.Valid)
|
||||||
|
assert.WithinDuration(t, s.Clock.Now(), user.LastAuthenticatedAt.Time, time.Second*10)
|
||||||
|
assert.EqualValues(t, s.Config.Auth.DefaultUserScopes, user.Scopes)
|
||||||
|
|
||||||
|
assert.NotNil(t, user.R.AppUserProfile)
|
||||||
|
assert.False(t, user.R.AppUserProfile.LegalAcceptedAt.Valid)
|
||||||
|
|
||||||
|
assert.Len(t, user.R.AccessTokens, 1)
|
||||||
|
assert.Equal(t, strfmt.UUID4(user.R.AccessTokens[0].Token), *response.AccessToken)
|
||||||
|
assert.Len(t, user.R.RefreshTokens, 1)
|
||||||
|
assert.Equal(t, strfmt.UUID4(user.R.RefreshTokens[0].Token), *response.RefreshToken)
|
||||||
|
|
||||||
|
res2 := test.PerformRequest(t, s, "POST", "/api/v1/auth/login", payload, nil)
|
||||||
|
assert.Equal(t, http.StatusOK, res2.Result().StatusCode)
|
||||||
|
|
||||||
|
var response2 types.PostLoginResponse
|
||||||
|
test.ParseResponseAndValidate(t, res2, &response2)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, response2.AccessToken)
|
||||||
|
assert.NotEqual(t, response.AccessToken, *response2.AccessToken)
|
||||||
|
assert.NotEmpty(t, response2.RefreshToken)
|
||||||
|
assert.NotEqual(t, response.RefreshToken, *response2.RefreshToken)
|
||||||
|
assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response2.ExpiresIn)
|
||||||
|
assert.Equal(t, auth.TokenTypeBearer, *response2.TokenType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRegisterAlreadyExists(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
payload := test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", payload, nil)
|
||||||
|
test.RequireHTTPError(t, res, httperrors.ErrConflictUserAlreadyExists)
|
||||||
|
|
||||||
|
user, err := models.Users(
|
||||||
|
models.UserWhere.Username.EQ(fix.User1.Username),
|
||||||
|
qm.Load(models.UserRels.AppUserProfile),
|
||||||
|
qm.Load(models.UserRels.AccessTokens),
|
||||||
|
qm.Load(models.UserRels.RefreshTokens),
|
||||||
|
).One(ctx, s.DB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, user.ID, fix.User1.ID)
|
||||||
|
|
||||||
|
assert.NotNil(t, user.R.AppUserProfile)
|
||||||
|
assert.Len(t, user.R.AccessTokens, 1)
|
||||||
|
assert.Len(t, user.R.RefreshTokens, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRegisterBadRequest(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
fix := fixtures.Fixtures()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
payload test.GenericPayload
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "MissingUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": "definitely not an email",
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyUsername",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": "",
|
||||||
|
"password": fixtures.PlainTestUserPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyPassword",
|
||||||
|
payload: test.GenericPayload{
|
||||||
|
"username": fix.User1.Username,
|
||||||
|
"password": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
res := test.PerformRequest(t, s, "POST", "/api/v1/auth/register", tt.payload, nil)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
|
||||||
|
var response httperrors.HTTPValidationError
|
||||||
|
test.ParseResponseAndValidate(t, res, &response)
|
||||||
|
|
||||||
|
test.Snapshoter.Save(t, response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
55
internal/api/handlers/common/get_healthy.go
Normal file
55
internal/api/handlers/common/get_healthy.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// nolint:revive
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// We use 521 to indicate an error state
|
||||||
|
// same as Cloudflare: https://support.cloudflare.com/hc/en-us/articles/115003011431#521error
|
||||||
|
httpStatusDown = 521
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetHealthyRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.Management.GET("/healthy", getHealthyHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heathly check (= liveness)
|
||||||
|
// Returns an human readable string about the current service status.
|
||||||
|
// In addition to readiness probes, it performs actual write probes.
|
||||||
|
// Note that /-/healthy is private (shielded by the mgmt-secret) as it may expose sensitive information about your service.
|
||||||
|
// Structured upon https://prometheus.io/docs/prometheus/latest/management_api/
|
||||||
|
func getHealthyHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
if !s.Ready() {
|
||||||
|
return c.String(httpStatusDown, "Not ready.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var str strings.Builder
|
||||||
|
fmt.Fprintln(&str, "Ready.")
|
||||||
|
|
||||||
|
// General Timeout and associated context.
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request().Context(), s.Config.Management.LivenessTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
healthyStr, errs := ProbeLiveness(ctx, s.DB, s.Config.Management.ProbeWriteablePathsAbs, s.Config.Management.ProbeWriteableTouchfile)
|
||||||
|
str.WriteString(healthyStr)
|
||||||
|
|
||||||
|
// Finally return the health status according to the seen states
|
||||||
|
if ctx.Err() != nil || len(errs) != 0 {
|
||||||
|
fmt.Fprintln(&str, "Probes failed.")
|
||||||
|
return c.String(httpStatusDown, str.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(&str, "Probes succeeded.")
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, str.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
110
internal/api/handlers/common/get_healthy_test.go
Normal file
110
internal/api/handlers/common/get_healthy_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package common_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHealthySuccess(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
// explicitly set touchfile that no other test has (so we can explicitly remove it beforehand.)
|
||||||
|
s.Config.Management.ProbeWriteableTouchfile = ".healthy-test"
|
||||||
|
|
||||||
|
for _, writeablePath := range s.Config.Management.ProbeWriteablePathsAbs {
|
||||||
|
os.Remove(path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile))
|
||||||
|
|
||||||
|
// also remove after test completion.
|
||||||
|
defer os.Remove(path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile))
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
require.Contains(t, res.Body.String(), "seq_health=1")
|
||||||
|
|
||||||
|
firstTouchTime := make([]time.Time, len(s.Config.Management.ProbeWriteablePathsAbs))
|
||||||
|
|
||||||
|
// expect a new touchfiles were written
|
||||||
|
for i, writeablePath := range s.Config.Management.ProbeWriteablePathsAbs {
|
||||||
|
filePath := path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile)
|
||||||
|
stat, err := os.Stat(filePath)
|
||||||
|
require.NoErrorf(t, err, "Expected to have %v", filePath)
|
||||||
|
firstTouchTime[i] = stat.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
res = test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
require.Contains(t, res.Body.String(), "seq_health=2")
|
||||||
|
|
||||||
|
// expect touchfiles modTime was updated
|
||||||
|
for i, writeablePath := range s.Config.Management.ProbeWriteablePathsAbs {
|
||||||
|
filePath := path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile)
|
||||||
|
stat, err := os.Stat(filePath)
|
||||||
|
require.NoErrorf(t, err, "Expected to have %v", filePath)
|
||||||
|
|
||||||
|
assert.NotEqual(t, firstTouchTime[i], stat.ModTime())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHealthyNoAuth(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/healthy", nil, nil)
|
||||||
|
require.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHealthyWrongAuth(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret=i-have-no-idea-about-the-pass", nil, nil)
|
||||||
|
require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHealthyDBPingError(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
// forcefully close the DB
|
||||||
|
s.DB.Close()
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||||
|
require.Equal(t, 521, res.Result().StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHealthyDBSeqError(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
// forcefully remove the sequence
|
||||||
|
if _, err := s.DB.Exec("DROP SEQUENCE seq_health;"); err != nil {
|
||||||
|
t.Fatal(err, "was unable to drop sequence seq_health")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||||
|
require.Equal(t, 521, res.Result().StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHealthyMountError(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
s.Config.Management.ProbeWriteablePathsAbs = []string{"/this/path/does/not/exist"}
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||||
|
require.Equal(t, 521, res.Result().StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHealthyNotReady(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
// forcefully remove an initialized component to check if ready state works
|
||||||
|
s.Mailer = nil
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||||
|
require.Equal(t, 521, res.Result().StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
40
internal/api/handlers/common/get_ready.go
Normal file
40
internal/api/handlers/common/get_ready.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// nolint:revive
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetReadyRoute(s *api.Server) *echo.Route {
|
||||||
|
return s.Router.Management.GET("/ready", getReadyHandler(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readiness check
|
||||||
|
// This endpoint returns 200 when our Service is ready to serve traffic (i.e. respond to queries).
|
||||||
|
// Does read-only probes apart from the general server ready state.
|
||||||
|
// Note that /-/ready is typically public (and not shielded by a mgmt-secret), we thus prevent information leakage here and only return `"Ready."`.
|
||||||
|
// Structured upon https://prometheus.io/docs/prometheus/latest/management_api/
|
||||||
|
func getReadyHandler(s *api.Server) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
if !s.Ready() {
|
||||||
|
return c.String(httpStatusDown, "Not ready.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// General Timeout and associated context.
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request().Context(), s.Config.Management.ReadinessTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, errs := ProbeReadiness(ctx, s.DB, s.Config.Management.ProbeWriteablePathsAbs)
|
||||||
|
|
||||||
|
// Finally return the health status according to the seen states
|
||||||
|
if ctx.Err() != nil || len(errs) != 0 {
|
||||||
|
return c.String(httpStatusDown, "Not ready.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, "Ready.")
|
||||||
|
}
|
||||||
|
}
|
||||||
41
internal/api/handlers/common/get_ready_test.go
Normal file
41
internal/api/handlers/common/get_ready_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package common_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||||
|
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetReadyReadiness(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil)
|
||||||
|
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||||
|
require.Equal(t, "Ready.", res.Body.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetReadyReadinessBroken(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
// forcefully remove an initialized component to check if ready state works
|
||||||
|
s.Mailer = nil
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil)
|
||||||
|
require.Equal(t, 521, res.Result().StatusCode)
|
||||||
|
require.Equal(t, "Not ready.", res.Body.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetReadyDBBrokenNotReady(t *testing.T) {
|
||||||
|
test.WithTestServer(t, func(s *api.Server) {
|
||||||
|
// forcefully remove pg
|
||||||
|
err := s.DB.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil)
|
||||||
|
require.Equal(t, 521, res.Result().StatusCode)
|
||||||
|
require.Equal(t, "Not ready.", res.Body.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user