feat: 0.0.1
This commit is contained in:
146 files changed
+10444
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Auth
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
meta {
|
||||||
|
name: Get User
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{uri
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Users
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: HTTP
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
@@ -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
@@ -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" ]
|
||||||
@@ -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
|
||||||
@@ -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.).
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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");
|
||||||
@@ -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;
|
||||||
@@ -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 *;
|
||||||
@@ -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 *;
|
||||||
Whitespace-only changes.
@@ -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
|
||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<body>
|
||||||
|
<script src="oauth2-redirect.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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()}));
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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*/
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"swagger-ui.js","mappings":"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 truncated
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
Reference in new issue
Block a user