feat: 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-08 01:15:07 +00:00
commit 961bdd81d4
146 files changed
+10444

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
indent_size = 4
[*.{js,ts,vue,json,md,yml,yaml,html,css}]
indent_style = space
indent_size = 2
+16
View File
@@ -0,0 +1,16 @@
name: Run unit tests
on:
pull_request:
branches: [master]
jobs:
build:
name: Build image
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: https://github.com/actions/checkout@v4
#TODO: IMPLEMENT BUILD STEPS HERE
+46
View File
@@ -0,0 +1,46 @@
name: Run unit tests
on:
pull_request:
branches: [master]
jobs:
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16.3
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: simple_bank
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Set up Go 1.25.x
uses: https://github.com/actions/setup-go@v5
with:
go-version: "1.25.4"
- name: Check out code
uses: https://github.com/actions/checkout@v4
- name: Install golang-migrate
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate.linux-amd64 /usr/bin/migrate
which migrate
- name: Run migrations
run: make migrate-up
- name: Run tests
run: make test
+16
View File
@@ -0,0 +1,16 @@
name: Run unit tests
on:
pull_request:
branches: [master]
jobs:
build:
name: Build image
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
#TODO: IMPLEMENT BUILD STEPS HERE
+49
View File
@@ -0,0 +1,49 @@
name: Run unit tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16.3
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: simple_bank
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
go-version: ^1.25.4
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Install golang-migrate
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Run migrations
run: make migrate-up
- name: Test
run: make test
+27
View File
@@ -0,0 +1,27 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
.DS_Store
*.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/
# Go workspace file
go.work
go.work.sum
# env file
.env
+34
View File
@@ -0,0 +1,34 @@
version: "2"
run:
timeout: 5m
linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- revive
settings:
revive:
rules:
- name: exported
arguments:
- disableStutteringCheck: false
- name: package-comments
- name: error-naming
- name: error-strings
- name: receiver-naming
- name: var-naming
staticcheck:
checks: ["all", "-ST1000"]
exclusions:
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
formatters:
enable:
- gofumpt
+33
View File
@@ -0,0 +1,33 @@
meta {
name: Login
type: http
seq: 1
}
post {
url: {{uri}}/auth/login
body: json
auth: inherit
}
body:json {
{
"username":"test",
"password":"TestPassword"
}
}
script:post-response {
const data = res.getBody();
const token = data.data.access_token;
const refresh_token = data.data.refresh_token;
bru.setEnvVar("token", token);
bru.setEnvVar("refresh_token", refresh_token);
}
settings {
encodeUrl: true
timeout: 0
}
+30
View File
@@ -0,0 +1,30 @@
meta {
name: Refresh
type: http
seq: 2
}
post {
url: {{uri}}/auth/refresh
body: json
auth: inherit
}
body:json {
{
"refresh_token": "{{refresh_token}}"
}
}
script:post-response {
const data = res.getBody();
const token = data.data.access_token;
bru.setEnvVar("token", token);
}
settings {
encodeUrl: true
timeout: 0
}
+8
View File
@@ -0,0 +1,8 @@
meta {
name: Auth
seq: 1
}
auth {
mode: inherit
}
+15
View File
@@ -0,0 +1,15 @@
meta {
name: Get User
type: http
seq: 2
}
get {
url: {{uri
body: none
auth: inherit
}
settings {
encodeUrl: true
}
+20
View File
@@ -0,0 +1,20 @@
meta {
name: Me
type: http
seq: 1
}
get {
url: {{uri}}/users/me
body: none
auth: bearer
}
auth:bearer {
token: {{token}}
}
settings {
encodeUrl: true
timeout: 0
}
+8
View File
@@ -0,0 +1,8 @@
meta {
name: Users
seq: 2
}
auth {
mode: inherit
}
+8
View File
@@ -0,0 +1,8 @@
meta {
name: HTTP
seq: 2
}
auth {
mode: inherit
}
+9
View File
@@ -0,0 +1,9 @@
{
"version": "1",
"name": "TemplateAPI",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}
@@ -0,0 +1,7 @@
vars {
uri: http://localhost:8080/api/v1
}
vars:secret [
token,
refresh_token
]
+24
View File
@@ -0,0 +1,24 @@
#Build stage
FROM golang:1.25.4-alpine3.22 AS builder
WORKDIR /api
COPY . .
RUN go build -o main main.go
#Run stage
FROM alpine:3.22
WORKDIR /api
COPY --from=builder /api/main .
COPY api.env .
COPY start.sh .
COPY wait-for.sh .
COPY db/migrations ./db/migrations
EXPOSE 8080
CMD [ "/api/main" ]
ENTRYPOINT [ "/api/start.sh" ]
+58
View File
@@ -0,0 +1,58 @@
include api.env
MIGRATION_PATH = ./cmd/db/migrations
export
test:
@go test -v -cover -short ./...
mock:
@mockgen --package mockdb --destination internal/db/mock/store.go $(PKG_NAME)/internal/db/sqlc Store
@mockgen --package mockwk --destination internal/worker/mock/distributor.go $(PKG_NAME)/internal/worker TaskDistributor
@mockgen --package mocksvc --destination internal/service/mock/service.go $(PKG_NAME)/internal/service UserService,AuthService
up:
@docker compose --env-file api.env up -d
down:
@docker compose --env-file api.env down
reload:
@docker compose --env-file api.env down && docker rmi $(PROJ_NAME)-api && docker compose --env-file api.env up -d
server:
@air -c air.toml
migration:
@migrate create -seq -ext sql -dir $(MIGRATION_PATH) $(filter-out $@,$(MAKECMDGOALS))
migrate-up:
@migrate -path=$(MIGRATION_PATH) -database=$(DB_ADDR) -verbose up
migrate-down:
@migrate -path=$(MIGRATION_PATH) -database=$(DB_ADDR) -verbose down $(filter-out $@,$(MAKECMDGOALS))
sqlc:
@sqlc generate
dbdocs:
@dbdocs build -o ./docs/dbdocs ./docs/db.dbml
@echo "Database documentation generated at ./docs/dbdocs"
dbschema:
@dbml2sql --postgres -o docs/schema.sql docs/db.dbml
proto:
@rm -f pb/*.go
@rm -f docs/swagger/*.swagger.json
@protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
--go-grpc_out=pb --go-grpc_opt=paths=source_relative \
--grpc-gateway_out=pb --grpc-gateway_opt=paths=source_relative \
--openapiv2_out=docs/swagger --openapiv2_opt=allow_merge=true,merge_file_name=$(PROJ_NAME)\
proto/*.proto
@statik -src=./docs/swagger -dest=./docs
evans:
@evans --host localhost --port 9090 -r repl
lint:
@golangci-lint run ./...
.PHONY: test mock up down reload server migration migrate-up migrate-down sqlc dbdocs dbschema proto evans lint
+171
View File
@@ -0,0 +1,171 @@
# simple_bank
simple_bank is a small example backend service in Go that demonstrates a banking-style API with:
- HTTP gateway (grpc-gateway) and gRPC server
- PostgreSQL for persistent storage (SQLC generated queries)
- Redis + Asynq for background task queue / worker processing
- Migrations via golang-migrate
- Email verification task example
This repository is intended as a reference/demo for building a production-ish Go service with background jobs.
---
## Features
- RESTful HTTP gateway (default `:8080`) backed by gRPC (default `:9090`)
- gRPC API and generated protobufs (uses grpc-gateway)
- Database operations generated with `sqlc`
- Background job dispatching and processing with `asynq` (Redis)
- Config-driven via environment variables
- Example Send Verification Email background task
---
## Prerequisites
- Go 1.20+ (or compatible)
- PostgreSQL
- Redis
- make/migrate/other CLIs (optional for migrations)
- Docker & Docker Compose (recommended for local dev)
---
## Quickstart (development)
1. Copy environment example (or use `api.env`):
- Main variables you will want to set:
- `DB_ADDR` (Postgres connection string)
- `REDIS_ADDR` (Redis host:port)
- `HTTP_SERVER_ADDR` (default `:8080`)
- `GRPC_SERVER_ADDR` (default `:9090`)
- `ENVIRONMENT` (e.g. `development` to enable console logs)
2. Start local infra with Docker Compose (if you want local Postgres + Redis):
- (This repo includes a `docker-compose.yml` with Postgres and Redis)
- Example:
- docker-compose up -d
- Confirm Postgres and Redis are reachable at the addresses in your env.
3. Run DB migrations:
- Using golang-migrate CLI (example):
- migrate -path db/migrations -database "postgres://postgres:postgres@localhost:5432/simple_bank?sslmode=disable" up
- Adjust the `database` URL to match your `DB_ADDR` environment variable.
4. Download dependencies and build:
- go mod download
- go build -o simple_bank ./...
5. Run the service:
- With the environment loaded (example):
- export $(cat api.env | xargs) # or use your preferred env loader
- ./simple_bank
- Running the binary will:
- start the task processor (asynq server) that consumes background jobs
- start gRPC server (on `GRPC_SERVER_ADDR`)
- start the HTTP gateway (on `HTTP_SERVER_ADDR`) in a goroutine/handler
- Alternatively, run directly with `go run`:
- go run ./main.go
6. Verify:
- HTTP gateway: http://localhost:8080 (or whatever `HTTP_SERVER_ADDR` you configure)
- gRPC: connect at the configured `GRPC_SERVER_ADDR`
- Redis dashboard / Redis Commander: if using docker-compose, a redis-commander service may be available
---
## Background Worker (Asynq)
- Tasks are enqueued by the app and processed by an asynq server embedded in the application.
- The worker configuration (queues and handlers) lives under `worker/`:
- `worker.NewRedisTaskProcessor` creates the asynq server (`asynq.Server`) with queue weights and error handler.
- `worker.NewRedisTaskDistributor` creates a client to enqueue tasks.
- Example queue names: `CriticalQueue`, `DefaultQueue`.
Customizing logs and formatting for asynq:
- The asynq server can be given a custom logger via `asynq.Config.Logger`. This project includes `worker/logger.go` which adapts logs to `zerolog`. Edit `worker/logger.go` if you want to change asynq log formatting/fields.
- If you also want to capture/format other libraries' logs (for example libraries that use the standard `log` package), redirect stdlib logs to your `zerolog` instance at startup.
---
## Environment Variables
Important variables (see `api.env` for full list):
- Server:
- `HTTP_SERVER_ADDR` (default `:8080`)
- `GRPC_SERVER_ADDR` (default `:9090`)
- `ENVIRONMENT` (`development` for console logs)
- Database:
- `DB_DRIVER` (e.g. `pgx`)
- `DB_ADDR` (postgres connection string)
- `MIGRATIONS_URL` (e.g. `file://db/migrations`)
- Redis:
- `REDIS_ADDR` (e.g. `localhost:6379`)
- `REDIS_PASSWORD`
- `REDIS_DB`
- Auth / Tokens:
- `AUTH_TOKEN_SECRET`, `AUTH_TOKEN_EXP`, `REFRESH_TOKEN_EXP`
- Email:
- `EMAIL_FROM`, `EMAIL_SMTP_PASSWORD` (used by the worker email task)
- Rate limiter:
- `RATE_LIMITER_REQS_COUNT`, `RATE_LIMITER_TIME_FRAME`, `RATE_LIMITER_ENABLED`
---
## Testing
- Unit tests:
- go test ./...
- Integration tests:
- Ensure Postgres and Redis are running and configured as in `api.env` before running tests that depend on external services.
---
## Development tips
- To change queue weights, error handling or task handlers, edit `worker/processor.go`.
- To change how tasks are enqueued, review `worker/distributer.go`.
- To adjust logging format globally, modify `main.go` startup logger configuration and `worker/logger.go`.
- Protobuf / gRPC: regenerate stubs if you change proto files (this project uses `pb/` and generated code).
---
## Project layout (high level)
- `main.go` — application entrypoint
- `api/` — Gin HTTP handlers (HTTP server)
- `gapi/` — gRPC server, grpc-gateway wiring
- `worker/` — asynq task distributor & processor, custom logger
- `db/` — SQLC generated code and migrations
- `pb/` — generated protobuf/gRPC code
- `docs/statik` — static files (swagger UI) bundled with the binary
---
## Troubleshooting
- If migrations fail:
- Confirm `DB_ADDR` is correct and database is reachable.
- Check that `db/migrations` contains the migration files and paths are correct.
- If Redis jobs don't process:
- Confirm the app can reach Redis (`REDIS_ADDR`).
- Ensure the process that runs the asynq server is running (the app starts it by default).
- If logs appear in different formats:
- Redirect stdlib logs into `zerolog` or update `worker/logger.go` to include more structured context.
---
## Contributing
- Fork and open pull requests.
- Keep changes small and focused; add/update unit tests where appropriate.
---
If you'd like, I can:
- Add a short example curl snippet for the HTTP API endpoints.
- Add a sample `docker-compose` snippet and explicit commands for local development.
- Add a "Deploy" section with production tips (env secrets, TLS, metrics, etc.).
+52
View File
@@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/api"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "docs", "scripts"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = ["cmd", "internal", "pkg"]
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true
+45
View File
@@ -0,0 +1,45 @@
PKG_NAME=github.com/FernandoJVideira/simple_bank
PROJ_NAME=simple_bank
SERVICE_NAME="Simple Bank"
# Server Configuration
HTTP_SERVER_ADDR=:8080
GRPC_SERVER_ADDR=:9090
EXTERNAL_URL=localhost:8080
FRONTEND_URL=http://localhost:5173
ENVIRONMENT=development
ALLOWED_ORIGINS=https://*,http://* # TODO: Change to the actual allowed origins on production
# Database Configuration
DB_ADDR=postgres://postgres:postgres@localhost:5432/simple_bank?sslmode=disable
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_MAX_OPEN_CONNS=30
DB_MAX_IDLE_CONNS=30
DB_MAX_IDLE_TIME=15m
MIGRATIONS_URL=file://cmd/db/migrations
# Redis Configuration
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0
# Authentication
AUTH_TOKEN_SECRET=gDEt23BT2AsYzYUDu.f94mqBKLF@-w_ao
AUTH_TOKEN_MIN_KEY_SIZE=32
AUTH_TOKEN_EXP=15m
REFRESH_TOKEN_EXP=168h
AUTH_TOKEN_ISS=lkapi
AUTH_TOKEN_AUD=lkapi
# Email Configuration
EMAIL_FROM=[email protected]
EMAIL_FROM_NAME=Simple Bank
EMAIL_SMTP_PASSWORD=afyvhkfwafcedvin
# Rate Limiter
RATE_LIMITER_REQS_COUNT=20
RATE_LIMITER_TIME_FRAME=1m
RATE_LIMITER_ENABLED=true
+45
View File
@@ -0,0 +1,45 @@
PKG_NAME=github.com/FernandoJVideira/simple_bank
PROJ_NAME=simple_bank
SERVICE_NAME="Simple Bank"
# Server Configuration
HTTP_SERVER_ADDR=:8080
GRPC_SERVER_ADDR=:9090
EXTERNAL_URL=localhost:8080
FRONTEND_URL=http://localhost:5173
ENVIRONMENT=development
ALLOWED_ORIGINS=https://*,http://* # TODO: Change to the actual allowed origins on production
# Database Configuration
DB_ADDR=postgres://postgres:postgres@localhost:5432/simple_bank?sslmode=disable
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_MAX_OPEN_CONNS=30
DB_MAX_IDLE_CONNS=30
DB_MAX_IDLE_TIME=15m
MIGRATIONS_URL=file://db/migrations
# Redis Configuration
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0
# Authentication
AUTH_TOKEN_SECRET=gDEt23BT2AsYzYUDu.f94mqBKLF@-w_ao
AUTH_TOKEN_MIN_KEY_SIZE=32
AUTH_TOKEN_EXP=15m
REFRESH_TOKEN_EXP=168h
AUTH_TOKEN_ISS=lkapi
AUTH_TOKEN_AUD=lkapi
# Email Configuration
EMAIL_FROM=[email protected]
EMAIL_FROM_NAME=Simple Bank
EMAIL_SMTP_PASSWORD=afyvhkfwafcedvin
# Rate Limiter
RATE_LIMITER_REQS_COUNT=20
RATE_LIMITER_TIME_FRAME=1m
RATE_LIMITER_ENABLED=true
+300
View File
@@ -0,0 +1,300 @@
package main
import (
"context"
"errors"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/hibiken/asynq"
"github.com/rs/cors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"golang.org/x/sync/errgroup"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
gapi "github.com/FernandoJVideira/simple_bank/internal/handlers/grpc"
api "github.com/FernandoJVideira/simple_bank/internal/handlers/http"
"github.com/FernandoJVideira/simple_bank/internal/mailer"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/rakyll/statik/fs"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"google.golang.org/protobuf/encoding/protojson"
_ "github.com/FernandoJVideira/simple_bank/docs/statik"
)
var interruptSignals = []os.Signal{
os.Interrupt,
syscall.SIGTERM,
syscall.SIGINT,
}
// @title Simple Bank API
// @version 1.0
// @description API for Simple Bank application
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email [email protected]
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @host localhost:8080
// @BasePath /v1
// @schemes http https
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func main() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal().Err(err).Msg("cannot load config")
}
if config.Environment == "development" {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
ctx, cancel := signal.NotifyContext(context.Background(), interruptSignals...)
defer cancel()
connPool, err := pgxpool.New(ctx, config.DBAddr)
if err != nil {
log.Fatal().Err(err).Msg("cannot connect to db")
}
runDBMigration(config.MigrationURL, config.DBAddr)
redisOpt := asynq.RedisClientOpt{
Addr: config.RedisAddr,
Password: config.RedisPassword,
DB: config.RedisDB,
}
taskDist := worker.NewRedisTaskDistributor(redisOpt)
store := db.NewPostgresStore(connPool)
tokenMaker, err := auth.NewPasetoMaker(config)
if err != nil {
log.Fatal().Err(err).Msg("cannot create token maker")
}
userService := service.NewUserService(store, taskDist)
authService := service.NewAuthService(store, tokenMaker, config)
wg, ctx := errgroup.WithContext(ctx)
runGinServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
runTaskProcessor(ctx, wg, redisOpt, store, config)
//runGatewayServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
//runGrpcServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
err = wg.Wait()
if err != nil {
log.Fatal().Err(err).Msg("application terminated with error")
}
}
func runTaskProcessor(ctx context.Context, wg *errgroup.Group, redisOpt asynq.RedisClientOpt, store db.Store, config config.Config) {
mailer := mailer.NewGmailSender(config.EmailSenderName, config.EmailSender, config.EmailPassword)
taskProcessor := worker.NewRedisTaskProcessor(redisOpt, store, mailer, config)
log.Info().Msg("start task processor")
if err := taskProcessor.Start(); err != nil {
log.Fatal().Err(err).Msg("cannot start task processor")
}
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("gracefully shutting down task processor")
taskProcessor.Shutdown()
log.Info().Msg("task processor shut down")
return nil
})
}
func runDBMigration(migrationURL, dbSource string) {
migration, err := migrate.New(migrationURL, dbSource)
if err != nil {
log.Fatal().Err(err).Msg("cannot create new migrate instance")
}
if err = migration.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal().Err(err).Msg("failed to run migrate up")
}
log.Info().Msg("db migrated successfully")
}
func runGinServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, td worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
s, err := api.NewServer(config, store, td, tokenMaker, userService, authService)
if err != nil {
log.Fatal().Err(err).Msg("cannot create server")
}
wg.Go(func() error {
err = s.Start(config.HTTPServerAddr)
if err != nil {
log.Fatal().Err(err).Msg("cannot start server:")
return err
}
return nil
})
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("shutting down server")
err := s.Shutdown(ctx)
if err != nil {
log.Error().Err(err).Msg("cannot shutdown server")
return err
}
log.Info().Msg("server shut down")
return nil
})
}
func runGrpcServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, taskDistributor worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
server, err := gapi.NewServer(config, store, taskDistributor, tokenMaker, userService, authService)
if err != nil {
log.Fatal().Err(err).Msg("cannot create server:")
}
grpcLogger := grpc.UnaryInterceptor(gapi.GrpcLogger)
grpcServer := grpc.NewServer(grpcLogger)
pb.RegisterSimpleBankServer(grpcServer, server)
reflection.Register(grpcServer)
listener, err := net.Listen("tcp", config.GrpcServerAddr)
if err != nil {
log.Fatal().Err(err).Msg("cannot create listener")
}
wg.Go(func() error {
log.Info().Msgf("start gRPC server at %s", listener.Addr().String())
err = grpcServer.Serve(listener)
if err != nil {
if errors.Is(err, grpc.ErrServerStopped) {
return nil
}
log.Error().Err(err).Msg("gRPC server failed to serve")
return err
}
return nil
})
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("gracefully shutting down gRPC server")
grpcServer.GracefulStop()
log.Info().Msg("gRPC server shut down")
return nil
})
}
func runGatewayServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, taskDistributor worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
server, err := gapi.NewServer(config, store, taskDistributor, tokenMaker, userService, authService)
if err != nil {
log.Fatal().Err(err).Msg("cannot create server:")
}
opts := runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
},
})
grpcMux := runtime.NewServeMux(opts)
err = pb.RegisterSimpleBankHandlerServer(ctx, grpcMux, server)
if err != nil {
log.Fatal().Err(err).Msg("cannot register handler server")
}
mux := http.NewServeMux()
mux.Handle("/", grpcMux)
statikFS, err := fs.New()
if err != nil {
log.Fatal().Err(err).Msg("cannot create statik fs")
}
swaggerHandler := http.StripPrefix("/swagger/", http.FileServer(statikFS))
mux.Handle("/swagger/", swaggerHandler)
c := cors.New(
cors.Options{
AllowedOrigins: config.AllowedOrigins,
AllowedMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowedHeaders: []string{
"Accept",
"Authorization",
"Content-Type",
"X-CSRF-Token",
},
AllowCredentials: true,
Debug: config.Environment == "development",
},
)
handler := c.Handler(gapi.HTTPLogger(mux))
httpServer := &http.Server{
Handler: handler,
Addr: config.HTTPServerAddr,
}
wg.Go(func() error {
log.Info().Msgf("start HTTP gateway server at %s", httpServer.Addr)
err = httpServer.ListenAndServe()
if err != nil {
if errors.Is(err, http.ErrServerClosed) {
return nil
}
log.Error().Err(err).Msg("cannot start http gateway server")
return err
}
return nil
})
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("gracefully shutting down HTTP gateway server")
err := httpServer.Shutdown(context.Background())
if err != nil {
log.Error().Err(err).Msg("cannot shut down http gateway server")
return err
}
log.Info().Msg("HTTP gateway server shut down")
return nil
})
}
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS "users";
DROP TABLE IF EXISTS "verify_emails";
DROP TABLE IF EXISTS "sessions";
@@ -0,0 +1,39 @@
-- SQL dump generated using DBML (dbml.dbdiagram.io)
-- Database: PostgreSQL
-- Generated at: 2025-11-30T03:13:20.470Z
CREATE TABLE "users" (
"username" varchar PRIMARY KEY,
"role" varchar NOT NULL DEFAULT 'member',
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"is_email_verified" bool NOT NULL DEFAULT false,
"password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01',
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "verify_emails" (
"id" bigserial PRIMARY KEY,
"username" varchar NOT NULL,
"email" varchar NOT NULL,
"secret_code" varchar NOT NULL,
"is_used" bool NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT (now()),
"expired_at" timestamptz NOT NULL DEFAULT (now() + interval '15 minutes')
);
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY,
"username" varchar NOT NULL,
"refresh_token" varchar NOT NULL,
"user_agent" varchar NOT NULL,
"client_ip" varchar NOT NULL,
"is_blocked" boolean NOT NULL DEFAULT false,
"expires_at" timestamptz NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
ALTER TABLE "verify_emails" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
ALTER TABLE "sessions" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
+17
View File
@@ -0,0 +1,17 @@
-- name: CreateSession :one
INSERT INTO sessions (
id,
username,
refresh_token,
user_agent,
client_ip,
is_blocked,
expires_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
)
RETURNING *;
-- name: GetSession :one
SELECT * FROM sessions
WHERE id = $1 LIMIT 1;
+25
View File
@@ -0,0 +1,25 @@
-- name: CreateUser :one
INSERT INTO users (
username,
hashed_password,
full_name,
email
) VALUES (
$1, $2, $3, $4
)
RETURNING *;
-- name: GetUser :one
SELECT * FROM users
WHERE username = $1 LIMIT 1;
-- name: UpdateUser :one
UPDATE users
SET
hashed_password = COALESCE(sqlc.narg('hashed_password'), hashed_password),
password_changed_at = COALESCE(sqlc.narg('password_changed_at'), password_changed_at),
full_name = COALESCE(sqlc.narg('full_name'), full_name),
email = COALESCE(sqlc.narg('email'), email),
is_email_verified = COALESCE(sqlc.narg('is_email_verified'), is_email_verified)
WHERE username = sqlc.arg('username')
RETURNING *;
+21
View File
@@ -0,0 +1,21 @@
-- name: CreateVerifyEmail :one
INSERT INTO verify_emails (
username,
email,
secret_code
) VALUES (
$1, $2, $3
)
RETURNING *;
-- name: UpdateVerifyEmail :one
UPDATE verify_emails
SET
is_used = TRUE
WHERE
id = @id
AND secret_code = @secret_code
AND is_used = FALSE
AND expired_at > NOW()
RETURNING *;
View File
Whitespace-only changes.
+58
View File
@@ -0,0 +1,58 @@
services:
db:
image: postgres:18.1-alpine3.22
container_name: postgres
networks:
- api-network
environment:
POSTGRES_DB: ${PROJ_NAME:-temp_api}
POSTGRES_USER: ${DB_USERNAME:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
volumes:
- db-data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:8.4.0-alpine
restart: unless-stopped
container_name: redis
networks:
- api-network
ports:
- "6379:6379"
command: redis-server --save 60 1 --loglevel warning
redis-commander:
container_name: redis-commander
hostname: redis-commander
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOST=redis
ports:
- "127.0.0.1:8081:8081"
depends_on:
- redis
restart: unless-stopped
# api:
# build:
# context: .
# dockerfile: Dockerfile
# networks:
# - api-network
# ports:
# - "8080:8080"
# - "9090:9090" # for GRPC API
# depends_on:
# - db
# - redis
# container_name: ${PROJ_NAME:-temp_api}-api
# environment:
# - DB_ADDR=${DB_ADDR} # db:5432 inside the docker network
# - REDIS_ADDR=${REDIS_ADDR} # redis:6379 inside the docker network
# - SERVER_ADDRESS=0.0.0.0:${SERVER_ADDR:-8080}
# entrypoint: ["/api/wait-for.sh", "db:5432", "--", "/api/start.sh"]
# command: ["/api/main"]
volumes:
db-data:
networks:
api-network:
driver: bridge
+38
View File
@@ -0,0 +1,38 @@
Project simple_bank {
database_type: 'PostgreSQL'
Note: '''
# Simple Bank Database
'''
}
Table users as U {
username varchar [pk]
role varchar [not null, default: 'member']
hashed_password varchar [not null]
full_name varchar [not null]
email varchar [unique, not null]
is_email_verified bool [not null, default: false]
password_changed_at timestamptz [not null, default: '0001-01-01']
created_at timestamptz [not null, default: `now()`]
}
Table verify_emails {
id bigserial [pk]
username varchar [ref: > U.username, not null]
email varchar [not null]
secret_code varchar [not null]
is_used bool [not null, default: false]
created_at timestamptz [not null, default: `now()`]
expired_at timestamptz [not null, default: `now() + interval '15 minutes'`]
}
Table sessions {
id uuid [pk]
username varchar [ref: > U.username, not null]
refresh_token varchar [not null]
user_agent varchar [not null]
client_ip varchar [not null]
is_blocked boolean [not null, default: false]
expires_at timestamptz [not null]
created_at timestamptz [not null, default: `now()`]
}
+172
View File
@@ -0,0 +1,172 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"email": "[email protected]"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/v1/auth/register": {
"post": {
"description": "Register a new user account",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Create a new user",
"parameters": [
{
"description": "User registration details",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.createUserRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.JSONResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api.userResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/util.JSONResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/util.JSONResponse"
}
}
}
}
}
},
"definitions": {
"api.createUserRequest": {
"type": "object",
"required": [
"email",
"full_name",
"password",
"username"
],
"properties": {
"email": {
"type": "string"
},
"full_name": {
"type": "string"
},
"password": {
"type": "string",
"minLength": 6
},
"username": {
"type": "string"
}
}
},
"api.userResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"full_name": {
"type": "string"
},
"password_changed_at": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"util.JSONResponse": {
"type": "object",
"properties": {
"data": {},
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "Type \"Bearer\" followed by a space and JWT token.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:8080",
BasePath: "/v1",
Schemes: []string{"http", "https"},
Title: "Simple Bank API",
Description: "API for Simple Bank application",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
+39
View File
@@ -0,0 +1,39 @@
-- SQL dump generated using DBML (dbml.dbdiagram.io)
-- Database: PostgreSQL
-- Generated at: 2025-11-30T03:13:20.470Z
CREATE TABLE "users" (
"username" varchar PRIMARY KEY,
"role" varchar NOT NULL DEFAULT 'member',
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"is_email_verified" bool NOT NULL DEFAULT false,
"password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01',
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "verify_emails" (
"id" bigserial PRIMARY KEY,
"username" varchar NOT NULL,
"email" varchar NOT NULL,
"secret_code" varchar NOT NULL,
"is_used" bool NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT (now()),
"expired_at" timestamptz NOT NULL DEFAULT (now() + interval '15 minutes')
);
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY,
"username" varchar NOT NULL,
"refresh_token" varchar NOT NULL,
"user_agent" varchar NOT NULL,
"client_ip" varchar NOT NULL,
"is_blocked" boolean NOT NULL DEFAULT false,
"expires_at" timestamptz NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
ALTER TABLE "verify_emails" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
ALTER TABLE "sessions" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
+14
View File
@@ -0,0 +1,14 @@
// Code generated by statik. DO NOT EDIT.
package statik
import (
"github.com/rakyll/statik/fs"
)
func init() {
data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x0b\x03~[\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x00favicon-16x16.pngUT\x05\x00\x01\xb6\x8e+i\x00\x99\x02f\xfd\x89PNG\x0d\n\x1a\n\x00\x00\x00\x0dIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x03\x00\x00\x00(-\x0fS\x00\x00\x015PLTEb\xb14a\xb14^\xab5[\xa55W\xa07U\x9d7R\x978Q\x968/^@.]@-ZA+WA3f>4f?9o=%NC$MC$OC&MB$KB#LB!IC$KB$LB$MB%MB$NC%NC\x1cAD\x1c?E\x1fCD\x1dCD\x1fDD EC\"IC\"IC#JB'SA$LC&PA\"GB\"HC)VA+Y?$MA%MB\x146F\x154D\x152G\x113D\x125F\x101B\x0c1I\x15+@\x00$I\x003M\x0033\x00\x00\x00\x00\x00\x00\x85\xea-\x84\xe9,\x83\xe8,\x82\xe6-\x81\xe5,\x7f\xe2.\x80\xe1.}\xdd.|\xdd.v\xd20t\xd00r\xca1q\xc91p\xc81o\xc62m\xc51m\xc41l\xc32k\xc02j\xbf2i\xbe3h\xbb3h\xba3g\xb83K\x8d9J\x8a:J\x89:D\x7f;C\x7f<@y=>w=9n>8m>8n?6j?5h?3e?\x1b=E\x1b>E\x1c?E\x1c=E\x1eCE\x1fDD FD`%\x11/\x00\x00\x00;tRNS\xf4\xf4\xf5\xf5\xf6\xf5\xf7\xf6\xee\xee\xef\xf0\xea\xea\xe7\xe1\xe1\xe0\xe0\xe3\xe3\xdf\xdc\xdb\xdb\xda\xd9\xd8\xd8\xdb\xcf\xbf\xbc\xba\xac\xab\xa9\xa9\xa1\x99\x96\x94\x8e\x89\x85\x84L1$\x1e\x1d\x1f\x15\x0c\x07\n\x05\x01\x00\x07\x07\xae\xc9\x00\x00\x00\xd8IDATx\xda=\xcf\xd9.CQ\x18\x86\xe1\xcfn\x8a\x8dRi\xa9\"\x86\xb61\xcfs\xd6\xbb[\xb3\x84\x12\x1bA\x8c5\x94;u\xe0\x86\xa4\x12\xc1Z\xcdN\x9f\xa3\xff\xff\xce^\x19k.\x97Iv\x0fL-\xb9[\xc6\xac\x0fw\x94KP:N\x8c\xae\xbaac0N\xa4ih\xcd\x0e\x85\x96\xe8\xdd\xdb$\x967\x9a\xf7\xe1\xf2\x01\xeb\xf1\x1e\xda\x16T\x08\xe1}\x0bk\xe7\x0d\xc2I\xf5\x04\xf0\x1a\xe0\xbc@\xd0\xa7\x14\\\xdd\xec\x9f\x1f\x9c\x1e\x9eT. \xed\xfdI\xbfq\xff\xcb\xaf\xf9\xb5\xef\x98\xf4\xa3l\x00OE\x9c\xe7\"A\xaf\xc6C\xa8\xeebmW\xe1lB\xcb\xadp[\xc1\xba\xbb\x86\xf6E\x991\x8f\x86\xe6\x9c\xf1\x94\xca\x7f(\xf2\x99IK6p\xba\xf3\xc8\xc5\x95\x13#\xf58ke6\x9b\xec\xea\x9f\xa9\xe7\xff\x03\xcdJ9\x84\xc0\xe4\xbb\xd1\x00\x00\x00\x00IEND\xaeB`\x82\x01\x00\x00\xff\xffPK\x07\x08\\\xa1\xa9S\xa3\x02\x00\x00\x99\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x0b\x03~[\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x00favicon-32x32.pngUT\x05\x00\x01\xb6\x8e+i\x00t\x02\x8b\xfd\x89PNG\x0d\n\x1a\n\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\x00\x00\x00\x90PLTE\x00\x00\x00\x103D\x165F\x166F\x176F\x00.:\x165F\x188E\x177F\x1a<E\x0f1@\x143D\x155F\x166F\x165F\x165E\x165F\x154F\x166F\x165F\x163G\x85\xea-\x176G!GC\x81\xe5,3f?p\xc91/^@7k>Z\xa56~\xe0.C\x80;w\xd4/_\xae59o>n\xc52?x<s\xce0&OB,YA\x1eBEe\xb74z\xd9.\x83\xe8,H\x87:J\x8a:I\x88:N\x909xo\x8d\xe5\x00\x00\x00\x15tRNS\x00\x15\xcd\xf4\xe1\x07\x99\xfe\xf8\xfe\x10 w\xc4\xa9F\x8aS\xd7\xbd-\x8ak\xf8t\x00\x00\x01~IDATx\xda\x85S\xd9v\x820\x10\x1d%\x10\"\xee\x96 kd\x07\xc5\xb6\xff\xffw-I \x14=\xf6\xbeLr\xe6\xce>\x03\x06\xf6i\xbf&\xaeK\xd6\xfb\x93\x0d\xcfX9\x16\xb2\xb0\xfa|T!C\xd7Y-\xf5[\x0b\x93<\xf0%\x82<Dk\xfb\xc7\xcbf\x87I\xe4\xcf\x10%\xb8\xdb\x18\xbdG\xd8\xcd_ g\xc4\x9b\xec 7\xe6\xc6 ':\x11{MK\xff\x05J\xba\xb6U~\x98\x0e\xff\xbe\\\xbaI\xf1(\x03X\xc9\xf0\xab9\xc6\xa3\xa6\xa5q6\xc8\xc4\x1d\x82\x1c\xa9\xfcTX\xa4\x93i\x8cWiD\x9d_\x82%\xdf~\x8c\x99q^c+\xe5\xd5\xb5\xe1\x80\xaa\xc2\x06\xc5\xa4\xef\x056\xf2q\xc3\x038L\xf5\x8f\xa3\x94\x1a\x94K\x110\x07.\xb1zb\xe7\xcf\xd0PE\x8f/@\n\xd58L\xe6\x84\x02S%\xcf\xa0\xf2\x0d\x91\xd7}\xdbeA\xc3\x85\xe0M_s\x0ce\x96\x16\xb8#!\x0b8\xbf\x0b\xce\x83\xack\xfb\xa8\x1b :\x84\xf86!\x94\xc1\x97\xd0!v:I\xca\xe6\x046&\xb9\x03\x87\xf5\xbaL\xe1\x1b`7\x95y\xc2\x9b&\xdc\x8d^ \x9f\x1ae\xbbW\xdd\xc9\xda\x10\xee\xb3V\xc3~\x1cVl\x86U`5\xc8L\x0e\xcb\xa3\xa14*\xd8 \xf5\xe0x)k\x91\xe3\x86\xa3\xee\x9aAT\xf6\xb3\x85\x01\xfb\xfc\xcf\xca\x81g\xbd_Z\x80\x83\xc5\xf2\xa5>\xa7\xc4\x83 \x1f\xe4\xfd\xe1\x00\xac.\xf8\xf6\xf4\x860g\x1c\x8e\xf7\xf1|\xbc&\xce\xf6\xd5\xf9\xff\x00\xc6\x8cF{\xbe\xb8\x05g\x00\x00\x00\x00IEND\xaeB`\x82\x01\x00\x00\xff\xffPK\x07\x08\xa0\xd3\x9fC~\x02\x00\x00t\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x0b\x03~[\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x00index.cssUT\x05\x00\x01\xb6\x8e+il\x8d\xc1\n\x830\x0c\x86\xef}\x8a\xc0n\xb2\xc2\xce\xf5i\x12M5\xac6\x10;\xa7\x8e\xbd\xfb\xa8\xee2\x18\xb9\x04\xfe\xef\xe3\x1b\xcb\x94\xe0\xe5\x00\x00HW?\xcb.y\x08@j=\x9b']\xdbc\xd3\x85-&}\x06\xf0\x93\xee~\xeeLS\"\xb4\xd9/lE:L\xbf\x9c\xdf\x02\x9cP\xeb\xde\xce5W\xd7\x04\xe2\xa8\xc6\xf5\xc3X\xd8\xfeT%\x8flR\x0e\x85\xb4\xdf\xbe\xc8\x846H\x0ep;\x1b\x84\xdd}0}\xe4>\xc0%b\xbdj|\x02\x00\x00\xff\xffPK\x07\x08\xa3k\xae\x90\x8a\x00\x00\x00\xca\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x0b\x03~[\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00 \x00index.htmlUT\x05\x00\x01\xb6\x8e+i\xa4\x92O\x8b\xdb0\x10\xc5\xef\xf9\x14\x13\xdde\x91\x04B\x0f\xb2/\xfdC\x0b--49\xf4([\x13{\xba\x8al\xa4q\xd6\xc9\xa7_ly aY\x02\x9b\x93\x87\xf7\xfc~\x03o\xa4\x97R\xc2\xf7\xdd\xaf\x9fph\x03D6L\x15X\x8a\x1c\xa8\xec\x99Z\x0fe\xef\xadC({r\x16\xa4,\x16z\xf9\xe5\xf7\xe7\xdd\xbf?_\xa1\xe1\xa3+\x16z\xfc\x803\xbe\xce\x05zQ,\x00t\x83\xc6\x8e\x03\x80>\"\x1b\xa8\x1a\x13\"r.\xf6\xbbo\xf2\x93\x98-&vX\xfc}6u\x8d\x01\xf6?\xb4JJr\x1d\xf9'\x08\xe8r\x11\xf9\xec06\x88,\x80\xcf\x1d\xe6\x82q`U\xc5(\xa0 x\xc8E\xa6b\xa2\xc8\x9e\xb2IW\x1f\xa0\x90\xb78\xbc\x17Line truncated
fs.Register(data)
}
+152
View File
@@ -0,0 +1,152 @@
{
"schemes": [
"http",
"https"
],
"swagger": "2.0",
"info": {
"description": "API for Simple Bank application",
"title": "Simple Bank API",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"email": "[email protected]"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/v1",
"paths": {
"/v1/auth/register": {
"post": {
"description": "Register a new user account",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Create a new user",
"parameters": [
{
"description": "User registration details",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.createUserRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.JSONResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api.userResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/util.JSONResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/util.JSONResponse"
}
}
}
}
}
},
"definitions": {
"api.createUserRequest": {
"type": "object",
"required": [
"email",
"full_name",
"password",
"username"
],
"properties": {
"email": {
"type": "string"
},
"full_name": {
"type": "string"
},
"password": {
"type": "string",
"minLength": 6
},
"username": {
"type": "string"
}
}
},
"api.userResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"full_name": {
"type": "string"
},
"password_changed_at": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"util.JSONResponse": {
"type": "object",
"properties": {
"data": {},
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "Type \"Bearer\" followed by a space and JWT token.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}
+100
View File
@@ -0,0 +1,100 @@
basePath: /v1
definitions:
api.createUserRequest:
properties:
email:
type: string
full_name:
type: string
password:
minLength: 6
type: string
username:
type: string
required:
- email
- full_name
- password
- username
type: object
api.userResponse:
properties:
created_at:
type: string
email:
type: string
full_name:
type: string
password_changed_at:
type: string
username:
type: string
type: object
util.JSONResponse:
properties:
data: {}
error:
type: string
message:
type: string
success:
type: boolean
type: object
host: localhost:8080
info:
contact:
email: [email protected]
name: API Support
description: API for Simple Bank application
license:
name: MIT
url: https://opensource.org/licenses/MIT
termsOfService: http://swagger.io/terms/
title: Simple Bank API
version: "1.0"
paths:
/v1/auth/register:
post:
consumes:
- application/json
description: Register a new user account
parameters:
- description: User registration details
in: body
name: request
required: true
schema:
$ref: '#/definitions/api.createUserRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/util.JSONResponse'
- properties:
data:
$ref: '#/definitions/api.userResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/util.JSONResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/util.JSONResponse'
summary: Create a new user
tags:
- auth
schemes:
- http
- https
securityDefinitions:
BearerAuth:
description: Type "Bearer" followed by a space and JWT token.
in: header
name: Authorization
type: apiKey
swagger: "2.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

+16
View File
@@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
+19
View File
@@ -0,0 +1,19 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<!doctype html>
<html lang="en-US">
<body>
<script src="oauth2-redirect.js"></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
"use strict";function run(){var e,r,t,a=window.opener.swaggerUIRedirectOauth2,o=a.state,n=a.redirectUrl;if((t=(r=/code|token|error/.test(window.location.hash)?window.location.hash.substring(1).replace("?","&"):location.search.substring(1)).split("&")).forEach((function(e,r,t){t[r]='"'+e.replace("=",'":"')+'"'})),e=(r=r?JSON.parse("{"+t.join()+"}",(function(e,r){return""===e?r:decodeURIComponent(r)})):{}).state===o,"accessCode"!==a.auth.schema.get("flow")&&"authorizationCode"!==a.auth.schema.get("flow")&&"authorization_code"!==a.auth.schema.get("flow")||a.auth.code)a.callback({auth:a.auth,token:r,isValid:e,redirectUrl:n});else if(e||a.errCb({authId:a.auth.name,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),r.code)delete a.state,a.auth.code=r.code,a.callback({auth:a.auth,redirectUrl:n});else{let e;r.error&&(e="["+r.error+"]: "+(r.error_description?r.error_description+". ":"no accessCode received from the server. ")+(r.error_uri?"More info: "+r.error_uri:"")),a.errCb({authId:a.auth.name,source:"auth",level:"error",message:e||"[Authorization failed]: no accessCode received from the server"})}window.close()}"loading"!==document.readyState?run():document.addEventListener("DOMContentLoaded",(function(){run()}));
+312
View File
@@ -0,0 +1,312 @@
{
"swagger": "2.0",
"info": {
"title": "Simple Bank API",
"version": "0.0.1",
"contact": {
"name": "fvideira",
"url": "https://github.com/FernandoJVideira/simple_bank",
"email": "[email protected]"
}
},
"tags": [
{
"name": "SimpleBank"
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/create_user": {
"post": {
"summary": "Create User",
"description": "Create a new user in the Simple Bank system.",
"operationId": "SimpleBank_CreateUser",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/pbCreateUserResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/pbCreateUserRequest"
}
}
],
"tags": [
"SimpleBank"
]
}
},
"/v1/login_user": {
"post": {
"summary": "Login User",
"description": "Login an existing user in the Simple Bank system.",
"operationId": "SimpleBank_LoginUser",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/pbLoginUserResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/pbLoginUserRequest"
}
}
],
"tags": [
"SimpleBank"
]
}
},
"/v1/update_user": {
"patch": {
"summary": "Update User",
"description": "Update an existing user in the Simple Bank system.",
"operationId": "SimpleBank_UpdateUser",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/pbUpdateUserResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/pbUpdateUserRequest"
}
}
],
"tags": [
"SimpleBank"
]
}
},
"/v1/verify_email": {
"get": {
"summary": "Verify Email",
"description": "Verify user's email in the Simple Bank system.",
"operationId": "SimpleBank_VerifyEmail",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/pbVerifyEmailResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "id",
"in": "query",
"required": false,
"type": "string",
"format": "int64"
},
{
"name": "code",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"SimpleBank"
]
}
}
},
"definitions": {
"pbCreateUserRequest": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"fullName": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"pbCreateUserResponse": {
"type": "object",
"properties": {
"user": {
"$ref": "#/definitions/pbUser"
}
}
},
"pbLoginUserRequest": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"pbLoginUserResponse": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
},
"accessToken": {
"type": "string"
},
"accessTokenExpiresAt": {
"type": "string",
"format": "date-time"
},
"refreshToken": {
"type": "string"
},
"refreshTokenExpiresAt": {
"type": "string",
"format": "date-time"
},
"user": {
"$ref": "#/definitions/pbUser"
}
}
},
"pbUpdateUserRequest": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"fullName": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"pbUpdateUserResponse": {
"type": "object",
"properties": {
"user": {
"$ref": "#/definitions/pbUser"
}
}
},
"pbUser": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"fullName": {
"type": "string"
},
"email": {
"type": "string"
},
"passwordChangedAt": {
"type": "string",
"format": "date-time"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
}
},
"pbVerifyEmailResponse": {
"type": "object",
"properties": {
"isVerified": {
"type": "boolean"
}
}
},
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
window.onload = function () {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "simple_bank.swagger.json",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "StandaloneLayout",
});
//</editor-fold>
};
+2
View File
@@ -0,0 +1,2 @@
/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var s={251:(s,o)=>{o.read=function(s,o,i,a,u){var _,w,x=8*u-a-1,C=(1<<x)-1,j=C>>1,L=-7,B=i?u-1:0,$=i?-1:1,U=s[o+B];for(B+=$,_=U&(1<<-L)-1,U>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=$,L-=8);if(0===_)_=1-j;else{if(_===C)return w?NaN:1/0*(U?-1:1);w+=Math.pow(2,a),_-=j}return(U?-1:1)*w*Math.pow(2,_-a)},o.write=function(s,o,i,a,u,_){var w,x,C,j=8*_-u-1,L=(1<<j)-1,B=L>>1,$=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,U=a?0:_-1,V=a?1:-1,z=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(x=isNaN(o)?1:0,w=L):(w=Math.floor(Math.log(o)/Math.LN2),o*(C=Math.pow(2,-w))<1&&(w--,C*=2),(o+=w+B>=1?$/C:$*Math.pow(2,1-B))*C>=2&&(w++,C/=2),w+B>=L?(x=0,w=L):w+B>=1?(x=(o*C-1)*Math.pow(2,u),w+=B):(x=o*Math.pow(2,B-1)*Math.pow(2,u),w=0));u>=8;s[i+U]=255&x,U+=V,x/=256,u-=8);for(w=w<<u|x,j+=u;j>0;s[i+U]=255&w,U+=V,w/=256,j-=8);s[i+U-V]|=128*z}},462:(s,o,i)=>{"use strict";var a=i(40975);s.exports=a},659:(s,o,i)=>{var a=i(51873),u=Object.prototype,_=u.hasOwnProperty,w=u.toString,x=a?a.toStringTag:void 0;s.exports=function getRawTag(s){var o=_.call(s,x),i=s[x];try{s[x]=void 0;var a=!0}catch(s){}var u=w.call(s);return a&&(o?s[x]=i:delete s[x]),u}},694:(s,o,i)=>{"use strict";i(91599);var a=i(37257);i(12560),s.exports=a},953:(s,o,i)=>{"use strict";s.exports=i(53375)},1733:s=>{var o=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;s.exports=function asciiWords(s){return s.match(o)||[]}},1882:(s,o,i)=>{var a=i(72552),u=i(23805);s.exports=function isFunction(s){if(!u(s))return!1;var o=a(s);return"[object Function]"==o||"[object GeneratorFunction]"==o||"[object AsyncFunction]"==o||"[object Proxy]"==o}},1907:(s,o,i)=>{"use strict";var a=i(41505),u=Function.prototype,_=u.call,w=a&&u.bind.bind(_,_);s.exports=a?w:function(s){return function(){return _.apply(s,arguments)}}},2205:function(s,o,i){var a;a=void 0!==i.g?i.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var o,i=String(s),a=i.length,u=-1,_="",w=i.charCodeAt(0);++u<a;)0!=(o=i.charCodeAt(u))?_+=o>=1&&o<=31||127==o||0==u&&o>=48&&o<=57||1==u&&o>=48&&o<=57&&45==w?"\\"+o.toString(16)+" ":0==u&&1==a&&45==o||!(o>=128||45==o||95==o||o>=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122)?"\\"+i.charAt(u):i.charAt(u):_+="�";return _};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(a)},2209:(s,o,i)=>{"use strict";var a,u=i(9404),_=function productionTypeChecker(){invariant(!1,"ImmutablePropTypes type checking code is stripped in production.")};_.isRequired=_;var w=function getProductionTypeChecker(){return _};function getPropType(s){var o=typeof s;return Array.isArray(s)?"array":s instanceof RegExp?"object":s instanceof u.Iterable?"Immutable."+s.toSource().split(" ")[0]:o}function createChainableTypeChecker(s){function checkType(o,i,a,u,_,w){for(var x=arguments.length,C=Array(x>6?x-6:0),j=6;j<x;j++)C[j-6]=arguments[j];return w=w||a,u=u||"<<anonymous>>",null!=i[a]?s.apply(void 0,[i,a,u,_,w].concat(C)):o?new Error("Required "+_+" `"+w+"` was not specified in `"+u+"`."):void 0}var o=checkType.bind(null,!1);return o.isRequired=checkType.bind(null,!0),o}function createIterableSubclassTypeChecker(s,o){return function createImmutableTypeChecker(s,o){return createChainableTypeChecker((function validate(i,a,u,_,w){var x=i[a];if(!o(x)){var C=getPropType(x);return new Error("Invalid "+_+" `"+w+"` of type `"+C+"` supplied to `"+u+"`, expected `"+s+"`.")}return null}))}("Iterable."+s,(function(s){return u.Iterable.isIterable(s)&&o(s)}))}(a={listOf:w,mapOf:w,orderedMapOf:w,setOf:w,orderedSetOf:w,stackOf:w,iterableOf:w,recordOf:w,shape:w,contains:w,mapContains:w,orderedMapContains:w,list:_,map:_,orderedMap:_,set:_,orderedSet:_,stack:_,seq:_,record:_,iterable:_}).iterable.indexed=createIterableSubclassTypeChecker("Indexed",u.Iterable.isIndexed),a.iterable.keyed=createIterableSubclassTypeChecker("Keyed",u.Iterable.isKeyed),s.exports=a},2404:(s,o,i)=>{var a=i(60270);s.exports=function isEqual(s,o){return a(s,o)}},2523:s=>{s.exports=function baseFindIndex(s,o,i,a){for(var u=s.length,_=i+(a?1:-1);a?_--:++_<u;)if(o(s[_],_,s))return _;return-1}},2532:(s,o,i)=>{"use strict";var a=i(45951),u=Object.defineProperty;s.exports=function(s,o){try{u(a,s,{value:o,configurable:!0,writable:!0})}catch(i){a[s]=o}return o}},2694:(s,o,i)=>{"use strict";var a=i(6925);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,s.exports=function(){function shim(s,o,i,u,_,w){if(w!==a){var x=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at httpLine truncated
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui-bundle.js","mappings":"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 truncated
@@ -0,0 +1,3 @@
/*! For license information please see swagger-ui-es-bundle-core.js.LICENSE.txt */
import*as e from"ieee754";import*as t from"to-buffer";import*as r from"base64-js";import*as n from"react";import*as a from"redux";import*as o from"immutable";import*as s from"redux-immutable";import*as l from"serialize-error";import*as i from"lodash/merge";import*as c from"lodash/camelCase";import*as u from"lodash/upperFirst";import*as d from"lodash/memoize";import*as p from"lodash/find";import*as m from"lodash/some";import*as f from"lodash/eq";import*as h from"lodash/isFunction";import*as g from"css.escape";import*as y from"url-parse";import*as S from"reselect";import*as _ from"prop-types";import*as v from"lodash/omit";import*as b from"js-yaml";import*as w from"zenscroll";import*as C from"react-immutable-proptypes";import*as x from"lodash/reduce";import*as O from"lodash/get";import*as k from"@babel/runtime-corejs3/helpers/extends";import*as N from"classnames";import*as A from"react-copy-to-clipboard";import*as I from"react-immutable-pure-component";import*as j from"react-debounce-input";import*as R from"lodash/isPlainObject";import*as T from"randexp";import*as B from"lodash/isEmpty";import*as P from"lodash/constant";import*as M from"lodash/isString";import*as q from"lodash/debounce";import*as L from"lodash/set";import*as U from"lodash/fp/assocPath";import*as D from"swagger-client/es/resolver/strategies/generic";import*as $ from"swagger-client/es/resolver/strategies/openapi-2";import*as V from"swagger-client/es/resolver/strategies/openapi-3-0";import*as J from"swagger-client/es/resolver/strategies/openapi-3-1-apidom";import*as F from"swagger-client/es/resolver";import*as z from"swagger-client/es/execute";import*as K from"swagger-client/es/http";import*as W from"swagger-client/es/subtree-resolver";import*as H from"swagger-client/es/helpers";import*as G from"react-dom";import*as X from"react-redux";import*as Y from"lodash/identity";import*as Q from"react-syntax-highlighter/dist/esm/light";import*as Z from"react-syntax-highlighter/dist/esm/languages/hljs/javascript";import*as ee from"react-syntax-highlighter/dist/esm/languages/hljs/json";import*as te from"react-syntax-highlighter/dist/esm/languages/hljs/xml";import*as re from"react-syntax-highlighter/dist/esm/languages/hljs/bash";import*as ne from"react-syntax-highlighter/dist/esm/languages/hljs/yaml";import*as ae from"react-syntax-highlighter/dist/esm/languages/hljs/http";import*as oe from"react-syntax-highlighter/dist/esm/languages/hljs/powershell";import*as se from"react-syntax-highlighter/dist/esm/styles/hljs/agate";import*as le from"react-syntax-highlighter/dist/esm/styles/hljs/arta";import*as ie from"react-syntax-highlighter/dist/esm/styles/hljs/monokai";import*as ce from"react-syntax-highlighter/dist/esm/styles/hljs/nord";import*as ue from"react-syntax-highlighter/dist/esm/styles/hljs/obsidian";import*as de from"react-syntax-highlighter/dist/esm/styles/hljs/tomorrow-night";import*as pe from"react-syntax-highlighter/dist/esm/styles/hljs/idea";import*as me from"js-file-download";import*as fe from"lodash/zipObject";import*as he from"lodash/toString";import*as ge from"xml-but-prettier";import*as ye from"lodash/toLower";import*as Ee from"remarkable";import*as Se from"remarkable/linkify";import*as _e from"dompurify";import*as ve from"lodash/escapeRegExp";import*as be from"lodash/has";import*as we from"lodash/fp/set";var Ce={7:function(e){var t,r="object"==typeof Reflect?Reflect:null,n=r&&"function"==typeof r.apply?r.apply:function ReflectApply(e,t,r){return Function.prototype.apply.call(e,t,r)};t=r&&"function"==typeof r.ownKeys?r.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function ReflectOwnKeys(e){return Object.getOwnPropertyNames(e)};var a=Number.isNaN||function NumberIsNaN(e){return e!=e};function EventEmitter(){EventEmitter.init.call(this)}e.exports=EventEmitter,e.exports.once=function once(e,t){return new Promise((function(r,n){function errorListener(r){e.removeListener(t,resolver),n(r)}function resolver(){"function"==typeof e.removeListener&&e.removeListener("error",errorListener),r([].slice.call(arguments))}eventTargetAgnosticAddListener(e,t,resolver,{once:!0}),"error"!==t&&function addErrorHandlerIfEventEmitter(e,t,r){"function"==typeof e.on&&eventTargetAgnosticAddListener(e,"error",t,r)}(e,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var o=10;function checkListener(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function _getMaxListeners(e){return void 0===e._maxListeners?EventEmitter.defaultMaxListeners:e._maxListeners}function _addListener(e,t,r,n){var a,o,s;if(checkListener(r),void 0===(o=e._events)?(o=e._events=Object.create(null),e._eventsCount=0):(void 0!==o.newListener&&(e.emit("newListener",t,r.listener?r.listener:r),o=e._events)Line truncated
//# sourceMappingURL=swagger-ui-es-bundle-core.js.map
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui-es-bundle-core.js","mappings":"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 truncated
+2
View File
@@ -0,0 +1,2 @@
/*! For license information please see swagger-ui-es-bundle.js.LICENSE.txt */
(()=>{var s={251:(s,o)=>{o.read=function(s,o,i,a,u){var _,w,x=8*u-a-1,C=(1<<x)-1,j=C>>1,L=-7,B=i?u-1:0,$=i?-1:1,U=s[o+B];for(B+=$,_=U&(1<<-L)-1,U>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=$,L-=8);if(0===_)_=1-j;else{if(_===C)return w?NaN:1/0*(U?-1:1);w+=Math.pow(2,a),_-=j}return(U?-1:1)*w*Math.pow(2,_-a)},o.write=function(s,o,i,a,u,_){var w,x,C,j=8*_-u-1,L=(1<<j)-1,B=L>>1,$=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,U=a?0:_-1,V=a?1:-1,z=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(x=isNaN(o)?1:0,w=L):(w=Math.floor(Math.log(o)/Math.LN2),o*(C=Math.pow(2,-w))<1&&(w--,C*=2),(o+=w+B>=1?$/C:$*Math.pow(2,1-B))*C>=2&&(w++,C/=2),w+B>=L?(x=0,w=L):w+B>=1?(x=(o*C-1)*Math.pow(2,u),w+=B):(x=o*Math.pow(2,B-1)*Math.pow(2,u),w=0));u>=8;s[i+U]=255&x,U+=V,x/=256,u-=8);for(w=w<<u|x,j+=u;j>0;s[i+U]=255&w,U+=V,w/=256,j-=8);s[i+U-V]|=128*z}},462:(s,o,i)=>{"use strict";var a=i(40975);s.exports=a},659:(s,o,i)=>{var a=i(51873),u=Object.prototype,_=u.hasOwnProperty,w=u.toString,x=a?a.toStringTag:void 0;s.exports=function getRawTag(s){var o=_.call(s,x),i=s[x];try{s[x]=void 0;var a=!0}catch(s){}var u=w.call(s);return a&&(o?s[x]=i:delete s[x]),u}},694:(s,o,i)=>{"use strict";i(91599);var a=i(37257);i(12560),s.exports=a},953:(s,o,i)=>{"use strict";s.exports=i(53375)},1733:s=>{var o=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;s.exports=function asciiWords(s){return s.match(o)||[]}},1882:(s,o,i)=>{var a=i(72552),u=i(23805);s.exports=function isFunction(s){if(!u(s))return!1;var o=a(s);return"[object Function]"==o||"[object GeneratorFunction]"==o||"[object AsyncFunction]"==o||"[object Proxy]"==o}},1907:(s,o,i)=>{"use strict";var a=i(41505),u=Function.prototype,_=u.call,w=a&&u.bind.bind(_,_);s.exports=a?w:function(s){return function(){return _.apply(s,arguments)}}},2205:function(s,o,i){var a;a=void 0!==i.g?i.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var o,i=String(s),a=i.length,u=-1,_="",w=i.charCodeAt(0);++u<a;)0!=(o=i.charCodeAt(u))?_+=o>=1&&o<=31||127==o||0==u&&o>=48&&o<=57||1==u&&o>=48&&o<=57&&45==w?"\\"+o.toString(16)+" ":0==u&&1==a&&45==o||!(o>=128||45==o||95==o||o>=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122)?"\\"+i.charAt(u):i.charAt(u):_+="�";return _};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(a)},2209:(s,o,i)=>{"use strict";var a,u=i(9404),_=function productionTypeChecker(){invariant(!1,"ImmutablePropTypes type checking code is stripped in production.")};_.isRequired=_;var w=function getProductionTypeChecker(){return _};function getPropType(s){var o=typeof s;return Array.isArray(s)?"array":s instanceof RegExp?"object":s instanceof u.Iterable?"Immutable."+s.toSource().split(" ")[0]:o}function createChainableTypeChecker(s){function checkType(o,i,a,u,_,w){for(var x=arguments.length,C=Array(x>6?x-6:0),j=6;j<x;j++)C[j-6]=arguments[j];return w=w||a,u=u||"<<anonymous>>",null!=i[a]?s.apply(void 0,[i,a,u,_,w].concat(C)):o?new Error("Required "+_+" `"+w+"` was not specified in `"+u+"`."):void 0}var o=checkType.bind(null,!1);return o.isRequired=checkType.bind(null,!0),o}function createIterableSubclassTypeChecker(s,o){return function createImmutableTypeChecker(s,o){return createChainableTypeChecker((function validate(i,a,u,_,w){var x=i[a];if(!o(x)){var C=getPropType(x);return new Error("Invalid "+_+" `"+w+"` of type `"+C+"` supplied to `"+u+"`, expected `"+s+"`.")}return null}))}("Iterable."+s,(function(s){return u.Iterable.isIterable(s)&&o(s)}))}(a={listOf:w,mapOf:w,orderedMapOf:w,setOf:w,orderedSetOf:w,stackOf:w,iterableOf:w,recordOf:w,shape:w,contains:w,mapContains:w,orderedMapContains:w,list:_,map:_,orderedMap:_,set:_,orderedSet:_,stack:_,seq:_,record:_,iterable:_}).iterable.indexed=createIterableSubclassTypeChecker("Indexed",u.Iterable.isIndexed),a.iterable.keyed=createIterableSubclassTypeChecker("Keyed",u.Iterable.isKeyed),s.exports=a},2404:(s,o,i)=>{var a=i(60270);s.exports=function isEqual(s,o){return a(s,o)}},2523:s=>{s.exports=function baseFindIndex(s,o,i,a){for(var u=s.length,_=i+(a?1:-1);a?_--:++_<u;)if(o(s[_],_,s))return _;return-1}},2532:(s,o,i)=>{"use strict";var a=i(45951),u=Object.defineProperty;s.exports=function(s,o){try{u(a,s,{value:o,configurable:!0,writable:!0})}catch(i){a[s]=o}return o}},2694:(s,o,i)=>{"use strict";var a=i(6925);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,s.exports=function(){function shim(s,o,i,u,_,w){if(w!==a){var x=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw x.name="Invariant Violation",x}}function getShim(){return shim}shim.isRequired=shim;var s={array:shim,bigint:shim,bool:shim,func:shim,number:shim,object:shim,string:shim,symbol:shim,any:shim,arrayOf:getShim,element:sLine truncated
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui-es-bundle.js","mappings":"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 truncated
@@ -0,0 +1,2 @@
/*! For license information please see swagger-ui-standalone-preset.js.LICENSE.txt */
!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SwaggerUIStandalonePreset=e():t.SwaggerUIStandalonePreset=e()}(this,(()=>(()=>{var t={2:(t,e,r)=>{var n=r(2199),o=r(4664),i=r(5950);t.exports=function getAllKeys(t){return n(t,i,o)}},41:(t,e,r)=>{"use strict";var n=r(655),o=r(8068),i=r(9675),a=r(5795);t.exports=function defineDataProperty(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`obj` must be an object or a function`");if("string"!=typeof e&&"symbol"!=typeof e)throw new i("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new i("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new i("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new i("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new i("`loose`, if provided, must be a boolean");var s=arguments.length>3?arguments[3]:null,u=arguments.length>4?arguments[4]:null,c=arguments.length>5?arguments[5]:null,f=arguments.length>6&&arguments[6],l=!!a&&a(t,e);if(n)n(t,e,{configurable:null===c&&l?l.configurable:!c,enumerable:null===s&&l?l.enumerable:!s,value:r,writable:null===u&&l?l.writable:!u});else{if(!f&&(s||u||c))throw new o("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");t[e]=r}}},76:t=>{"use strict";t.exports=Function.prototype.call},79:(t,e,r)=>{var n=r(3702),o=r(80),i=r(4739),a=r(8655),s=r(1175);function ListCache(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}ListCache.prototype.clear=n,ListCache.prototype.delete=o,ListCache.prototype.get=i,ListCache.prototype.has=a,ListCache.prototype.set=s,t.exports=ListCache},80:(t,e,r)=>{var n=r(6025),o=Array.prototype.splice;t.exports=function listCacheDelete(t){var e=this.__data__,r=n(e,t);return!(r<0)&&(r==e.length-1?e.pop():o.call(e,r,1),--this.size,!0)}},104:(t,e,r)=>{var n=r(3661);function memoize(t,e){if("function"!=typeof t||null!=e&&"function"!=typeof e)throw new TypeError("Expected a function");var memoized=function(){var r=arguments,n=e?e.apply(this,r):r[0],o=memoized.cache;if(o.has(n))return o.get(n);var i=t.apply(this,r);return memoized.cache=o.set(n,i)||o,i};return memoized.cache=new(memoize.Cache||n),memoized}memoize.Cache=n,t.exports=memoize},251:(t,e)=>{e.read=function(t,e,r,n,o){var i,a,s=8*o-n-1,u=(1<<s)-1,c=u>>1,f=-7,l=r?o-1:0,p=r?-1:1,h=t[e+l];for(l+=p,i=h&(1<<-f)-1,h>>=-f,f+=s;f>0;i=256*i+t[e+l],l+=p,f-=8);for(a=i&(1<<-f)-1,i>>=-f,f+=n;f>0;a=256*a+t[e+l],l+=p,f-=8);if(0===i)i=1-c;else{if(i===u)return a?NaN:1/0*(h?-1:1);a+=Math.pow(2,n),i-=c}return(h?-1:1)*a*Math.pow(2,i-n)},e.write=function(t,e,r,n,o,i){var a,s,u,c=8*i-o-1,f=(1<<c)-1,l=f>>1,p=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,h=n?0:i-1,d=n?1:-1,y=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(s=isNaN(e)?1:0,a=f):(a=Math.floor(Math.log(e)/Math.LN2),e*(u=Math.pow(2,-a))<1&&(a--,u*=2),(e+=a+l>=1?p/u:p*Math.pow(2,1-l))*u>=2&&(a++,u/=2),a+l>=f?(s=0,a=f):a+l>=1?(s=(e*u-1)*Math.pow(2,o),a+=l):(s=e*Math.pow(2,l-1)*Math.pow(2,o),a=0));o>=8;t[r+h]=255&s,h+=d,s/=256,o-=8);for(a=a<<o|s,c+=o;c>0;t[r+h]=255&a,h+=d,a/=256,c-=8);t[r+h-d]|=128*y}},270:(t,e,r)=>{var n=r(7068),o=r(346);t.exports=function baseIsEqual(t,e,r,i,a){return t===e||(null==t||null==e||!o(t)&&!o(e)?t!=t&&e!=e:n(t,e,r,i,baseIsEqual,a))}},289:(t,e,r)=>{var n=r(2651);t.exports=function mapCacheGet(t){return n(this,t).get(t)}},294:t=>{t.exports=function isLength(t){return"number"==typeof t&&t>-1&&t%1==0&&t<=9007199254740991}},317:t=>{t.exports=function mapToArray(t){var e=-1,r=Array(t.size);return t.forEach((function(t,n){r[++e]=[n,t]})),r}},346:t=>{t.exports=function isObjectLike(t){return null!=t&&"object"==typeof t}},361:t=>{var e=/^(?:0|[1-9]\d*)$/;t.exports=function isIndex(t,r){var n=typeof t;return!!(r=null==r?9007199254740991:r)&&("number"==n||"symbol"!=n&&e.test(t))&&t>-1&&t%1==0&&t<r}},376:t=>{"use strict";t.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},392:t=>{t.exports=function getValue(t,e){return null==t?void 0:t[e]}},414:t=>{"use strict";t.exports=Math.round},453:(t,e,r)=>{"use strict";var n,o=r(9612),i=r(9383),a=r(1237),s=r(9290),u=r(9538),c=r(8068),f=r(9675),l=r(5345),p=r(1514),h=r(8968),d=r(6188),y=r(8002),_=r(5880),g=r(414),m=r(3093),v=Function,getEvalledConstructor=function(t){try{return v('"use strict"; return ('+t+").constructor;")()}catch(t){}},b=r(5795),w=r(655),throwTypeError=function(){throw new f},I=b?function(){try{return throwTypeError}catch(t){try{return b(arguments,"callee").get}cLine truncated
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui-standalone-preset.js","mappings":"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 truncated
+3
View File
@@ -0,0 +1,3 @@
.swagger-ui{color:#3b4151;font-family:sans-serif}.swagger-ui html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.swagger-ui body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui footer,.swagger-ui header,.swagger-ui nav,.swagger-ui section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;overflow:visible}.swagger-ui pre{font-family:monospace,monospace;font-size:1em}.swagger-ui a{background-color:transparent;-webkit-text-decoration-skip:objects}.swagger-ui abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.swagger-ui b,.swagger-ui strong{font-weight:inherit;font-weight:bolder}.swagger-ui code,.swagger-ui kbd,.swagger-ui samp{font-family:monospace,monospace;font-size:1em}.swagger-ui dfn{font-style:italic}.swagger-ui mark{background-color:#ff0;color:#000}.swagger-ui small{font-size:80%}.swagger-ui sub,.swagger-ui sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.swagger-ui sub{bottom:-.25em}.swagger-ui sup{top:-.5em}.swagger-ui audio,.swagger-ui video{display:inline-block}.swagger-ui audio:not([controls]){display:none;height:0}.swagger-ui img{border-style:none}.swagger-ui svg:not(:root){overflow:hidden}.swagger-ui button,.swagger-ui input,.swagger-ui optgroup,.swagger-ui select,.swagger-ui textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}.swagger-ui button,.swagger-ui input{overflow:visible}.swagger-ui button,.swagger-ui select{text-transform:none}.swagger-ui [type=reset],.swagger-ui [type=submit],.swagger-ui button,.swagger-ui html [type=button]{-webkit-appearance:button}.swagger-ui [type=button]::-moz-focus-inner,.swagger-ui [type=reset]::-moz-focus-inner,.swagger-ui [type=submit]::-moz-focus-inner,.swagger-ui button::-moz-focus-inner{border-style:none;padding:0}.swagger-ui [type=button]:-moz-focusring,.swagger-ui [type=reset]:-moz-focusring,.swagger-ui [type=submit]:-moz-focusring,.swagger-ui button:-moz-focusring{outline:1px dotted ButtonText}.swagger-ui fieldset{padding:.35em .75em .625em}.swagger-ui legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}.swagger-ui progress{display:inline-block;vertical-align:baseline}.swagger-ui textarea{overflow:auto}.swagger-ui [type=checkbox],.swagger-ui [type=radio]{box-sizing:border-box;padding:0}.swagger-ui [type=number]::-webkit-inner-spin-button,.swagger-ui [type=number]::-webkit-outer-spin-button{height:auto}.swagger-ui [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.swagger-ui [type=search]::-webkit-search-cancel-button,.swagger-ui [type=search]::-webkit-search-decoration{-webkit-appearance:none}.swagger-ui ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.swagger-ui details,.swagger-ui menu{display:block}.swagger-ui summary{display:list-item}.swagger-ui canvas{display:inline-block}.swagger-ui [hidden],.swagger-ui template{display:none}.swagger-ui .debug *{outline:1px solid gold}.swagger-ui .debug-white *{outline:1px solid #fff}.swagger-ui .debug-black *{outline:1px solid #000}.swagger-ui .debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) repeat 0 0}.swagger-ui .debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDbLine truncated
/*# sourceMappingURL=swagger-ui.css.map*/
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui.css","mappings":"AAOA,YCHI,aCcU,CDfZ,sBCeY,kCFCZ,0BACA,8BACA,kBAUF,QACE,iHAOF,aAME,gBAQF,aACE,eACA,4DAWF,aAGE,oBAOF,eACE,gBAQF,sBACE,SACA,iBACA,iBAQF,+BACE,cACA,eAWF,4BACE,qCACA,yBAQF,kBACE,CACA,yBACA,CADA,wCACA,CADA,gCACA,kCAOF,mBAEE,CAOF,kBAPE,CASA,kDAQF,+BAGE,cACA,iBAOF,iBACE,kBAOF,qBACE,WACA,mBAOF,aACE,iCAQF,aAEE,cACA,kBACA,wBACA,iBAGF,aACE,iBAGF,SACE,qCAUF,oBAEE,mCAOF,YACE,SACA,iBAOF,iBACE,4BAOF,eACE,mGAWF,sBAKE,eACA,iBACA,SACA,sCAQF,gBAEE,uCAQF,mBAEE,sGASF,yBAIE,yKAOF,iBAIE,UACA,6JAOF,6BAIE,sBAOF,0BACE,oBAUF,qBACE,cACA,cACA,eACA,UACA,mBACA,sBAQF,oBACE,wBACA,sBAOF,aACE,sDAQF,qBAEE,UACA,2GAOF,WAEE,2BAQF,4BACE,oBACA,8GAOF,uBAEE,0CAQF,yBACE,aACA,sCAWF,aAEE,qBAOF,iBACE,oBAUF,oBACE,CAQA,0CAUF,YACE,sBGnbF,kDACA,kDACA,+CCDA,yEACE,4BAGF,yEACE,iCAGF,kEACE,kCAGF,kEACE,2tBClBF,qBAkCE,2BCrBF,QACE,kBACA,iCAGF,sDACA,sDAEA,kDACA,sDAEA,oDACA,mDAEA,oDACA,mDAEA,qDACA,mDAEA,sDAKI,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,oCAGJ,6BACI,QACE,kBACA,oCAEF,yDACA,yDACA,qDACA,yDACA,uDACA,sDACA,uDACA,sDACA,wDACA,sDACA,yDAII,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,yDAIR,4BACI,QACE,kBACA,mCAEF,wDACA,wDACA,oDACA,wDACA,sDACA,qDACA,sDACA,qDACA,uDACA,qDACA,wDAII,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,qCAIR,4BACI,QACE,kBACA,mCAEF,wDACA,wDACA,oDACA,wDACA,sDACA,qDACA,sDACA,qDACA,uDACA,qDACA,wDAII,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,kBC3HR,kCCQE,qDACA,qEAEF,sBACE,wDACA,0FAGF,qBACE,uDACA,sEAGF,qBACE,uDACA,0DCVA,uBACA,CAFF,2BAEE,qBAIA,uBACA,CAFF,2BAEE,uBAIA,wBACA,CAFF,2BAEE,wBAIA,0BACA,CAFF,2BAEE,sBAIA,qBACA,CAFF,2BAEE,oCAGF,0BAEI,uBACA,CAFF,2BAEE,wBAIA,uBACA,CAFF,2BAEE,0BAIA,wBACA,CAFF,2BAEE,2BAIA,0BACA,CAFF,2BAEE,yBAIA,qBACA,CAFF,2BAEE,yDAIJ,yBAEI,uBACA,CAFF,2BAEE,uBAIA,uBACA,CAFF,2BAEE,yBAIA,wBACA,CAFF,2BAEE,0BAIA,0BACA,CAFF,2BAEE,wBAIA,qBACA,CAFF,2BAEE,qCAIJ,yBAEI,uBACA,CAFF,2BAEE,uBAIA,uBACA,CAFF,2BAEE,yBAIA,wBACA,CAFF,2BAEE,0BAIA,0BACA,CAFF,2BAEE,wBAIA,qBACA,CAFF,2BAEE,uBChHJ,mDACA,qDACA,6CAEA,wBACE,sDACA,wDACA,kEAGF,uBACE,qDACA,uDACA,8CAGF,uBACE,qDACA,uDACA,2BCPA,oDACA,4DACA,gEACA,kEACA,8DACA,oEAGF,mBACE,uDACA,+DACA,mEACA,qEACA,iEACA,yFAGF,kBACE,sDACA,8DACA,kEACA,oEACA,gEACA,qEAGF,kBACE,sDACA,8DACA,kEACA,oEACA,gEACA,wDCnCF,iBC2CQ,4BD1CR,iBC2Ca,2BD1Cb,iBC2CY,0BD1CZ,iBC2CW,sBD1CX,iBC2CO,wBD1CP,iBC2CS,8BD1CT,iBC2Ce,2BD1Cf,iBC2CY,4BD1CZ,iBC2Ca,4BD1Cb,oBC2Ca,uBD1Cb,iBC2CQ,0BDzCR,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,gCCuDW,2BDtDX,iCCuDY,4BDtDZ,iCCuDa,0BDrDb,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,4BC8BW,2BD7BX,6BC8BY,4BD7BZ,6BC8Ba,0BD5Bb,oBCyCW,qBDxCX,oBCyCM,2BDxCN,oBCyCY,wBDxCZ,oBCyCS,sBDxCT,oBCyCO,wBDxCP,iBCyCS,8BDxCT,oBCyCe,wBDxCf,oBCyCS,8BDxCT,oBCyCe,2BDxCf,oBCyCY,0BDxCZ,oBCyCW,sBDxCX,oBCyCO,4BDxCP,oBCyCa,4BDxCb,oBCyCa,uBDxCb,oBCyCQ,6BDxCR,oBCyCc,sBDxCd,oBCyCO,2BDxCP,oBCyCY,sBDxCZ,oBCyCO,4BDxCP,oBCyCa,+BDxCb,oBCyCgB,6BDxChB,oBCyCc,8BDxCd,oBCyCe,+BDxCf,oBCyCgB,4BDxChB,oBCyCa,6BDvCb,wBCVc,yBDWd,sCE3DE,eDmBmB,kBClBnB,qBDmBgB,kBClBhB,oBDmBgB,kBClBhB,mBDmBgB,kBClBhB,kBDmBgB,qBClBhB,kBDmBqB,sBClBrB,oBDmBmB,yBClBnB,wBACI,0BACA,sBAEJ,2BACI,6BACA,wBAGA,2BACA,CAFJ,wBAEI,uBAGA,4BACA,CAFJ,yBAEI,oCAGN,oBACE,eDNmB,qBCOnB,qBDNgB,qBCOhB,oBDNgB,qBCOhB,mBDNgB,qBCOhB,kBDNgB,wBCOhB,kBDNqB,yBCOrB,oBDNmB,4BCOnB,wBACI,0BACA,yBAEJ,2BACI,6BACA,2BAGA,2BACA,CAFJ,wBAEI,0BAGA,4BACA,CAFJ,yBAEI,yDAIN,mBACE,eDhCmB,oBCiCnB,qBDhCgB,oBCiChB,oBDhCgB,oBCiChB,mBDhCgB,oBCiChB,kBDhCgB,uBCiChB,kBDhCqB,wBCiCrB,oBDhCmB,2BCiCnB,wBACI,0BACA,wBAEJ,2BACI,6BACA,0BAGA,2BACA,CAFJ,wBAEI,yBAGA,4BACA,CAFJ,yBAEI,qCAIN,mBACE,eD1DmB,oBC2DnB,qBD1DgB,oBC2DhB,oBD1DgB,oBC2DhB,mBD1DgB,oBC2DhB,kBD1DgB,uBC2DhB,kBD1DqB,wBC2DrB,oBD1DmB,2BC2DnB,wBACI,0BACA,wBAEJ,2BACI,6BACA,0BAGA,2BACA,CAFJ,wBAEI,yBAGA,4BACA,CAFJ,yBAEI,yBCrGN,2CACA,0CACA,wCACA,qDAEA,0BACE,8CACA,6CACA,2CACA,0EAGF,yBACE,6CACA,4CACA,0CACA,sDAGF,yBACE,6CACA,4CACA,0CACA,oCCvBF,cH6BoB,kBG5BpB,oBH6BiB,kBG5BjB,mBH6BiB,kBG5BjB,kBH6BiB,kBG5BjB,iBH6BiB,kBG5BjB,iBH6BiB,mBG1BjB,kBHqBoB,mBGpBpB,oBHoBoB,mBGnBpB,qBHmBoB,mBGlBpB,mBHkBoB,oCGhBpB,oBACE,cHekB,qBGdlB,oBHee,qBGdf,mBHee,qBGdf,kBHee,qBGdf,iBHee,qBGdf,iBHee,sBGdf,kBHSkB,sBGRlB,oBHQkB,sBGPlB,qBHOkB,sBGNlB,mBHMkB,yDGHpB,mBACE,cHEkB,oBGDlB,oBHEe,oBGDf,mBHEe,oBGDf,kBHEe,oBGDf,iBHEe,oBGDf,iBHEe,qBGDf,kBHJkB,qBGKlB,oBHLkB,qBGMlB,qBHNkB,qBGOlB,mBHPkB,qCGUpB,mBACE,cHXkB,oBGYlB,oBHXe,oBGYf,mBHXe,oBGYf,kBHXe,oBGYf,iBHXe,oBGYf,iBHXe,qBGYf,kBHjBkB,qBGkBlB,oBHlBkB,qBGmBlB,qBHnBkB,qBGoBlB,mBHpBkB,wBIxCpB,qCJ8Ce,uBI7Cf,qCJ8Ce,uBI7Cf,yCJ8Ce,uBI7Cf,uCJ8Ce,uBI7Cf,uCJ8Ce,oCI5Cf,yBACE,qCJuCa,0BItCb,qCJuCa,0BItCb,yCJuCa,0BItCb,uCJuCa,0BItCb,uCJuCa,yDIpCf,wBACE,qCJ+Ba,yBI9Bb,qCJ+Ba,yBI9Bb,yCJ+Ba,yBI9Bb,uCJ+Ba,yBI9Bb,uCJ+Ba,qCI5Bf,wBACE,qCJuBa,yBItBb,qCJuBa,yBItBb,yCJuBa,yBItBb,uCJuBa,yBItBb,uCJuBa,mBKxDf,eACE,kBACA,gBACA,oBCkBF,2BACA,8BACA,6BACA,0BAEA,8BACA,iCACA,gCALine truncated
+2
View File
@@ -0,0 +1,2 @@
!function webpackUniversalModuleDefinition(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUICore=t():e.SwaggerUICore=t()}(this,(()=>(()=>{"use strict";var e={158:e=>{e.exports=require("buffer")}},t={};function __webpack_require__(r){var a=t[r];if(void 0!==a)return a.exports;var n=t[r]={exports:{}};return e[r](n,n.exports,__webpack_require__),n.exports}__webpack_require__.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return __webpack_require__.d(t,{a:t}),t},__webpack_require__.d=(e,t)=>{for(var r in t)__webpack_require__.o(t,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},__webpack_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};__webpack_require__.d(r,{default:()=>To});var a={};__webpack_require__.r(a),__webpack_require__.d(a,{CLEAR:()=>D,CLEAR_BY:()=>U,NEW_AUTH_ERR:()=>L,NEW_SPEC_ERR:()=>$,NEW_SPEC_ERR_BATCH:()=>V,NEW_THROWN_ERR:()=>R,NEW_THROWN_ERR_BATCH:()=>J,clear:()=>clear,clearBy:()=>clearBy,newAuthErr:()=>newAuthErr,newSpecErr:()=>newSpecErr,newSpecErrBatch:()=>newSpecErrBatch,newThrownErr:()=>newThrownErr,newThrownErrBatch:()=>newThrownErrBatch});var n={};__webpack_require__.r(n),__webpack_require__.d(n,{AUTHORIZE:()=>he,AUTHORIZE_OAUTH2:()=>ye,CONFIGURE_AUTH:()=>fe,LOGOUT:()=>ge,RESTORE_AUTHORIZATION:()=>Se,SHOW_AUTH_POPUP:()=>de,authPopup:()=>authPopup,authorize:()=>authorize,authorizeAccessCodeWithBasicAuthentication:()=>authorizeAccessCodeWithBasicAuthentication,authorizeAccessCodeWithFormParams:()=>authorizeAccessCodeWithFormParams,authorizeApplication:()=>authorizeApplication,authorizeOauth2:()=>authorizeOauth2,authorizeOauth2WithPersistOption:()=>authorizeOauth2WithPersistOption,authorizePassword:()=>authorizePassword,authorizeRequest:()=>authorizeRequest,authorizeWithPersistOption:()=>authorizeWithPersistOption,configureAuth:()=>configureAuth,logout:()=>logout,logoutWithPersistOption:()=>logoutWithPersistOption,persistAuthorizationIfNeeded:()=>persistAuthorizationIfNeeded,preAuthorizeImplicit:()=>preAuthorizeImplicit,restoreAuthorization:()=>restoreAuthorization,showDefinitions:()=>showDefinitions});var s={};__webpack_require__.r(s),__webpack_require__.d(s,{authorized:()=>be,definitionsForRequirements:()=>definitionsForRequirements,definitionsToAuthorize:()=>we,getConfigs:()=>Ce,getDefinitionsByNames:()=>getDefinitionsByNames,isAuthorized:()=>isAuthorized,selectAuthPath:()=>selectAuthPath,shownDefinitions:()=>ve});var o={};__webpack_require__.r(o),__webpack_require__.d(o,{TOGGLE_CONFIGS:()=>Me,UPDATE_CONFIGS:()=>Pe,downloadConfig:()=>downloadConfig,getConfigByUrl:()=>getConfigByUrl,loaded:()=>actions_loaded,toggle:()=>toggle,update:()=>update});var l={};__webpack_require__.r(l),__webpack_require__.d(l,{get:()=>get});var c={};__webpack_require__.r(c),__webpack_require__.d(c,{transform:()=>transform});var i={};__webpack_require__.r(i),__webpack_require__.d(i,{transform:()=>parameter_oneof_transform});var p={};__webpack_require__.r(p),__webpack_require__.d(p,{allErrors:()=>Xe,lastError:()=>Ge});var m={};__webpack_require__.r(m),__webpack_require__.d(m,{SHOW:()=>rt,UPDATE_FILTER:()=>et,UPDATE_LAYOUT:()=>Ze,UPDATE_MODE:()=>tt,changeMode:()=>changeMode,show:()=>actions_show,updateFilter:()=>updateFilter,updateLayout:()=>updateLayout});var u={};__webpack_require__.r(u),__webpack_require__.d(u,{current:()=>current,currentFilter:()=>currentFilter,isShown:()=>isShown,showSummary:()=>nt,whatMode:()=>whatMode});var d={};__webpack_require__.r(d),__webpack_require__.d(d,{taggedOperations:()=>taggedOperations});var h={};__webpack_require__.r(h),__webpack_require__.d(h,{getActiveLanguage:()=>lt,getDefaultExpanded:()=>ct,getGenerators:()=>ot,getSnippetGenerators:()=>getSnippetGenerators});var g={};__webpack_require__.r(g),__webpack_require__.d(g,{JsonSchemaArrayItemFile:()=>JsonSchemaArrayItemFile,JsonSchemaArrayItemText:()=>JsonSchemaArrayItemText,JsonSchemaForm:()=>JsonSchemaForm,JsonSchema_array:()=>JsonSchema_array,JsonSchema_boolean:()=>JsonSchema_boolean,JsonSchema_object:()=>JsonSchema_object,JsonSchema_string:()=>JsonSchema_string});var y={};__webpack_require__.r(y),__webpack_require__.d(y,{allowTryItOutFor:()=>allowTryItOutFor,basePath:()=>cr,canExecuteScheme:()=>canExecuteScheme,consumes:()=>ar,consumesOptionsFor:()=>consumesOptionsFor,contentTypeValues:()=>contentTypeValues,currentProducesFor:()=>currentProducesFor,definitions:()=>lr,externalDocs:()=>Yt,findDefinition:()=>findDefinition,getOAS3RequiredRequestBodyContentType:()=>getOAS3RequiredRequestBodyContentType,getParameter:()=>getParameter,hasHost:()=>fr,host:()=>ir,info:()=>Gt,isMediaTypeSchemaPropertiesEqual:()=>isMediaTypeSchemaPropertiesEqual,isOAS3:()=>Xt,lastError:()=>Dt,mutateLine truncated
//# sourceMappingURL=swagger-ui.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui.js","mappings":"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 truncated
+99
View File
@@ -0,0 +1,99 @@
module github.com/FernandoJVideira/simple_bank
go 1.25.4
require (
aidanwoods.dev/go-paseto v1.5.4
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-migrate/migrate/v4 v4.19.0
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3
github.com/hibiken/asynq v0.25.1
github.com/jackc/pgx/v5 v5.7.6
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/rakyll/statik v0.1.7
github.com/redis/go-redis/v9 v9.16.0
github.com/rs/cors v1.11.1
github.com/rs/zerolog v1.34.0
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.43.0
golang.org/x/sync v0.17.0
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
)
require (
aidanwoods.dev/go-result v0.3.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
github.com/go-openapi/swag/loading v0.25.1 // indirect
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+311
View File
@@ -0,0 +1,311 @@
aidanwoods.dev/go-paseto v1.5.4 h1:MH+SBroZEk5Q5pjhVh4l48HIbrdWhWI3SZmA/DXhnuw=
aidanwoods.dev/go-paseto v1.5.4/go.mod h1:Rn37AIcqrvSMu0YPw65CrlEUuoyKL6Yw6B0htrGr3EU=
aidanwoods.dev/go-result v0.3.1 h1:ee98hpohYUVYbI+pa6gUHTyoRerIudgjky/IPSowDXQ=
aidanwoods.dev/go-result v0.3.1/go.mod h1:GKnFg8p/BKulVD3wsfULiPhpPmrTWyiTIbz8EWuUqSk=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
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/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.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
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/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
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/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
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.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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
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.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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.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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
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/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
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/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
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.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
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=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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/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.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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
+128
View File
@@ -0,0 +1,128 @@
package auth
import (
"errors"
"fmt"
"time"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// JWTMaker is a JSON Web Token maker
type JWTMaker struct {
secretKey string
config config.Config
}
func NewJWTClaims(payload *Payload, config config.Config) jwt.Claims {
return &jwt.MapClaims{
"iat": jwt.NewNumericDate(payload.IssuedAt),
"iss": config.TokenISS,
"exp": jwt.NewNumericDate(payload.ExpiredAt),
"nbf": jwt.NewNumericDate(payload.IssuedAt),
"aud": []string{config.TokenAUD},
"sub": payload.Username,
"role": payload.Role,
"jti": payload.ID.String(),
}
}
// NewJWTMaker creates a new JWTMaker
func NewJWTMaker(config config.Config) (Maker, error) {
if len(config.TokenKey) < config.MinTokenSize {
return nil, fmt.Errorf("invalid key size: must be at least %d characters", config.MinTokenSize)
}
return &JWTMaker{secretKey: config.TokenKey, config: config}, nil
}
func (maker *JWTMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) {
payload, err := NewPayload(username, role, duration)
if err != nil {
return "", nil, err
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, NewJWTClaims(payload, maker.config))
signedString, err := jwtToken.SignedString([]byte(maker.secretKey))
return signedString, payload, err
}
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
payloadClaims := jwt.MapClaims{}
keyFunc := func(token *jwt.Token) (any, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(maker.secretKey), nil
}
jwtToken, err := jwt.ParseWithClaims(token, payloadClaims, keyFunc)
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
} else if errors.Is(err, ErrInvalidToken) {
return nil, ErrInvalidToken
} else {
return nil, err
}
}
payloadClaims, ok := jwtToken.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrInvalidToken
}
payload, err := parseTypes(payloadClaims)
if err != nil {
return nil, err
}
return payload, nil
}
func parseTypes(claims jwt.MapClaims) (*Payload, error) {
// Safe type assertions with error checking
jti, ok := claims["jti"].(string)
if !ok {
return nil, ErrInvalidToken
}
id, err := uuid.Parse(jti)
if err != nil {
return nil, ErrInvalidToken
}
username, ok := claims["sub"].(string)
if !ok {
return nil, ErrInvalidToken
}
role, ok := claims["role"].(string)
if !ok {
return nil, ErrInvalidToken
}
// Parse numeric dates
nbf, ok := claims["nbf"].(float64)
if !ok {
return nil, ErrInvalidToken
}
exp, ok := claims["exp"].(float64)
if !ok {
return nil, ErrInvalidToken
}
return &Payload{
ID: id,
Username: username,
Role: role,
IssuedAt: time.Unix(int64(nbf), 0),
ExpiredAt: time.Unix(int64(exp), 0),
}, nil
}
+83
View File
@@ -0,0 +1,83 @@
package auth
import (
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
)
func TestJWTMaker(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewJWTMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := time.Minute
role := role.MemberRole
issuedAt := time.Now()
expiredAt := issuedAt.Add(duration)
token, payload, err := maker.CreateToken(username, role, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.NoError(t, err)
require.NotEmpty(t, payload)
require.NotZero(t, payload.ID)
require.Equal(t, username, payload.Username)
require.Equal(t, role, payload.Role)
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}
func TestExpiredJWTToken(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewJWTMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := -time.Minute
token, payload, err := maker.CreateToken(username, role.MemberRole, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrExpiredToken.Error())
require.Nil(t, payload)
}
func TestInvalidJWTTokenAlgNone(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
payload, err := NewPayload(testutil.RandomOwner(), role.MemberRole, time.Minute)
require.NoError(t, err)
jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, NewJWTClaims(payload, config))
token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
require.NoError(t, err)
maker, err := NewJWTMaker(config)
require.NoError(t, err)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrInvalidToken.Error())
require.Nil(t, payload)
}
+11
View File
@@ -0,0 +1,11 @@
package auth
import "time"
// Maker is an interface for managing tokens
type Maker interface {
// CreateToken creates a new token for a specific username and duration
CreateToken(username string, role string, duration time.Duration) (string, *Payload, error)
// VerifyToken checks if the token is valid or not
VerifyToken(token string) (*Payload, error)
}
+97
View File
@@ -0,0 +1,97 @@
package auth
import (
"strings"
"time"
"aidanwoods.dev/go-paseto"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/google/uuid"
)
type PasetoMaker struct {
config config.Config
symmetricKey paseto.V4SymmetricKey
implicit []byte
}
func NewPasetoMaker(config config.Config) (Maker, error) {
maker := &PasetoMaker{
config: config,
symmetricKey: paseto.NewV4SymmetricKey(),
implicit: []byte("implicit"),
}
return maker, nil
}
func (maker *PasetoMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) {
payload, err := NewPayload(username, role, duration)
if err != nil {
return "", nil, err
}
token := paseto.NewToken()
token.SetIssuedAt(payload.IssuedAt)
token.SetExpiration(payload.ExpiredAt)
token.SetString("role", payload.Role)
token.SetAudience(maker.config.TokenAUD)
token.SetIssuer(maker.config.TokenISS)
token.SetSubject(payload.Username)
token.SetJti(payload.ID.String())
signedToken := token.V4Encrypt(maker.symmetricKey, maker.implicit)
return signedToken, payload, nil
}
func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
parser := paseto.NewParser()
parser.AddRule(paseto.NotExpired())
parsedToken, err := parser.ParseV4Local(maker.symmetricKey, token, maker.implicit)
if err != nil {
if strings.Contains(err.Error(), "token has expired") {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
payload, err := parseClaims(parsedToken)
if err != nil {
return nil, err
}
return payload, nil
}
func parseClaims(t *paseto.Token) (*Payload, error) {
id, err := t.GetJti()
if err != nil {
return nil, ErrInvalidToken
}
username, err := t.GetSubject()
if err != nil {
return nil, ErrInvalidToken
}
role, err := t.GetString("role")
if err != nil {
return nil, ErrInvalidToken
}
issuedAt, err := t.GetIssuedAt()
if err != nil {
return nil, ErrInvalidToken
}
expiredAt, err := t.GetExpiration()
if err != nil {
return nil, ErrInvalidToken
}
payload := &Payload{
ID: uuid.MustParse(id),
Username: username,
Role: role,
IssuedAt: issuedAt,
ExpiredAt: expiredAt,
}
return payload, nil
}
+62
View File
@@ -0,0 +1,62 @@
package auth
import (
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/stretchr/testify/require"
)
func TestPasetoMaker(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewPasetoMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := time.Minute
issuedAt := time.Now()
expiredAt := issuedAt.Add(duration)
role := role.MemberRole
token, payload, err := maker.CreateToken(username, role, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.NoError(t, err)
require.NotEmpty(t, payload)
require.NotZero(t, payload.ID)
require.Equal(t, username, payload.Username)
require.Equal(t, role, payload.Role)
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}
func TestExpiredPasetoToken(t *testing.T) {
config, err := config.LoadTestConfig("../../")
require.NoError(t, err)
maker, err := NewPasetoMaker(config)
require.NoError(t, err)
username := testutil.RandomOwner()
duration := -time.Minute
token, payload, err := maker.CreateToken(username, role.MemberRole, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrExpiredToken.Error())
require.Nil(t, payload)
}
+39
View File
@@ -0,0 +1,39 @@
package auth
import (
"fmt"
"time"
"github.com/google/uuid"
)
var (
ErrExpiredToken = fmt.Errorf("token has expired")
ErrInvalidToken = fmt.Errorf("token is invalid")
)
// Payload contains the payload data of the token
type Payload struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"` // or UserID
Role string `json:"role"`
IssuedAt time.Time `json:"issued_at"`
ExpiredAt time.Time `json:"expired_at"`
}
// NewPayload creates a new token payload with a specific username and duration
func NewPayload(username string, role string, duration time.Duration) (*Payload, error) {
tokenID, err := uuid.NewRandom()
if err != nil {
return nil, err
}
payload := &Payload{
ID: tokenID,
Username: username,
Role: role,
IssuedAt: time.Now(),
ExpiredAt: time.Now().Add(duration),
}
return payload, nil
}
+65
View File
@@ -0,0 +1,65 @@
package config
import (
"time"
"github.com/spf13/viper"
)
// Config stores all configuration of the application.
// The values are read from environment variables or a config file.
type Config struct {
// Server settings
ServiceName string `mapstructure:"SERVICE_NAME"`
FrontendURL string `mapstructure:"FRONTEND_URL"`
AllowedOrigins []string `mapstructure:"ALLOWED_ORIGINS"`
DBAddr string `mapstructure:"DB_ADDR"`
RedisAddr string `mapstructure:"REDIS_ADDR"`
RedisPassword string `mapstructure:"REDIS_PASSWORD"`
RedisDB int `mapstructure:"REDIS_DB"`
MigrationURL string `mapstructure:"MIGRATIONS_URL"`
HTTPServerAddr string `mapstructure:"HTTP_SERVER_ADDR"`
GrpcServerAddr string `mapstructure:"GRPC_SERVER_ADDR"`
AuthTokenExp time.Duration `mapstructure:"AUTH_TOKEN_EXP"`
RefreshTokenExp time.Duration `mapstructure:"REFRESH_TOKEN_EXP"`
MinTokenSize int `mapstructure:"AUTH_TOKEN_MIN_KEY_SIZE"`
TokenKey string `mapstructure:"AUTH_TOKEN_SECRET"`
TokenISS string `mapstructure:"AUTH_TOKEN_ISS"`
TokenAUD string `mapstructure:"AUTH_TOKEN_AUD"`
EmailSender string `mapstructure:"EMAIL_FROM"`
EmailSenderName string `mapstructure:"EMAIL_FROM_NAME"`
EmailPassword string `mapstructure:"EMAIL_SMTP_PASSWORD"`
Environment string `mapstructure:"ENVIRONMENT"`
}
// LoadConfig reads configuration from file or environment variables.
func LoadConfig(path string) (config Config, err error) {
viper.AddConfigPath(path)
viper.SetConfigName("api")
viper.SetConfigType("env") // could be json, toml, yaml, env, etc.
viper.AutomaticEnv() // read in environment variables that match
err = viper.ReadInConfig()
if err != nil {
return
}
err = viper.Unmarshal(&config)
return
}
// LoadTestConfig reads test configuration from file or environment variables.
func LoadTestConfig(path string) (config Config, err error) {
viper.AddConfigPath(path)
viper.SetConfigName("api_test")
viper.SetConfigType("env") // could be json, toml, yaml, env, etc.
viper.AutomaticEnv() // read in environment variables that match
err = viper.ReadInConfig()
if err != nil {
return
}
err = viper.Unmarshal(&config)
return
}
+178
View File
@@ -0,0 +1,178 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/FernandoJVideira/simple_bank/internal/db/sqlc (interfaces: Store)
//
// Generated by this command:
//
// mockgen --package mockdb --destination internal/db/mock/store.go github.com/FernandoJVideira/simple_bank/internal/db/sqlc Store
//
// Package mockdb is a generated GoMock package.
package mockdb
import (
context "context"
reflect "reflect"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
uuid "github.com/google/uuid"
gomock "go.uber.org/mock/gomock"
)
// MockStore is a mock of Store interface.
type MockStore struct {
ctrl *gomock.Controller
recorder *MockStoreMockRecorder
isgomock struct{}
}
// MockStoreMockRecorder is the mock recorder for MockStore.
type MockStoreMockRecorder struct {
mock *MockStore
}
// NewMockStore creates a new mock instance.
func NewMockStore(ctrl *gomock.Controller) *MockStore {
mock := &MockStore{ctrl: ctrl}
mock.recorder = &MockStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStore) EXPECT() *MockStoreMockRecorder {
return m.recorder
}
// CreateSession mocks base method.
func (m *MockStore) CreateSession(ctx context.Context, arg db.CreateSessionParams) (db.Session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSession", ctx, arg)
ret0, _ := ret[0].(db.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateSession indicates an expected call of CreateSession.
func (mr *MockStoreMockRecorder) CreateSession(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockStore)(nil).CreateSession), ctx, arg)
}
// CreateUser mocks base method.
func (m *MockStore) CreateUser(ctx context.Context, arg db.CreateUserParams) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUser", ctx, arg)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUser indicates an expected call of CreateUser.
func (mr *MockStoreMockRecorder) CreateUser(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), ctx, arg)
}
// CreateUserTx mocks base method.
func (m *MockStore) CreateUserTx(ctx context.Context, arg db.CreateUserTxParams) (db.CreateUserTxResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUserTx", ctx, arg)
ret0, _ := ret[0].(db.CreateUserTxResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUserTx indicates an expected call of CreateUserTx.
func (mr *MockStoreMockRecorder) CreateUserTx(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserTx", reflect.TypeOf((*MockStore)(nil).CreateUserTx), ctx, arg)
}
// CreateVerifyEmail mocks base method.
func (m *MockStore) CreateVerifyEmail(ctx context.Context, arg db.CreateVerifyEmailParams) (db.VerifyEmail, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateVerifyEmail", ctx, arg)
ret0, _ := ret[0].(db.VerifyEmail)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateVerifyEmail indicates an expected call of CreateVerifyEmail.
func (mr *MockStoreMockRecorder) CreateVerifyEmail(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVerifyEmail", reflect.TypeOf((*MockStore)(nil).CreateVerifyEmail), ctx, arg)
}
// GetSession mocks base method.
func (m *MockStore) GetSession(ctx context.Context, id uuid.UUID) (db.Session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSession", ctx, id)
ret0, _ := ret[0].(db.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSession indicates an expected call of GetSession.
func (mr *MockStoreMockRecorder) GetSession(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockStore)(nil).GetSession), ctx, id)
}
// GetUser mocks base method.
func (m *MockStore) GetUser(ctx context.Context, username string) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUser", ctx, username)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUser indicates an expected call of GetUser.
func (mr *MockStoreMockRecorder) GetUser(ctx, username any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockStore)(nil).GetUser), ctx, username)
}
// UpdateUser mocks base method.
func (m *MockStore) UpdateUser(ctx context.Context, arg db.UpdateUserParams) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", ctx, arg)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockStoreMockRecorder) UpdateUser(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), ctx, arg)
}
// UpdateVerifyEmail mocks base method.
func (m *MockStore) UpdateVerifyEmail(ctx context.Context, arg db.UpdateVerifyEmailParams) (db.VerifyEmail, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateVerifyEmail", ctx, arg)
ret0, _ := ret[0].(db.VerifyEmail)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateVerifyEmail indicates an expected call of UpdateVerifyEmail.
func (mr *MockStoreMockRecorder) UpdateVerifyEmail(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateVerifyEmail", reflect.TypeOf((*MockStore)(nil).UpdateVerifyEmail), ctx, arg)
}
// VerifyEmailTx mocks base method.
func (m *MockStore) VerifyEmailTx(ctx context.Context, arg db.VerifyEmailTxParams) (db.VerifyEmailTxResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifyEmailTx", ctx, arg)
ret0, _ := ret[0].(db.VerifyEmailTxResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifyEmailTx indicates an expected call of VerifyEmailTx.
func (mr *MockStoreMockRecorder) VerifyEmailTx(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyEmailTx", reflect.TypeOf((*MockStore)(nil).VerifyEmailTx), ctx, arg)
}
+32
View File
@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}
+34
View File
@@ -0,0 +1,34 @@
package db
import (
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
const (
ForeignKeyViolation = "23503"
UniqueViolation = "23505"
CheckViolation = "23514"
)
var ErrRecordNotFound = pgx.ErrNoRows
var ErrUniqueViolation = &pgconn.PgError{
Code: UniqueViolation,
}
var ErrForeignKeyViolation = &pgconn.PgError{
Code: ForeignKeyViolation,
}
var ErrCheckViolation = &pgconn.PgError{
Code: CheckViolation,
}
func ErrorCode(err error) string {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
return pgErr.Code
}
return ""
}
+24
View File
@@ -0,0 +1,24 @@
package db
import (
"context"
"fmt"
)
// execTx executes a function within a database transaction
func (store *PostgresStore) execTx(ctx context.Context, fn func(*Queries) error) error {
tx, err := store.connPool.Begin(ctx)
if err != nil {
return err
}
q := New(tx)
err = fn(q)
if err != nil {
if rbErr := tx.Rollback(ctx); rbErr != nil {
return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr)
}
return err
}
return tx.Commit(ctx)
}
+30
View File
@@ -0,0 +1,30 @@
package db
import (
"context"
"log"
"os"
"testing"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
)
var testStore Store
func TestMain(m *testing.M) {
config, err := config.LoadTestConfig("../../..")
if err != nil {
log.Fatal("cannot load config:", err)
}
connPool, err := pgxpool.New(context.Background(), config.DBAddr)
if err != nil {
log.Fatal("cannot connect to db:", err)
}
testStore = NewPostgresStore(connPool)
os.Exit(m.Run())
}
+43
View File
@@ -0,0 +1,43 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"time"
"github.com/google/uuid"
)
type Session struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
RefreshToken string `json:"refresh_token"`
UserAgent string `json:"user_agent"`
ClientIp string `json:"client_ip"`
IsBlocked bool `json:"is_blocked"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
type User struct {
Username string `json:"username"`
Role string `json:"role"`
HashedPassword string `json:"hashed_password"`
FullName string `json:"full_name"`
Email string `json:"email"`
IsEmailVerified bool `json:"is_email_verified"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
}
type VerifyEmail struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
SecretCode string `json:"secret_code"`
IsUsed bool `json:"is_used"`
CreatedAt time.Time `json:"created_at"`
ExpiredAt time.Time `json:"expired_at"`
}
+23
View File
@@ -0,0 +1,23 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/google/uuid"
)
type Querier interface {
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
CreateVerifyEmail(ctx context.Context, arg CreateVerifyEmailParams) (VerifyEmail, error)
GetSession(ctx context.Context, id uuid.UUID) (Session, error)
GetUser(ctx context.Context, username string) (User, error)
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
UpdateVerifyEmail(ctx context.Context, arg UpdateVerifyEmailParams) (VerifyEmail, error)
}
var _ Querier = (*Queries)(nil)
+83
View File
@@ -0,0 +1,83 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: sessions.sql
package db
import (
"context"
"time"
"github.com/google/uuid"
)
const createSession = `-- name: CreateSession :one
INSERT INTO sessions (
id,
username,
refresh_token,
user_agent,
client_ip,
is_blocked,
expires_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
)
RETURNING id, username, refresh_token, user_agent, client_ip, is_blocked, expires_at, created_at
`
type CreateSessionParams struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
RefreshToken string `json:"refresh_token"`
UserAgent string `json:"user_agent"`
ClientIp string `json:"client_ip"`
IsBlocked bool `json:"is_blocked"`
ExpiresAt time.Time `json:"expires_at"`
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
row := q.db.QueryRow(ctx, createSession,
arg.ID,
arg.Username,
arg.RefreshToken,
arg.UserAgent,
arg.ClientIp,
arg.IsBlocked,
arg.ExpiresAt,
)
var i Session
err := row.Scan(
&i.ID,
&i.Username,
&i.RefreshToken,
&i.UserAgent,
&i.ClientIp,
&i.IsBlocked,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}
const getSession = `-- name: GetSession :one
SELECT id, username, refresh_token, user_agent, client_ip, is_blocked, expires_at, created_at FROM sessions
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetSession(ctx context.Context, id uuid.UUID) (Session, error) {
row := q.db.QueryRow(ctx, getSession, id)
var i Session
err := row.Scan(
&i.ID,
&i.Username,
&i.RefreshToken,
&i.UserAgent,
&i.ClientIp,
&i.IsBlocked,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}
+27
View File
@@ -0,0 +1,27 @@
package db
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
type Store interface {
Querier
CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error)
VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error)
}
// PostgresStore provides all functions to execute db queries and transactions
type PostgresStore struct {
*Queries
connPool *pgxpool.Pool
}
// NewPostgresStore creates a new Store
func NewPostgresStore(connPool *pgxpool.Pool) Store {
return &PostgresStore{
connPool: connPool,
Queries: New(connPool),
}
}
+34
View File
@@ -0,0 +1,34 @@
package db
import (
"context"
"github.com/rs/zerolog/log"
)
type CreateUserTxParams struct {
CreateUserParams
AfterCreate func(user User) error
}
type CreateUserTxResult struct {
User User `json:"user"`
}
func (store *PostgresStore) CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error) {
var result CreateUserTxResult
err := store.execTx(ctx, func(q *Queries) error {
var err error
result.User, err = q.CreateUser(ctx, arg.CreateUserParams)
if err != nil {
return err
}
log.Info().Msgf("user %s created successfully", result.User.Username)
return arg.AfterCreate(result.User)
})
return result, err
}
+44
View File
@@ -0,0 +1,44 @@
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type VerifyEmailTxParams struct {
EmailID int64
SecretCode string
}
type VerifyEmailTxResult struct {
User User
VerifyEmail VerifyEmail
}
func (store *PostgresStore) VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error) {
var result VerifyEmailTxResult
err := store.execTx(ctx, func(q *Queries) error {
var err error
result.VerifyEmail, err = q.UpdateVerifyEmail(ctx, UpdateVerifyEmailParams{
ID: arg.EmailID,
SecretCode: arg.SecretCode,
})
if err != nil {
return err
}
result.User, err = q.UpdateUser(ctx, UpdateUserParams{
Username: result.VerifyEmail.Username,
IsEmailVerified: pgtype.Bool{
Bool: true,
Valid: true,
},
})
return err
})
return result, err
}
+117
View File
@@ -0,0 +1,117 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: user.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (
username,
hashed_password,
full_name,
email
) VALUES (
$1, $2, $3, $4
)
RETURNING username, role, hashed_password, full_name, email, is_email_verified, password_changed_at, created_at
`
type CreateUserParams struct {
Username string `json:"username"`
HashedPassword string `json:"hashed_password"`
FullName string `json:"full_name"`
Email string `json:"email"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRow(ctx, createUser,
arg.Username,
arg.HashedPassword,
arg.FullName,
arg.Email,
)
var i User
err := row.Scan(
&i.Username,
&i.Role,
&i.HashedPassword,
&i.FullName,
&i.Email,
&i.IsEmailVerified,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
const getUser = `-- name: GetUser :one
SELECT username, role, hashed_password, full_name, email, is_email_verified, password_changed_at, created_at FROM users
WHERE username = $1 LIMIT 1
`
func (q *Queries) GetUser(ctx context.Context, username string) (User, error) {
row := q.db.QueryRow(ctx, getUser, username)
var i User
err := row.Scan(
&i.Username,
&i.Role,
&i.HashedPassword,
&i.FullName,
&i.Email,
&i.IsEmailVerified,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
hashed_password = COALESCE($1, hashed_password),
password_changed_at = COALESCE($2, password_changed_at),
full_name = COALESCE($3, full_name),
email = COALESCE($4, email),
is_email_verified = COALESCE($5, is_email_verified)
WHERE username = $6
RETURNING username, role, hashed_password, full_name, email, is_email_verified, password_changed_at, created_at
`
type UpdateUserParams struct {
HashedPassword pgtype.Text `json:"hashed_password"`
PasswordChangedAt pgtype.Timestamptz `json:"password_changed_at"`
FullName pgtype.Text `json:"full_name"`
Email pgtype.Text `json:"email"`
IsEmailVerified pgtype.Bool `json:"is_email_verified"`
Username string `json:"username"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
row := q.db.QueryRow(ctx, updateUser,
arg.HashedPassword,
arg.PasswordChangedAt,
arg.FullName,
arg.Email,
arg.IsEmailVerified,
arg.Username,
)
var i User
err := row.Scan(
&i.Username,
&i.Role,
&i.HashedPassword,
&i.FullName,
&i.Email,
&i.IsEmailVerified,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
+146
View File
@@ -0,0 +1,146 @@
package db
import (
"context"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/security"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/require"
)
func createRandomUser(t *testing.T) User {
hashedPassword, err := security.HashPassword(testutil.RandomString(6))
require.NoError(t, err)
// Create the user
arg := CreateUserParams{
Username: testutil.RandomOwner(),
HashedPassword: hashedPassword,
FullName: testutil.RandomOwner(),
Email: testutil.RandomEmail(),
}
user, err := testStore.CreateUser(context.Background(), arg)
require.NoError(t, err) // -> Err should be nil
require.NotEmpty(t, user)
require.Equal(t, arg.Username, user.Username)
require.Equal(t, arg.HashedPassword, user.HashedPassword)
require.Equal(t, arg.FullName, user.FullName)
require.Equal(t, arg.Email, user.Email)
require.True(t, user.PasswordChangedAt.IsZero())
require.NotZero(t, user.CreatedAt)
return user
}
func TestCreateUser(t *testing.T) {
createRandomUser(t)
}
func TestGetUser(t *testing.T) {
user1 := createRandomUser(t)
user2, err := testStore.GetUser(context.Background(), user1.Username)
require.NoError(t, err)
require.NotEmpty(t, user2)
require.Equal(t, user1.Username, user2.Username)
require.Equal(t, user1.HashedPassword, user2.HashedPassword)
require.Equal(t, user1.FullName, user2.FullName)
require.Equal(t, user1.Email, user2.Email)
require.WithinDuration(t, user1.CreatedAt, user2.CreatedAt, time.Second)
}
func TestUpdateUserOnlyFullName(t *testing.T) {
oldUser := createRandomUser(t)
newFullName := testutil.RandomOwner()
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
FullName: pgtype.Text{
String: newFullName,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.FullName, updatedUser.FullName)
require.Equal(t, newFullName, updatedUser.FullName)
require.Equal(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, oldUser.Email, updatedUser.Email)
}
func TestUpdateUserOnlyEmail(t *testing.T) {
oldUser := createRandomUser(t)
newEmail := testutil.RandomEmail()
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
Email: pgtype.Text{
String: newEmail,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.Email, updatedUser.Email)
require.Equal(t, newEmail, updatedUser.Email)
require.Equal(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, oldUser.FullName, updatedUser.FullName)
}
func TestUpdateUserOnlyPassword(t *testing.T) {
oldUser := createRandomUser(t)
newPassword, err := security.HashPassword(testutil.RandomString(6))
require.NoError(t, err)
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
HashedPassword: pgtype.Text{
String: newPassword,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, newPassword, updatedUser.HashedPassword)
require.Equal(t, oldUser.FullName, updatedUser.FullName)
require.Equal(t, oldUser.Email, updatedUser.Email)
}
func TestUpdateUserAllFields(t *testing.T) {
oldUser := createRandomUser(t)
newFullName := testutil.RandomOwner()
newEmail := testutil.RandomEmail()
newPassword, err := security.HashPassword(testutil.RandomString(6))
require.NoError(t, err)
updatedUser, err := testStore.UpdateUser(context.Background(), UpdateUserParams{
Username: oldUser.Username,
FullName: pgtype.Text{
String: newFullName,
Valid: true,
},
Email: pgtype.Text{
String: newEmail,
Valid: true,
},
HashedPassword: pgtype.Text{
String: newPassword,
Valid: true,
},
})
require.NoError(t, err)
require.NotEqual(t, oldUser.FullName, updatedUser.FullName)
require.Equal(t, newFullName, updatedUser.FullName)
require.NotEqual(t, oldUser.HashedPassword, updatedUser.HashedPassword)
require.Equal(t, newPassword, updatedUser.HashedPassword)
require.NotEqual(t, oldUser.Email, updatedUser.Email)
require.Equal(t, newEmail, updatedUser.Email)
}
+74
View File
@@ -0,0 +1,74 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: verify_email.sql
package db
import (
"context"
)
const createVerifyEmail = `-- name: CreateVerifyEmail :one
INSERT INTO verify_emails (
username,
email,
secret_code
) VALUES (
$1, $2, $3
)
RETURNING id, username, email, secret_code, is_used, created_at, expired_at
`
type CreateVerifyEmailParams struct {
Username string `json:"username"`
Email string `json:"email"`
SecretCode string `json:"secret_code"`
}
func (q *Queries) CreateVerifyEmail(ctx context.Context, arg CreateVerifyEmailParams) (VerifyEmail, error) {
row := q.db.QueryRow(ctx, createVerifyEmail, arg.Username, arg.Email, arg.SecretCode)
var i VerifyEmail
err := row.Scan(
&i.ID,
&i.Username,
&i.Email,
&i.SecretCode,
&i.IsUsed,
&i.CreatedAt,
&i.ExpiredAt,
)
return i, err
}
const updateVerifyEmail = `-- name: UpdateVerifyEmail :one
UPDATE verify_emails
SET
is_used = TRUE
WHERE
id = $1
AND secret_code = $2
AND is_used = FALSE
AND expired_at > NOW()
RETURNING id, username, email, secret_code, is_used, created_at, expired_at
`
type UpdateVerifyEmailParams struct {
ID int64 `json:"id"`
SecretCode string `json:"secret_code"`
}
func (q *Queries) UpdateVerifyEmail(ctx context.Context, arg UpdateVerifyEmailParams) (VerifyEmail, error) {
row := q.db.QueryRow(ctx, updateVerifyEmail, arg.ID, arg.SecretCode)
var i VerifyEmail
err := row.Scan(
&i.ID,
&i.Username,
&i.Email,
&i.SecretCode,
&i.IsUsed,
&i.CreatedAt,
&i.ExpiredAt,
)
return i, err
}
+12
View File
@@ -0,0 +1,12 @@
package role
import "slices"
const (
MemberRole = "member"
AdminRole = "admin"
)
func HasPermission(role string, accessibleRoles []string) bool {
return slices.Contains(accessibleRoles, role)
}
+51
View File
@@ -0,0 +1,51 @@
package gapi
import (
"context"
"fmt"
"strings"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"google.golang.org/grpc/metadata"
)
const (
authHeaderKey = "authorization"
authTypeBearer = "bearer"
)
func (s *Server) authorizeUser(ctx context.Context, accessibleRoles []string) (*auth.Payload, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("missing metadata")
}
values := md.Get(authHeaderKey)
if len(values) == 0 {
return nil, fmt.Errorf("missing authorization header")
}
authHeader := values[0]
fields := strings.Fields(authHeader)
if len(fields) < 2 {
return nil, fmt.Errorf("invalid authorization header format")
}
authType := strings.ToLower(fields[0])
if authType != authTypeBearer {
return nil, fmt.Errorf("unsupported authorization type: %s", authType)
}
accessToken := fields[1]
payload, err := s.tokenMaker.VerifyToken(accessToken)
if err != nil {
return nil, fmt.Errorf("invalid token: %v", err)
}
if !role.HasPermission(payload.Role, accessibleRoles) {
return nil, fmt.Errorf("permission denied")
}
return payload, nil
}
+38
View File
@@ -0,0 +1,38 @@
package gapi
import (
"time"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/timestamppb"
)
func convertUser(user db.User) *pb.User {
return &pb.User{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
CreatedAt: timestamppb.New(user.CreatedAt),
PasswordChangedAt: timestamppb.New(user.PasswordChangedAt),
}
}
func convertLoginUserResponse(
user db.User,
sessionID uuid.UUID,
accessToken string,
refreshToken string,
accessExpiredAt time.Time,
refreshExpiredAt time.Time,
) *pb.LoginUserResponse {
return &pb.LoginUserResponse{
User: convertUser(user),
SessionId: sessionID.String(),
AccessToken: accessToken,
RefreshToken: refreshToken,
AccessTokenExpiresAt: timestamppb.New(accessExpiredAt),
RefreshTokenExpiresAt: timestamppb.New(refreshExpiredAt),
}
}
+24
View File
@@ -0,0 +1,24 @@
package gapi
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func fieldViolation(field string, err error) *errdetails.BadRequest_FieldViolation {
return &errdetails.BadRequest_FieldViolation{
Field: field,
Description: err.Error(),
}
}
func invalidArgumentError(violations []*errdetails.BadRequest_FieldViolation) error {
badRequest := &errdetails.BadRequest{FieldViolations: violations}
stInvalid := status.New(codes.InvalidArgument, "invalid user request")
stDetails, err := stInvalid.WithDetails(badRequest)
if err != nil {
return stInvalid.Err()
}
return stDetails.Err()
}
+79
View File
@@ -0,0 +1,79 @@
package gapi
import (
"context"
"net/http"
"time"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func GrpcLogger(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
startTime := time.Now()
result, err := handler(ctx, req)
duration := time.Since(startTime)
statusCode := codes.Unknown
if st, ok := status.FromError(err); ok {
statusCode = st.Code()
}
logger := log.Info()
if err != nil {
logger = log.Error().Err(err)
}
logger.Str("protocol", "grpc").
Str("method", info.FullMethod).
Int("status", int(statusCode)).
Str("status_text", statusCode.String()).
Dur("duration", duration).
Msg("Recieved a gRPC request")
return result, err
}
type ResponseRecorder struct {
http.ResponseWriter
StatusCode int
Body []byte
}
func (rr *ResponseRecorder) WriteHeader(statusCode int) {
rr.StatusCode = statusCode
rr.ResponseWriter.WriteHeader(statusCode)
}
func (rr *ResponseRecorder) Write(b []byte) (int, error) {
rr.Body = b
return rr.ResponseWriter.Write(b)
}
func HTTPLogger(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
rr := &ResponseRecorder{
ResponseWriter: w,
StatusCode: http.StatusOK,
}
handler.ServeHTTP(rr, r)
duration := time.Since(startTime)
logger := log.Info()
if rr.StatusCode != http.StatusOK {
logger = log.Error().Bytes("body", rr.Body)
}
logger.Str("protocol", "http").
Str("method", r.Method).
Str("path", r.RequestURI).
Int("status", rr.StatusCode).
Str("status_text", http.StatusText(rr.StatusCode)).
Dur("duration", duration).
Msg("Recieved an HTTP request")
})
}
+46
View File
@@ -0,0 +1,46 @@
package gapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
)
func newTestServer(t *testing.T, store db.Store, td worker.TaskDistributor, userService service.UserService, authService service.AuthService) *Server {
config := config.Config{
AuthTokenExp: time.Minute,
AllowedOrigins: []string{
"*",
},
}
tokenMaker, err := auth.NewPasetoMaker(config)
require.NoError(t, err)
server, err := NewServer(config, store, td, tokenMaker, userService, authService)
require.NoError(t, err)
return server
}
func newContextWithBearerToken(t *testing.T, tokenMaker auth.Maker, username string, role string, duration time.Duration) context.Context {
authToken, _, err := tokenMaker.CreateToken(username, role, duration)
require.NoError(t, err)
bearerToken := fmt.Sprintf("%s %s", authTypeBearer, authToken)
md := metadata.MD{
authHeaderKey: []string{
bearerToken,
},
}
return metadata.NewIncomingContext(context.Background(), md)
}
+42
View File
@@ -0,0 +1,42 @@
package gapi
import (
"context"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
)
const (
grpcGatewayUserAgentHeader = "grpcgateway-user-agent"
userAgentHeader = "user-agent"
xForwardedForHeader = "x-forwarded-for"
)
type Metadata struct {
UserAgent string
ClientIP string
}
func (s *Server) extractMetadata(ctx context.Context) *Metadata {
mtdt := Metadata{}
md, ok := metadata.FromIncomingContext(ctx)
if ok {
if userAgent := md.Get(grpcGatewayUserAgentHeader); len(userAgent) > 0 {
mtdt.UserAgent = userAgent[0]
}
if userAgent := md.Get(userAgentHeader); len(userAgent) > 0 {
mtdt.UserAgent = userAgent[0]
}
if clientIPs := md.Get(xForwardedForHeader); len(clientIPs) > 0 {
mtdt.ClientIP = clientIPs[0]
}
}
if p, ok := peer.FromContext(ctx); ok {
mtdt.ClientIP = p.Addr.String()
}
return &mtdt
}
+58
View File
@@ -0,0 +1,58 @@
package gapi
import (
"context"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
violations := validateCreateUserRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
result, err := s.userService.CreateUser(ctx, service.RegisterUserRequest{
Username: req.GetUsername(),
Password: req.GetPassword(),
FullName: req.GetFullName(),
Email: req.GetEmail(),
})
if err != nil {
// Map service errors to gRPC status codes
if db.ErrorCode(err) == db.UniqueViolation {
return nil, status.Errorf(codes.AlreadyExists, "username already exists")
}
return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
}
resp := &pb.CreateUserResponse{
User: convertUser(result.User),
}
return resp, nil
}
func validateCreateUserRequest(req *pb.CreateUserRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateUsername(req.GetUsername()); err != nil {
violations = append(violations, fieldViolation("username", err))
}
if err := val.ValidateFullName(req.GetFullName()); err != nil {
violations = append(violations, fieldViolation("full_name", err))
}
if err := val.ValidatePassword(req.GetPassword()); err != nil {
violations = append(violations, fieldViolation("password", err))
}
if err := val.ValidateEmail(req.GetEmail()); err != nil {
violations = append(violations, fieldViolation("email", err))
}
return violations
}
@@ -0,0 +1,182 @@
package gapi
import (
"context"
"database/sql"
"testing"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/security"
"github.com/FernandoJVideira/simple_bank/internal/service"
mocksvc "github.com/FernandoJVideira/simple_bank/internal/service/mock"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func randomUser(t *testing.T) (user db.User, password string) {
password = testutil.RandomString(6)
hashedPassword, err := security.HashPassword(password)
if err != nil {
t.Fatal(err)
}
user = db.User{
Username: testutil.RandomOwner(),
HashedPassword: hashedPassword,
Role: role.MemberRole,
FullName: testutil.RandomOwner(),
Email: testutil.RandomEmail(),
}
return
}
func TestCreateUserRPC(t *testing.T) {
user, password := randomUser(t)
testCases := []struct {
name string
req *pb.CreateUserRequest
buildStubs func(userService *mocksvc.MockUserService)
checkResponse func(t *testing.T, res *pb.CreateUserResponse, err error)
}{
{
name: "OK",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
arg := service.RegisterUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
}
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Eq(arg)).
Times(1).
Return(&service.RegisterUserResponse{
User: user,
}, nil)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, user.Username, res.User.Username)
require.Equal(t, user.FullName, res.User.FullName)
require.Equal(t, user.Email, res.User.Email)
require.NotEmpty(t, res.User.CreatedAt)
},
},
{
name: "InternalError",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(nil, sql.ErrConnDone)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.Internal)
},
},
{
name: "DuplicateUsername",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(nil, db.ErrUniqueViolation)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.AlreadyExists)
},
},
{
name: "InvalidEmail",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: "invalid-email",
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.InvalidArgument)
},
},
{
name: "PasswordTooShort",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: "short",
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.InvalidArgument)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
userCtrl := gomock.NewController(t)
defer userCtrl.Finish()
userService := mocksvc.NewMockUserService(userCtrl)
tc.buildStubs(userService)
// Start test server and send request
s := newTestServer(t, nil, nil, userService, nil)
res, err := s.CreateUser(context.Background(), tc.req)
tc.checkResponse(t, res, err)
})
}
}
+64
View File
@@ -0,0 +1,64 @@
package gapi
import (
"context"
"errors"
"strings"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) LoginUser(ctx context.Context, req *pb.LoginUserRequest) (*pb.LoginUserResponse, error) {
violations := validateLoginUserRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
mtdt := s.extractMetadata(ctx)
result, err := s.authService.Login(ctx, service.LoginCredentials{
Username: req.GetUsername(),
Password: req.GetPassword(),
UserAgent: mtdt.UserAgent,
ClientIP: mtdt.ClientIP,
})
if err != nil {
errMsg := err.Error()
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(errMsg, "user not found") {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if strings.Contains(errMsg, "incorrect password") {
return nil, status.Errorf(codes.Unauthenticated, "incorrect password")
}
return nil, status.Errorf(codes.Internal, "failed to login: %v", err)
}
resp := convertLoginUserResponse(
result.User,
result.SessionID,
result.AccessToken,
result.RefreshToken,
result.AccessTokenExpiresAt,
result.RefreshTokenExpiresAt,
)
return resp, nil
}
// TODO: Change to throw "username or password is incorrect" instead of specifying which one is wrong
func validateLoginUserRequest(req *pb.LoginUserRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateUsername(req.GetUsername()); err != nil {
violations = append(violations, fieldViolation("username", err))
}
if err := val.ValidatePassword(req.GetPassword()); err != nil {
violations = append(violations, fieldViolation("password", err))
}
return violations
}
+88
View File
@@ -0,0 +1,88 @@
package gapi
import (
"context"
"errors"
"strings"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) {
authPayload, err := s.authorizeUser(ctx, []string{role.AdminRole, role.MemberRole})
if err != nil {
return nil, unauthenticatedError(err)
}
if authPayload.Role != role.AdminRole && authPayload.Username != req.GetUsername() {
return nil, status.Errorf(codes.PermissionDenied, "cannot update other user's information")
}
violations := validateUpdateUserRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
updateReq := service.UserUpdateRequest{
Username: req.GetUsername(),
}
// Set FullName if provided
if req.FullName != nil {
fullName := req.GetFullName()
updateReq.FullName = &fullName
}
// Set Email if provided
if req.Email != nil {
email := req.GetEmail()
updateReq.Email = &email
}
user, err := s.userService.UpdateUser(ctx, updateReq)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(err.Error(), "not found") {
return nil, status.Errorf(codes.NotFound, "user not found: %v", err)
}
return nil, status.Errorf(codes.Internal, "failed to update user: %v", err)
}
resp := &pb.UpdateUserResponse{
User: convertUser(*user),
}
return resp, nil
}
func validateUpdateUserRequest(req *pb.UpdateUserRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateUsername(req.GetUsername()); err != nil {
violations = append(violations, fieldViolation("username", err))
}
if req.FullName != nil {
if err := val.ValidateFullName(req.GetFullName()); err != nil {
violations = append(violations, fieldViolation("full_name", err))
}
}
if req.Password != nil {
if err := val.ValidatePassword(req.GetPassword()); err != nil {
violations = append(violations, fieldViolation("password", err))
}
}
if req.Email != nil {
if err := val.ValidateEmail(req.GetEmail()); err != nil {
violations = append(violations, fieldViolation("email", err))
}
}
return violations
}
func unauthenticatedError(err error) error {
return status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err)
}
@@ -0,0 +1,184 @@
package gapi
import (
"context"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
mocksvc "github.com/FernandoJVideira/simple_bank/internal/service/mock"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestUpdateUserRPC(t *testing.T) {
user, _ := randomUser(t)
newName := testutil.RandomOwner()
newEmail := testutil.RandomEmail()
invalidEmail := "invalid-email-format"
testCases := []struct {
name string
req *pb.UpdateUserRequest
buildStubs func(userService *mocksvc.MockUserService)
buildContext func(t *testing.T, tokenMaker auth.Maker) context.Context
checkResponse func(t *testing.T, res *pb.UpdateUserResponse, err error)
}{
{
name: "OK",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
arg := service.UserUpdateRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
}
updatedUser := db.User{
Username: user.Username,
HashedPassword: user.HashedPassword,
FullName: newName,
Email: newEmail,
}
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Eq(arg)).
Times(1).
Return(&updatedUser, nil)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.NoError(t, err)
require.NotNil(t, res)
updatedUser := res.GetUser()
require.Equal(t, user.Username, updatedUser.Username)
require.Equal(t, newName, updatedUser.FullName)
require.Equal(t, newEmail, updatedUser.Email)
},
}, {
name: "UserNotFound",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(nil, db.ErrRecordNotFound)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.NotFound)
},
}, {
name: "ExpiredToken",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(0)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, -time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.Unauthenticated)
},
}, {
name: "NoAuthorizationHeader",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(0)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return context.Background()
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.Unauthenticated)
},
}, {
name: "InvalidEmail",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &invalidEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(0)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.InvalidArgument)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
userCtrl := gomock.NewController(t)
defer userCtrl.Finish()
userService := mocksvc.NewMockUserService(userCtrl)
tc.buildStubs(userService)
// Start test server and send request
s := newTestServer(t, nil, nil, userService, nil)
ctx := tc.buildContext(t, s.tokenMaker)
// Call UpdateUser RPC
res, err := s.UpdateUser(ctx, tc.req)
tc.checkResponse(t, res, err)
})
}
}
@@ -0,0 +1,44 @@
package gapi
import (
"context"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) VerifyEmail(ctx context.Context, req *pb.VerifyEmailRequest) (*pb.VerifyEmailResponse, error) {
violations := validateVerifyEmailRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
result, err := s.userService.VerifyEmail(ctx, service.EmailVerificationRequest{
EmailID: req.GetId(),
SecretCode: req.GetCode(),
})
if err != nil {
return nil, status.Error(codes.Internal, "failed to verify email")
}
resp := &pb.VerifyEmailResponse{
IsVerified: result.User.IsEmailVerified,
}
return resp, nil
}
func validateVerifyEmailRequest(req *pb.VerifyEmailRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateEmailID(req.GetId()); err != nil {
violations = append(violations, fieldViolation("id", err))
}
if err := val.ValidateVerificationCode(req.GetCode()); err != nil {
violations = append(violations, fieldViolation("secret_code", err))
}
return violations
}
+43
View File
@@ -0,0 +1,43 @@
// Package gapi implements the gRPC server for the Simple Bank application.
package gapi
import (
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/FernandoJVideira/simple_bank/pb"
)
// Server serves gRPC requests for our banking service.
type Server struct {
pb.UnimplementedSimpleBankServer // Embedding for forward compatibility
config config.Config
store db.Store
tokenMaker auth.Maker
taskDistributor worker.TaskDistributor
userService service.UserService
authService service.AuthService
}
// NewServer creates a new gRPC server instance.
func NewServer(
config config.Config,
store db.Store,
taskDistributor worker.TaskDistributor,
tokenMaker auth.Maker,
userService service.UserService,
authService service.AuthService,
) (*Server, error) {
server := &Server{
config: config,
store: store,
tokenMaker: tokenMaker,
taskDistributor: taskDistributor,
userService: userService,
authService: authService,
}
return server, nil
}
+38
View File
@@ -0,0 +1,38 @@
package api
import (
"os"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/gin-gonic/gin"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/stretchr/testify/require"
)
func newTestServer(t *testing.T, store db.Store, td worker.TaskDistributor, userService service.UserService, authService service.AuthService) *Server {
config := config.Config{
AuthTokenExp: time.Minute,
AllowedOrigins: []string{
"*",
},
}
tokenMaker, err := auth.NewPasetoMaker(config)
require.NoError(t, err)
server, err := NewServer(config, store, td, tokenMaker, userService, authService)
require.NoError(t, err)
return server
}
func TestMain(m *testing.M) {
gin.SetMode(gin.TestMode)
os.Exit(m.Run())
}
+80
View File
@@ -0,0 +1,80 @@
package api
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/handlers/http/response"
"github.com/gin-gonic/gin"
)
const (
authHeaderKey = "authorization"
authTypeBearer = "bearer"
authorizationPayloadKey = "authorization_payload"
)
func authMiddleware(tokenMaker auth.Maker, accessibleRoles []string) gin.HandlerFunc {
return func(ctx *gin.Context) {
authorizationHeader := ctx.GetHeader(authHeaderKey)
if len(authorizationHeader) == 0 {
err := errors.New("authorization header is not provided")
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
return
}
// Split
fields := strings.Fields(authorizationHeader)
if len(fields) < 2 {
err := errors.New("invalid authorization header format")
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
}
authType := strings.ToLower(fields[0])
if authType != authTypeBearer {
err := fmt.Errorf("unsupported authorization type %s", authType)
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
return
}
accessToken := fields[1]
payload, err := tokenMaker.VerifyToken(accessToken)
if err != nil {
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
return
}
if !role.HasPermission(payload.Role, accessibleRoles) {
err := fmt.Errorf("permission denied")
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusForbidden, resp)
return
}
ctx.Set(authorizationPayloadKey, payload)
ctx.Next()
}
}
+114
View File
@@ -0,0 +1,114 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
mocksvc "github.com/FernandoJVideira/simple_bank/internal/service/mock"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func addAuth(t *testing.T, request *http.Request, tokenMaker auth.Maker, authType string, username string, role string, duration time.Duration) {
token, payload, err := tokenMaker.CreateToken(username, role, duration)
require.NoError(t, err)
require.NotEmpty(t, payload)
authorizationHeader := fmt.Sprintf("%s %s", authType, token)
request.Header.Set(authHeaderKey, authorizationHeader)
}
func TestAuthMiddleware(t *testing.T) {
username := testutil.RandomOwner()
testRole := role.MemberRole
testCases := []struct {
name string
setupAuth func(t *testing.T, request *http.Request, tokenMaker auth.Maker)
checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, authTypeBearer, username, testRole, time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
},
},
{
name: "NoAuthorization",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
// Do not add any authorization header
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "UnsupportedAuthType",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, "UnsupportedAuthType", username, testRole, time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "InvalidAuthFormat",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, "", username, testRole, time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "ExpiredAtToken",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, authTypeBearer, username, testRole, -time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
authCtrl := gomock.NewController(t)
defer authCtrl.Finish()
authService := mocksvc.NewMockAuthService(authCtrl)
s := newTestServer(t, nil, nil, nil, authService)
authPath := "/auth"
s.router.GET(
authPath,
authMiddleware(s.tokenMaker, []string{role.AdminRole, role.MemberRole}),
func(ctx *gin.Context) {
ctx.JSON(200, gin.H{})
},
)
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, authPath, nil)
require.NoError(t, err)
tc.setupAuth(t, request, s.tokenMaker)
s.router.ServeHTTP(recorder, request)
tc.checkResponse(t, recorder)
})
}
}
+8
View File
@@ -0,0 +1,8 @@
package response
type JSONResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
+130
View File
@@ -0,0 +1,130 @@
// Package api provides the HTTP server and routing for the banking service.
package api
import (
"context"
"net/http"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/rs/zerolog/log"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// Server serves HTTP requests for our banking service.
type Server struct {
config config.Config
store db.Store
tokenMaker auth.Maker
router *gin.Engine
taskDisributor worker.TaskDistributor
httpServer *http.Server
userService service.UserService
authService service.AuthService
}
func NewServer(
config config.Config,
store db.Store,
taskDistributor worker.TaskDistributor,
tokenMaker auth.Maker,
userService service.UserService,
authService service.AuthService,
) (*Server, error) {
s := &Server{
config: config,
store: store,
tokenMaker: tokenMaker,
taskDisributor: taskDistributor,
userService: userService,
authService: authService,
}
// if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// err := v.RegisterValidation("currency", validCurrency)
// if err != nil {
// return nil, err
// }
// }
s.setupRouter(config)
return s, nil
}
func (s *Server) setupRouter(config config.Config) {
router := gin.Default()
// Configure CORS middleware
router.Use(cors.New(cors.Config{
AllowOrigins: config.AllowedOrigins,
AllowMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowHeaders: []string{
"Accept",
"Authorization",
"Content-Type",
"X-CSRF-Token",
},
AllowCredentials: true,
}))
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Add routes
v1 := router.Group("/api/v1")
{
auth := v1.Group("/auth")
{
auth.POST("/login", s.loginUser)
auth.POST("/register", s.createUser)
auth.GET("/verify", s.verifyEmail)
auth.POST("/refresh", s.renewAccessToken)
}
user := v1.Group("/users")
{
me := user.Group("/me").Use(authMiddleware(s.tokenMaker, []string{role.AdminRole, role.MemberRole}))
{
me.GET("/", s.getCurrentUser)
me.PUT("/", s.updateUser)
me.PUT("/password", s.changePassword)
}
user.GET("/:username", s.getUser)
}
}
s.router = router
}
// Start runs the HTTP server on a specific address.
func (s *Server) Start(address string) error {
s.httpServer = &http.Server{
Addr: address,
Handler: s.router,
}
return s.httpServer.ListenAndServe()
}
func (s *Server) Shutdown(ctx context.Context) error {
log.Info().Msg("shutting down Gin HTTP server")
if s.httpServer != nil {
return s.httpServer.Shutdown(ctx)
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package api
import (
"errors"
"net/http"
"strings"
"time"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/handlers/http/response"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/gin-gonic/gin"
)
type renewAccessTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
type renewAccessTokenResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
}
func (s *Server) renewAccessToken(ctx *gin.Context) {
var req renewAccessTokenRequest
var resp response.JSONResponse
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
// Call auth service - all business logic moved there
result, err := s.authService.RenewAccessToken(ctx, service.TokenRenewalRequest{
RefreshToken: req.RefreshToken,
})
if err != nil {
statusCode := http.StatusInternalServerError
errMsg := err.Error()
// Map service errors to HTTP codes
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(errMsg, "session not found") {
statusCode = http.StatusNotFound
} else if strings.Contains(errMsg, "invalid") ||
strings.Contains(errMsg, "blocked") ||
strings.Contains(errMsg, "expired") ||
strings.Contains(errMsg, "mismatched") {
statusCode = http.StatusUnauthorized
}
resp = response.JSONResponse{
Success: false,
Error: errMsg,
}
ctx.JSON(statusCode, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Access token renewed successfully",
Data: renewAccessTokenResponse{
AccessToken: result.AccessToken,
AccessTokenExpiresAt: result.AccessTokenExpiresAt,
},
}
ctx.JSON(http.StatusOK, resp)
}
+429
View File
@@ -0,0 +1,429 @@
package api
import (
"errors"
"net/http"
"strings"
"time"
_ "github.com/FernandoJVideira/simple_bank/docs"
"github.com/FernandoJVideira/simple_bank/internal/auth"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/handlers/http/response"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type createUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=6"`
FullName string `json:"full_name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
type userResponse struct {
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
}
func newUserResponse(user db.User) userResponse {
return userResponse{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
PasswordChangedAt: user.PasswordChangedAt,
CreatedAt: user.CreatedAt,
}
}
// CreateUser godoc
// @Summary Create a new user
// @Description Register a new user account
// @Tags auth
// @Accept json
// @Produce json
// @Param request body createUserRequest true "User registration details"
// @Success 201 {object} response.JSONResponse{data=userResponse}
// @Failure 400 {object} response.JSONResponse
// @Failure 500 {object} response.JSONResponse
// @Router /v1/auth/register [post]
func (s *Server) createUser(ctx *gin.Context) {
var req createUserRequest
var resp response.JSONResponse
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
result, err := s.userService.CreateUser(ctx, service.RegisterUserRequest{
Username: req.Username,
Password: req.Password,
FullName: req.FullName,
Email: req.Email,
})
if err != nil {
errMessage := err.Error()
if db.ErrorCode(err) == db.UniqueViolation {
resp = response.JSONResponse{
Success: false,
Error: errMessage,
}
ctx.JSON(http.StatusForbidden, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User created successfully",
Data: newUserResponse(result.User),
}
ctx.JSON(http.StatusCreated, resp)
}
type getUserRequest struct {
Username string `uri:"username" binding:"required,alphanum"`
}
func (s *Server) getUser(ctx *gin.Context) {
var req getUserRequest
var resp response.JSONResponse
if err := ctx.ShouldBindUri(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
user, err := s.userService.GetUser(ctx, req.Username)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User retrieved successfully",
Data: newUserResponse(*user),
}
ctx.JSON(http.StatusOK, resp)
}
func (s *Server) getCurrentUser(ctx *gin.Context) {
var resp response.JSONResponse
authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload)
user, err := s.userService.GetUser(ctx, authPayload.Username)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Current user retrieved successfully",
Data: newUserResponse(*user),
}
ctx.JSON(http.StatusOK, resp)
}
type UpdateUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
FullName string `json:"full_name" binding:"omitempty,min=3"`
Email string `json:"email" binding:"omitempty,email"`
}
func (s *Server) updateUser(ctx *gin.Context) {
var req UpdateUserRequest
var resp response.JSONResponse
authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload)
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
if authPayload.Username != req.Username {
resp = response.JSONResponse{
Success: false,
Error: "Cannot update other user's information",
}
ctx.JSON(http.StatusForbidden, resp)
return
}
params := service.UserUpdateRequest{
Username: strings.TrimSpace(req.Username),
}
if strings.TrimSpace(req.FullName) != "" {
params.FullName = &req.FullName
}
if strings.TrimSpace(req.Email) != "" {
params.Email = &req.Email
}
user, err := s.userService.UpdateUser(ctx, params)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User updated successfully",
Data: newUserResponse(*user),
}
ctx.JSON(http.StatusOK, resp)
}
type changePasswordRequest struct {
CurrentPassword string `json:"current_password" binding:"required,min=6"`
NewPassword string `json:"new_password" binding:"required,min=6"`
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=NewPassword"`
}
func (s *Server) changePassword(ctx *gin.Context) {
var req changePasswordRequest
var resp response.JSONResponse
authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload)
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
updUser, err := s.userService.UpdatePassword(ctx, service.PasswordUpdateRequest{
Username: authPayload.Username,
OldPassword: req.CurrentPassword,
NewPassword: req.NewPassword,
})
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Password changed successfully",
Data: newUserResponse(*updUser),
}
ctx.JSON(http.StatusOK, resp)
}
type loginUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=6"`
}
type loginUserResponse struct {
SessionID uuid.UUID `json:"session_id"`
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`
User userResponse `json:"user"`
}
func (s *Server) loginUser(ctx *gin.Context) {
var req loginUserRequest
var resp response.JSONResponse
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
loginRes, err := s.authService.Login(ctx, service.LoginCredentials{
Username: req.Username,
Password: req.Password,
UserAgent: ctx.Request.UserAgent(),
ClientIP: ctx.ClientIP(),
})
if err != nil {
statusCode := http.StatusInternalServerError
errMsg := err.Error()
// Map service errors to HTTP codes
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(errMsg, "user not found") {
statusCode = http.StatusNotFound
} else if strings.Contains(errMsg, "incorrect username or password") {
statusCode = http.StatusUnauthorized
}
resp = response.JSONResponse{
Success: false,
Error: errMsg,
}
ctx.JSON(statusCode, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User logged in successfully",
Data: loginUserResponse{
SessionID: loginRes.SessionID,
AccessToken: loginRes.AccessToken,
AccessTokenExpiresAt: loginRes.AccessTokenExpiresAt,
RefreshToken: loginRes.RefreshToken,
RefreshTokenExpiresAt: loginRes.RefreshTokenExpiresAt,
User: newUserResponse(loginRes.User),
},
}
ctx.JSON(http.StatusOK, resp)
}
type verifyEmailRequest struct {
ID int64 `form:"id" binding:"required,min=1"`
Code string `form:"code" binding:"required,min=32,max=128"`
}
type verifyEmailResponse struct {
IsVerified bool `json:"is_verified"`
}
func (s *Server) verifyEmail(ctx *gin.Context) {
var req verifyEmailRequest
var resp response.JSONResponse
if err := ctx.ShouldBindQuery(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
result, err := s.userService.VerifyEmail(ctx, service.EmailVerificationRequest{
EmailID: req.ID,
SecretCode: req.Code,
})
if err != nil {
if errors.Is(err, db.ErrForeignKeyViolation) {
resp = response.JSONResponse{
Success: false,
Error: "Email verification record not found",
}
ctx.JSON(http.StatusForbidden, resp)
return
}
if errors.Is(err, db.ErrCheckViolation) {
resp = response.JSONResponse{
Success: false,
Error: "Invalid verification code",
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Email verified successfully",
Data: verifyEmailResponse{
IsVerified: result.User.IsEmailVerified,
},
}
ctx.JSON(http.StatusOK, resp)
}
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more