This commit is contained in:
40
.gitea/workflows/build.yml
Normal file
40
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Build and Release
|
||||||
|
run-name: ${{ gitea.actor }} build 🚀
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: azenkain/go-node:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create .env file
|
||||||
|
run: |
|
||||||
|
mkdir -p assets/resources
|
||||||
|
printf "%s" "${{ secrets.ENV_FILE }}" > assets/resources/.env
|
||||||
|
|
||||||
|
- name: Download Go dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Build for linux (embed env)
|
||||||
|
run: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o build/history-api ./cmd/history-api
|
||||||
|
|
||||||
|
- name: Stop and remove old containers
|
||||||
|
run: |
|
||||||
|
docker compose down || true
|
||||||
|
|
||||||
|
- name: Remove unused Docker resources
|
||||||
|
run: |
|
||||||
|
docker system prune -a --volumes -f
|
||||||
|
|
||||||
|
- name: Build and restart containers
|
||||||
|
run: |
|
||||||
|
docker compose pull
|
||||||
|
docker compose --env-file ./assets/resources/.env up -d --build
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,4 +2,6 @@
|
|||||||
pg_data
|
pg_data
|
||||||
.idea
|
.idea
|
||||||
*.log
|
*.log
|
||||||
*.env
|
*.env
|
||||||
|
*.env.dev
|
||||||
|
build
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
ENV TZ=Asia/Ho_Chi_Minh
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY build/history-api .
|
||||||
|
COPY data ./data
|
||||||
|
|
||||||
|
EXPOSE 3344
|
||||||
|
|
||||||
|
CMD ["./history-api"]
|
||||||
18
Makefile
18
Makefile
@@ -1,7 +1,10 @@
|
|||||||
DB_URL ?= postgres://history:secret@localhost:5432/history_map?sslmode=disable
|
DB_URL ?= postgres://history:secret@localhost:5432/history_map?sslmode=disable
|
||||||
APP = cmd/history-api/
|
APP_DIR = cmd/history-api
|
||||||
|
MAIN_APP = ./cmd/history-api/
|
||||||
|
MAIN_FILE = $(APP_DIR)/main.go
|
||||||
|
DOCS_DIR = docs
|
||||||
|
|
||||||
.PHONY: postgres createdb dropdb migrate-up migrate-down migrate-reset sqlc run build dev
|
.PHONY: migrate-up migrate-down migrate-reset swagger sqlc run build dev
|
||||||
|
|
||||||
migrate-up:
|
migrate-up:
|
||||||
migrate -path db/migrations -database "$(DB_URL)" up
|
migrate -path db/migrations -database "$(DB_URL)" up
|
||||||
@@ -13,13 +16,18 @@ migrate-reset:
|
|||||||
migrate -path db/migrations -database "$(DB_URL)" drop -f
|
migrate -path db/migrations -database "$(DB_URL)" drop -f
|
||||||
migrate -path db/migrations -database "$(DB_URL)" up
|
migrate -path db/migrations -database "$(DB_URL)" up
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
@echo "=> Generating Swagger docs..."
|
||||||
|
swag init -g $(MAIN_FILE) -o $(DOCS_DIR) --parseDependency --parseInternal
|
||||||
|
@echo "=> Swagger docs generated at $(DOCS_DIR)"
|
||||||
|
|
||||||
sqlc:
|
sqlc:
|
||||||
sqlc generate
|
sqlc generate
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go run $(APP)
|
@set GOARCH=amd64& set CGO_ENABLED=0&go run $(MAIN_APP)
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -o app $(APP)
|
@set GOOS=linux& set GOARCH=amd64& set CGO_ENABLED=0&go build -trimpath -ldflags="-s -w" -o build/history-api $(MAIN_APP)
|
||||||
|
|
||||||
dev: sqlc migrate-up run
|
dev: swagger sqlc migrate-up run
|
||||||
@@ -2,18 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
// _ "history-api/docs"
|
"fmt"
|
||||||
"history-api/internal/gen/sqlc"
|
_ "history-api/docs"
|
||||||
"history-api/pkg/cache"
|
"history-api/pkg/cache"
|
||||||
"history-api/pkg/config"
|
"history-api/pkg/config"
|
||||||
_ "history-api/pkg/log"
|
|
||||||
|
|
||||||
"fmt"
|
|
||||||
"history-api/pkg/database"
|
"history-api/pkg/database"
|
||||||
|
_ "history-api/pkg/log"
|
||||||
|
"history-api/pkg/mbtiles"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,15 +44,21 @@ func StartServer() {
|
|||||||
log.Error().Msg(err.Error())
|
log.Error().Msg(err.Error())
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
pool, err := database.Connect()
|
poolPg, err := database.NewPostgresqlDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg(err.Error())
|
log.Error().Msg(err.Error())
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer pool.Close()
|
defer poolPg.Close()
|
||||||
queries := sqlc.New(pool)
|
|
||||||
|
|
||||||
err = cache.Connect()
|
sqlTile, err := mbtiles.NewMBTilesDB("data/map.mbtiles")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msg(err.Error())
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer sqlTile.Close()
|
||||||
|
|
||||||
|
redisClient, err := cache.NewRedisClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg(err.Error())
|
log.Error().Msg(err.Error())
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -71,7 +75,7 @@ func StartServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverHttp := NewHttpServer()
|
serverHttp := NewHttpServer()
|
||||||
serverHttp.RegisterFiberRoutes()
|
serverHttp.SetupServer(poolPg, sqlTile, redisClient)
|
||||||
Singleton = serverHttp
|
Singleton = serverHttp
|
||||||
|
|
||||||
done := make(chan bool, 1)
|
done := make(chan bool, 1)
|
||||||
@@ -90,15 +94,25 @@ func StartServer() {
|
|||||||
log.Info().Msg("Graceful shutdown complete.")
|
log.Info().Msg("Graceful shutdown complete.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// @title Firefly Manager API
|
// @title History API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @description API to update Firefly Manager data
|
// @description This is a sample server for History API.
|
||||||
// @host localhost:3344
|
// @termsOfService http://swagger.io/terms/
|
||||||
// @BasePath /
|
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.url http://www.swagger.io/support
|
||||||
|
// @contact.email support@swagger.io
|
||||||
|
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
// @host localhost:3344
|
||||||
|
// @BasePath /
|
||||||
|
|
||||||
// @securityDefinitions.apikey BearerAuth
|
// @securityDefinitions.apikey BearerAuth
|
||||||
// @in header
|
// @in header
|
||||||
// @name Authorization
|
// @name Authorization
|
||||||
|
// @description Type "Bearer " followed by a space and JWT token.
|
||||||
func main() {
|
func main() {
|
||||||
StartServer()
|
StartServer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "history-api/internal/routes"
|
"database/sql"
|
||||||
// "history-api/internal/services"
|
_ "embed"
|
||||||
|
"history-api/docs"
|
||||||
|
"history-api/internal/controllers"
|
||||||
|
"history-api/internal/gen/sqlc"
|
||||||
|
"history-api/internal/repositories"
|
||||||
|
"history-api/internal/routes"
|
||||||
|
"history-api/internal/services"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"os"
|
||||||
|
|
||||||
swagger "github.com/gofiber/contrib/v3/swaggerui"
|
swagger "github.com/gofiber/contrib/v3/swaggerui"
|
||||||
|
middleware "github.com/gofiber/contrib/v3/zerolog"
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -27,17 +36,21 @@ func NewHttpServer() *FiberServer {
|
|||||||
}
|
}
|
||||||
cfg := swagger.Config{
|
cfg := swagger.Config{
|
||||||
BasePath: "/",
|
BasePath: "/",
|
||||||
FilePath: "./docs/swagger.json",
|
FileContent: docs.SwaggerJSON,
|
||||||
Path: "swagger",
|
Path: "swagger",
|
||||||
Title: "Swagger API Docs",
|
Title: "Swagger API Docs",
|
||||||
}
|
}
|
||||||
|
|
||||||
server.App.Use(swagger.New(cfg))
|
server.App.Use(swagger.New(cfg))
|
||||||
server.App.Use(logger.New())
|
|
||||||
|
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||||
|
server.App.Use(middleware.New(middleware.Config{
|
||||||
|
Logger: &logger,
|
||||||
|
}))
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FiberServer) RegisterFiberRoutes() {
|
func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.Cache) {
|
||||||
// Apply CORS middleware
|
// Apply CORS middleware
|
||||||
s.App.Use(cors.New(cors.Config{
|
s.App.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{"*"},
|
AllowOrigins: []string{"*"},
|
||||||
@@ -47,9 +60,21 @@ func (s *FiberServer) RegisterFiberRoutes() {
|
|||||||
MaxAge: 300,
|
MaxAge: 300,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// routes.UserRoutes(s.App)
|
// repo setup
|
||||||
// routes.AuthRoutes(s.App)
|
userRepo := repositories.NewUserRepository(sqlPg, redis)
|
||||||
// routes.MediaRoute(s.App)
|
roleRepo := repositories.NewRoleRepository(sqlPg, redis)
|
||||||
// routes.NotFoundRoute(s.App)
|
tileRepo := repositories.NewTileRepository(sqlTile, redis)
|
||||||
|
|
||||||
|
// service setup
|
||||||
|
authService := services.NewAuthService(userRepo, roleRepo)
|
||||||
|
tileService := services.NewTileService(tileRepo)
|
||||||
|
|
||||||
|
// controller setup
|
||||||
|
authController := controllers.NewAuthController(authService)
|
||||||
|
tileController := controllers.NewTileController(tileService)
|
||||||
|
|
||||||
|
// route setup
|
||||||
|
routes.AuthRoutes(s.App, authController)
|
||||||
|
routes.TileRoutes(s.App, tileController)
|
||||||
|
routes.NotFoundRoute(s.App)
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
data/land.mbtiles
Normal file
BIN
data/land.mbtiles
Normal file
Binary file not shown.
BIN
data/map.mbtiles
Normal file
BIN
data/map.mbtiles
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@ CREATE EXTENSION IF NOT EXISTS postgis;
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
email TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT,
|
||||||
google_id VARCHAR(255) UNIQUE,
|
google_id VARCHAR(255) UNIQUE,
|
||||||
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||||
is_verified BOOLEAN NOT NULL DEFAULT false,
|
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS user_verifications (
|
|||||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||||
verify_type SMALLINT NOT NULL, -- 1 = ID_CARD, 2 = EDUCATION, 3 = EXPERT
|
verify_type SMALLINT NOT NULL, -- 1 = ID_CARD, 2 = EDUCATION, 3 = EXPERT
|
||||||
document_url TEXT NOT NULL,
|
document_url TEXT NOT NULL,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
status SMALLINT NOT NULL DEFAULT 1, -- 1 pending, 2 approved, 3 rejected
|
status SMALLINT NOT NULL DEFAULT 1, -- 1 pending, 2 approved, 3 rejected
|
||||||
reviewed_by UUID REFERENCES users(id),
|
reviewed_by UUID REFERENCES users(id),
|
||||||
reviewed_at TIMESTAMPTZ,
|
reviewed_at TIMESTAMPTZ,
|
||||||
@@ -10,9 +11,18 @@ CREATE TABLE IF NOT EXISTS user_verifications (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
CREATE INDEX idx_user_verifications_user_id ON user_verifications(user_id);
|
CREATE INDEX idx_user_verifications_user_id
|
||||||
CREATE INDEX idx_user_verifications_user_type ON user_verifications(user_id, verify_type);
|
ON user_verifications(user_id)
|
||||||
CREATE INDEX idx_user_verifications_status ON user_verifications(status);
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_verifications_user_type
|
||||||
|
ON user_verifications(user_id, verify_type)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_verifications_status
|
||||||
|
ON user_verifications(status)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
CREATE INDEX idx_user_verifications_status_created
|
CREATE INDEX idx_user_verifications_status_created
|
||||||
ON user_verifications(status, created_at DESC);
|
ON user_verifications(status, created_at DESC)
|
||||||
|
WHERE is_deleted = false;
|
||||||
@@ -3,18 +3,23 @@ CREATE TABLE IF NOT EXISTS user_tokens (
|
|||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
token VARCHAR(255) NOT NULL UNIQUE,
|
token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
token_type SMALLINT NOT NULL,
|
token_type SMALLINT NOT NULL,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_user_tokens_token
|
CREATE INDEX idx_user_tokens_token
|
||||||
ON user_tokens(token);
|
ON user_tokens(token)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
CREATE INDEX idx_user_tokens_user_id
|
CREATE INDEX idx_user_tokens_user_id
|
||||||
ON user_tokens(user_id);
|
ON user_tokens(user_id)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
CREATE INDEX idx_user_tokens_type
|
CREATE INDEX idx_user_tokens_type
|
||||||
ON user_tokens(token_type);
|
ON user_tokens(token_type)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
CREATE INDEX idx_user_tokens_expires_at
|
CREATE INDEX idx_user_tokens_expires_at
|
||||||
ON user_tokens(expires_at);
|
ON user_tokens(expires_at)
|
||||||
|
WHERE is_deleted = false;
|
||||||
@@ -11,11 +11,38 @@ CREATE TABLE IF NOT EXISTS entities (
|
|||||||
description TEXT,
|
description TEXT,
|
||||||
thumbnail_url TEXT,
|
thumbnail_url TEXT,
|
||||||
status SMALLINT DEFAULT 1, -- 1 draft, 2 published
|
status SMALLINT DEFAULT 1, -- 1 draft, 2 published
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
reviewed_by UUID REFERENCES users(id),
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT now()
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_entities_slug ON entities(slug);
|
CREATE UNIQUE INDEX uniq_entities_slug_active
|
||||||
|
ON entities(slug)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_entities_type
|
||||||
|
ON entities(type_id)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX idx_entities_status_created
|
||||||
|
ON entities(status, created_at DESC)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX idx_entities_type_status
|
||||||
|
ON entities(type_id, status)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX idx_entities_reviewed_by
|
||||||
|
ON entities(reviewed_by)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_entities_name_search
|
||||||
|
ON entities USING gin (name gin_trgm_ops);
|
||||||
|
|
||||||
CREATE TRIGGER trigger_entities_updated_at
|
CREATE TRIGGER trigger_entities_updated_at
|
||||||
BEFORE UPDATE ON entities
|
BEFORE UPDATE ON entities
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
CREATE TABLE IF NOT EXISTS wiki_pages (
|
CREATE TABLE IF NOT EXISTS wikis (
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
|
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
note TEXT,
|
||||||
content TEXT,
|
content TEXT,
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT now()
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wiki_versions (
|
CREATE INDEX idx_wiki_entity
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
ON wikis(entity_id)
|
||||||
wiki_id UUID REFERENCES wiki_pages(id) ON DELETE CASCADE,
|
WHERE is_deleted = false;
|
||||||
created_user UUID REFERENCES users(id),
|
|
||||||
note TEXT,
|
|
||||||
content TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
approved_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_wiki_entity ON wiki_pages(entity_id);
|
CREATE TRIGGER trigger_wikis_updated_at
|
||||||
|
BEFORE UPDATE ON wikis
|
||||||
CREATE TRIGGER trigger_wiki_pages_updated_at
|
|
||||||
BEFORE UPDATE ON wiki_pages
|
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION update_updated_at();
|
EXECUTE FUNCTION update_updated_at();
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS geometries (
|
CREATE TABLE IF NOT EXISTS geometries (
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
geom GEOMETRY, -- point / polygon / line
|
geom GEOMETRY, -- point / polygon / line
|
||||||
time_start INT,
|
time_start INT,
|
||||||
time_end INT,
|
time_end INT,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
bbox GEOMETRY, -- optional
|
bbox GEOMETRY, -- optional
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT now()
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
@@ -13,9 +16,11 @@ CREATE TABLE IF NOT EXISTS geo_versions (
|
|||||||
geo_id UUID REFERENCES geometries(id) ON DELETE CASCADE,
|
geo_id UUID REFERENCES geometries(id) ON DELETE CASCADE,
|
||||||
created_user UUID REFERENCES users(id),
|
created_user UUID REFERENCES users(id),
|
||||||
geom GEOMETRY,
|
geom GEOMETRY,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
reviewed_by UUID REFERENCES users(id),
|
||||||
approved_at TIMESTAMPTZ
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS entity_geometries (
|
CREATE TABLE IF NOT EXISTS entity_geometries (
|
||||||
@@ -24,8 +29,33 @@ CREATE TABLE IF NOT EXISTS entity_geometries (
|
|||||||
PRIMARY KEY (entity_id, geometry_id)
|
PRIMARY KEY (entity_id, geometry_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_geo_time ON geometries(time_start, time_end);
|
CREATE INDEX idx_geom_spatial_active
|
||||||
CREATE INDEX idx_geom_spatial ON geometries USING GIST (geom);
|
ON geometries USING GIST (geom)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_geom_bbox
|
||||||
|
ON geometries USING GIST (bbox)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_geom_time_range
|
||||||
|
ON geometries
|
||||||
|
USING GIST (int4range(time_start, time_end))
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_geo_versions_geo_id
|
||||||
|
ON geo_versions(geo_id)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_geo_versions_reviewed_by
|
||||||
|
ON geo_versions(reviewed_by)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_geo_versions_created_at
|
||||||
|
ON geo_versions(created_at DESC)
|
||||||
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_entity_geometries_geometry
|
||||||
|
ON entity_geometries(geometry_id);
|
||||||
|
|
||||||
CREATE TRIGGER trigger_geometries_updated_at
|
CREATE TRIGGER trigger_geometries_updated_at
|
||||||
BEFORE UPDATE ON geometries
|
BEFORE UPDATE ON geometries
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
-- name: CreateUser :one
|
-- name: UpsertUser :one
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
name,
|
|
||||||
email,
|
email,
|
||||||
password_hash,
|
password_hash,
|
||||||
|
google_id,
|
||||||
|
auth_provider,
|
||||||
|
is_verified
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5
|
||||||
|
)
|
||||||
|
ON CONFLICT (email)
|
||||||
|
DO UPDATE SET
|
||||||
|
google_id = EXCLUDED.google_id,
|
||||||
|
auth_provider = EXCLUDED.auth_provider,
|
||||||
|
is_verified = users.is_verified OR EXCLUDED.is_verified,
|
||||||
|
updated_at = now()
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: CreateUserProfile :one
|
||||||
|
INSERT INTO user_profiles (
|
||||||
|
user_id,
|
||||||
|
display_name,
|
||||||
avatar_url
|
avatar_url
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4
|
$1, $2, $3
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: UpdateUser :one
|
-- name: UpdateUserProfile :one
|
||||||
UPDATE users
|
UPDATE user_profiles
|
||||||
SET
|
SET
|
||||||
name = $1,
|
display_name = $1,
|
||||||
avatar_url = $2,
|
full_name = $2,
|
||||||
is_active = $3,
|
avatar_url = $3,
|
||||||
is_verified = $4,
|
bio = $4,
|
||||||
|
location = $5,
|
||||||
|
website = $6,
|
||||||
|
country_code = $7,
|
||||||
|
phone = $8,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE users.id = $5 AND users.is_deleted = false
|
WHERE user_id = $9
|
||||||
RETURNING
|
RETURNING *;
|
||||||
users.id,
|
|
||||||
users.name,
|
|
||||||
users.email,
|
|
||||||
users.password_hash,
|
|
||||||
users.avatar_url,
|
|
||||||
users.is_active,
|
|
||||||
users.is_verified,
|
|
||||||
users.token_version,
|
|
||||||
users.refresh_token,
|
|
||||||
users.is_deleted,
|
|
||||||
users.created_at,
|
|
||||||
users.updated_at,
|
|
||||||
(
|
|
||||||
SELECT COALESCE(json_agg(json_build_object('id', roles.id, 'name', roles.name)), '[]')::json
|
|
||||||
FROM user_roles
|
|
||||||
JOIN roles ON user_roles.role_id = roles.id
|
|
||||||
WHERE user_roles.user_id = users.id
|
|
||||||
) AS roles;
|
|
||||||
|
|
||||||
-- name: UpdateUserPassword :exec
|
-- name: UpdateUserPassword :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
@@ -52,7 +55,6 @@ SET
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND is_deleted = false;
|
AND is_deleted = false;
|
||||||
|
|
||||||
|
|
||||||
-- name: VerifyUser :exec
|
-- name: VerifyUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
@@ -72,86 +74,133 @@ SET
|
|||||||
is_deleted = false
|
is_deleted = false
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: ExistsUserByEmail :one
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1 FROM users
|
|
||||||
WHERE email = $1
|
|
||||||
AND is_deleted = false
|
|
||||||
);
|
|
||||||
|
|
||||||
-- name: GetUsers :many
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
u.name,
|
|
||||||
u.email,
|
|
||||||
u.password_hash,
|
|
||||||
u.avatar_url,
|
|
||||||
u.is_active,
|
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
|
||||||
u.refresh_token,
|
|
||||||
u.is_deleted,
|
|
||||||
u.created_at,
|
|
||||||
u.updated_at,
|
|
||||||
COALESCE(
|
|
||||||
json_agg(
|
|
||||||
json_build_object('id', r.id, 'name', r.name)
|
|
||||||
) FILTER (WHERE r.id IS NOT NULL),
|
|
||||||
'[]'
|
|
||||||
)::json AS roles
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
|
||||||
LEFT JOIN roles r ON ur.role_id = r.id
|
|
||||||
WHERE u.is_deleted = false
|
|
||||||
GROUP BY u.id;
|
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
-- name: GetUserByID :one
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.name,
|
u.email,
|
||||||
u.email,
|
u.password_hash,
|
||||||
u.password_hash,
|
u.is_verified,
|
||||||
u.avatar_url,
|
u.token_version,
|
||||||
u.is_active,
|
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
|
||||||
u.refresh_token,
|
u.refresh_token,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at,
|
u.updated_at,
|
||||||
COALESCE(
|
|
||||||
json_agg(
|
-- profile JSON
|
||||||
json_build_object('id', r.id, 'name', r.name)
|
(
|
||||||
) FILTER (WHERE r.id IS NOT NULL),
|
SELECT json_build_object(
|
||||||
'[]'
|
'display_name', p.display_name,
|
||||||
)::json AS roles
|
'full_name', p.full_name,
|
||||||
|
'avatar_url', p.avatar_url,
|
||||||
|
'bio', p.bio,
|
||||||
|
'location', p.location,
|
||||||
|
'website', p.website,
|
||||||
|
'country_code', p.country_code,
|
||||||
|
'phone', p.phone
|
||||||
|
)
|
||||||
|
FROM user_profiles p
|
||||||
|
WHERE p.user_id = u.id
|
||||||
|
) AS profile,
|
||||||
|
|
||||||
|
-- roles JSON
|
||||||
|
(
|
||||||
|
SELECT COALESCE(
|
||||||
|
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||||
|
'[]'
|
||||||
|
)::json
|
||||||
|
FROM user_roles ur
|
||||||
|
JOIN roles r ON ur.role_id = r.id
|
||||||
|
WHERE ur.user_id = u.id
|
||||||
|
) AS roles
|
||||||
|
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
WHERE u.id = $1 AND u.is_deleted = false;
|
||||||
LEFT JOIN roles r ON ur.role_id = r.id
|
|
||||||
WHERE u.id = $1 AND u.is_deleted = false
|
-- name: GetTokenVersion :one
|
||||||
GROUP BY u.id;
|
SELECT token_version
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1 AND is_deleted = false;
|
||||||
|
|
||||||
|
-- name: UpdateTokenVersion :exec
|
||||||
|
UPDATE users
|
||||||
|
SET token_version = $2
|
||||||
|
WHERE id = $1 AND is_deleted = false;
|
||||||
|
|
||||||
-- name: GetUserByEmail :one
|
-- name: GetUserByEmail :one
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.name,
|
u.email,
|
||||||
u.email,
|
u.password_hash,
|
||||||
u.password_hash,
|
u.is_verified,
|
||||||
u.avatar_url,
|
u.token_version,
|
||||||
u.is_active,
|
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at,
|
u.updated_at,
|
||||||
COALESCE(
|
|
||||||
json_agg(
|
(
|
||||||
json_build_object('id', r.id, 'name', r.name)
|
SELECT json_build_object(
|
||||||
) FILTER (WHERE r.id IS NOT NULL),
|
'display_name', p.display_name,
|
||||||
'[]'
|
'full_name', p.full_name,
|
||||||
)::json AS roles
|
'avatar_url', p.avatar_url,
|
||||||
|
'bio', p.bio,
|
||||||
|
'location', p.location,
|
||||||
|
'website', p.website,
|
||||||
|
'country_code', p.country_code,
|
||||||
|
'phone', p.phone
|
||||||
|
)
|
||||||
|
FROM user_profiles p
|
||||||
|
WHERE p.user_id = u.id
|
||||||
|
) AS profile,
|
||||||
|
|
||||||
|
(
|
||||||
|
SELECT COALESCE(
|
||||||
|
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||||
|
'[]'
|
||||||
|
)::json
|
||||||
|
FROM user_roles ur
|
||||||
|
JOIN roles r ON ur.role_id = r.id
|
||||||
|
WHERE ur.user_id = u.id
|
||||||
|
) AS roles
|
||||||
|
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
WHERE u.email = $1 AND u.is_deleted = false;
|
||||||
LEFT JOIN roles r ON ur.role_id = r.id
|
|
||||||
WHERE u.email = $1 AND u.is_deleted = false
|
-- name: GetUsers :many
|
||||||
GROUP BY u.id;
|
SELECT
|
||||||
|
u.id,
|
||||||
|
u.email,
|
||||||
|
u.password_hash,
|
||||||
|
u.is_verified,
|
||||||
|
u.token_version,
|
||||||
|
u.refresh_token,
|
||||||
|
u.is_deleted,
|
||||||
|
u.created_at,
|
||||||
|
u.updated_at,
|
||||||
|
|
||||||
|
(
|
||||||
|
SELECT json_build_object(
|
||||||
|
'display_name', p.display_name,
|
||||||
|
'full_name', p.full_name,
|
||||||
|
'avatar_url', p.avatar_url,
|
||||||
|
'bio', p.bio,
|
||||||
|
'location', p.location,
|
||||||
|
'website', p.website,
|
||||||
|
'country_code', p.country_code,
|
||||||
|
'phone', p.phone
|
||||||
|
)
|
||||||
|
FROM user_profiles p
|
||||||
|
WHERE p.user_id = u.id
|
||||||
|
) AS profile,
|
||||||
|
|
||||||
|
(
|
||||||
|
SELECT COALESCE(
|
||||||
|
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||||
|
'[]'
|
||||||
|
)::json
|
||||||
|
FROM user_roles ur
|
||||||
|
JOIN roles r ON ur.role_id = r.id
|
||||||
|
WHERE ur.user_id = u.id
|
||||||
|
) AS roles
|
||||||
|
|
||||||
|
FROM users u
|
||||||
|
WHERE u.is_deleted = false;
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
email TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT,
|
||||||
google_id VARCHAR(255) UNIQUE,
|
google_id VARCHAR(255) UNIQUE,
|
||||||
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||||
is_verified BOOLEAN DEFAULT false,
|
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
token_version INT NOT NULL DEFAULT 1,
|
token_version INT NOT NULL DEFAULT 1,
|
||||||
refresh_token TEXT,
|
refresh_token TEXT,
|
||||||
is_deleted BOOLEAN DEFAULT false,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT now()
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE user_profiles (
|
CREATE TABLE IF NOT EXISTS user_profiles (
|
||||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
display_name TEXT,
|
display_name TEXT,
|
||||||
full_name TEXT,
|
full_name TEXT,
|
||||||
@@ -26,21 +26,10 @@ CREATE TABLE user_profiles (
|
|||||||
updated_at TIMESTAMPTZ DEFAULT now()
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE user_verifications (
|
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
||||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
verify_type SMALLINT NOT NULL, -- 1 = ID_CARD, 2 = EDUCATION, 3 = EXPERT
|
|
||||||
document_url TEXT NOT NULL,
|
|
||||||
status SMALLINT NOT NULL DEFAULT 1, -- 1 pending, 2 approved, 3 rejected
|
|
||||||
reviewed_by UUID REFERENCES users(id),
|
|
||||||
reviewed_at TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS roles (
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
name TEXT UNIQUE NOT NULL,
|
name TEXT UNIQUE NOT NULL,
|
||||||
is_deleted BOOLEAN DEFAULT false,
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT now()
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
@@ -51,11 +40,23 @@ CREATE TABLE IF NOT EXISTS user_roles (
|
|||||||
PRIMARY KEY (user_id, role_id)
|
PRIMARY KEY (user_id, role_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_verifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
verify_type SMALLINT NOT NULL, -- 1 = ID_CARD, 2 = EDUCATION, 3 = EXPERT
|
||||||
|
document_url TEXT NOT NULL,
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1, -- 1 pending, 2 approved, 3 rejected
|
||||||
|
reviewed_by UUID REFERENCES users(id),
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_tokens (
|
CREATE TABLE IF NOT EXISTS user_tokens (
|
||||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
token VARCHAR(255) NOT NULL UNIQUE, --1 = PasswordReset, 2 = EmailVerify, 3 = MagicLink, 4 = RefreshToken
|
token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
token_type SMALLINT NOT NULL,
|
token_type SMALLINT NOT NULL,
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|||||||
29
docker-compose-dev.yml
Normal file
29
docker-compose-dev.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgis/postgis:18-3.6
|
||||||
|
container_name: history_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: history
|
||||||
|
POSTGRES_PASSWORD: secret
|
||||||
|
POSTGRES_DB: history_map
|
||||||
|
PGDATA: /var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- ./pg_data:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U history -d history_map"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
cache:
|
||||||
|
image: redis:8.6.1-alpine
|
||||||
|
container_name: history_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
||||||
@@ -3,27 +3,71 @@ services:
|
|||||||
image: postgis/postgis:18-3.6
|
image: postgis/postgis:18-3.6
|
||||||
container_name: history_db
|
container_name: history_db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./assets/resources/.env
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: history
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: secret
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: history_map
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
PGDATA: /var/lib/postgresql/data
|
- PGDATA=/var/lib/postgresql/data
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./pg_data:/var/lib/postgresql
|
- pg_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U history -d history_map"]
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- history-api-project
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
image: redis:8.6.1-alpine
|
image: redis:8.6.1-alpine
|
||||||
container_name: history_redis
|
container_name: history_redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- history-api-project
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
image: migrate/migrate
|
||||||
|
container_name: history_migrate
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ./assets/resources/.env
|
||||||
|
volumes:
|
||||||
|
- ./db/migrations:/migrations
|
||||||
|
# Sử dụng sh -c để có thể chạy chuỗi lệnh
|
||||||
|
entrypoint:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
# Lấy URL database từ biến môi trường
|
||||||
|
DB_URL="postgres://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@db:5432/$${POSTGRES_DB}?sslmode=disable"
|
||||||
|
|
||||||
|
echo "Checking for dirty database..."
|
||||||
|
/migrate -path /migrations -database "$$DB_URL" up || \
|
||||||
|
(echo "Database is dirty, forcing to version 8..." && \
|
||||||
|
/migrate -path /migrations -database "$$DB_URL" force 8 && \
|
||||||
|
/migrate -path /migrations -database "$$DB_URL" up)
|
||||||
|
networks:
|
||||||
|
- history-api-project
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: history_app
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
cache:
|
||||||
|
condition: service_started
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "3344:3344"
|
||||||
|
networks:
|
||||||
|
- history-api-project
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
history-api-project:
|
||||||
322
docs/docs.go
Normal file
322
docs/docs.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
// 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",
|
||||||
|
"url": "http://www.swagger.io/support",
|
||||||
|
"email": "support@swagger.io"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {
|
||||||
|
"/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get a new access token using the user's current session/refresh token",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Refresh access token",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/signin": {
|
||||||
|
"post": {
|
||||||
|
"description": "Authenticate user and return token data",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Sign in an existing user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Sign In request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_request.SignInDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/signup": {
|
||||||
|
"post": {
|
||||||
|
"description": "Create a new user account",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Sign up a new user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Sign Up request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_request.SignUpDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/tiles/metadata": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve map metadata",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tile"
|
||||||
|
],
|
||||||
|
"summary": "Get tile metadata",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/tiles/{z}/{x}/{y}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Fetch vector or raster map tile data by Z, X, Y coordinates",
|
||||||
|
"produces": [
|
||||||
|
"application/octet-stream"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tile"
|
||||||
|
],
|
||||||
|
"summary": "Get a map tile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Zoom level (0-22)",
|
||||||
|
"name": "z",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "X coordinate",
|
||||||
|
"name": "x",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Y coordinate",
|
||||||
|
"name": "y",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"history-api_internal_dtos_request.SignInDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 5
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64,
|
||||||
|
"minLength": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"history-api_internal_dtos_request.SignUpDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"display_name",
|
||||||
|
"email",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"display_name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 50,
|
||||||
|
"minLength": 2
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 5
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64,
|
||||||
|
"minLength": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"history-api_internal_dtos_response.CommonResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"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:3344",
|
||||||
|
BasePath: "/",
|
||||||
|
Schemes: []string{},
|
||||||
|
Title: "History API",
|
||||||
|
Description: "This is a sample server for History API.",
|
||||||
|
InfoInstanceName: "swagger",
|
||||||
|
SwaggerTemplate: docTemplate,
|
||||||
|
LeftDelim: "{{",
|
||||||
|
RightDelim: "}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||||
|
}
|
||||||
6
docs/embed.go
Normal file
6
docs/embed.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package docs
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed swagger.json
|
||||||
|
var SwaggerJSON []byte
|
||||||
298
docs/swagger.json
Normal file
298
docs/swagger.json
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "This is a sample server for History API.",
|
||||||
|
"title": "History API",
|
||||||
|
"termsOfService": "http://swagger.io/terms/",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"url": "http://www.swagger.io/support",
|
||||||
|
"email": "support@swagger.io"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
"host": "localhost:3344",
|
||||||
|
"basePath": "/",
|
||||||
|
"paths": {
|
||||||
|
"/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get a new access token using the user's current session/refresh token",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Refresh access token",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/signin": {
|
||||||
|
"post": {
|
||||||
|
"description": "Authenticate user and return token data",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Sign in an existing user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Sign In request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_request.SignInDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/signup": {
|
||||||
|
"post": {
|
||||||
|
"description": "Create a new user account",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Sign up a new user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Sign Up request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_request.SignUpDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/tiles/metadata": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve map metadata",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tile"
|
||||||
|
],
|
||||||
|
"summary": "Get tile metadata",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/tiles/{z}/{x}/{y}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Fetch vector or raster map tile data by Z, X, Y coordinates",
|
||||||
|
"produces": [
|
||||||
|
"application/octet-stream"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Tile"
|
||||||
|
],
|
||||||
|
"summary": "Get a map tile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Zoom level (0-22)",
|
||||||
|
"name": "z",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "X coordinate",
|
||||||
|
"name": "x",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Y coordinate",
|
||||||
|
"name": "y",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"history-api_internal_dtos_request.SignInDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 5
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64,
|
||||||
|
"minLength": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"history-api_internal_dtos_request.SignUpDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"display_name",
|
||||||
|
"email",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"display_name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 50,
|
||||||
|
"minLength": 2
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 5
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64,
|
||||||
|
"minLength": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"history-api_internal_dtos_response.CommonResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securityDefinitions": {
|
||||||
|
"BearerAuth": {
|
||||||
|
"description": "Type \"Bearer \" followed by a space and JWT token.",
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
docs/swagger.yaml
Normal file
202
docs/swagger.yaml
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
basePath: /
|
||||||
|
definitions:
|
||||||
|
history-api_internal_dtos_request.SignInDto:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 5
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
maxLength: 64
|
||||||
|
minLength: 8
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
history-api_internal_dtos_request.SignUpDto:
|
||||||
|
properties:
|
||||||
|
display_name:
|
||||||
|
maxLength: 50
|
||||||
|
minLength: 2
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 5
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
maxLength: 64
|
||||||
|
minLength: 8
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- display_name
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
history-api_internal_dtos_response.CommonResponse:
|
||||||
|
properties:
|
||||||
|
data: {}
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
host: localhost:3344
|
||||||
|
info:
|
||||||
|
contact:
|
||||||
|
email: support@swagger.io
|
||||||
|
name: API Support
|
||||||
|
url: http://www.swagger.io/support
|
||||||
|
description: This is a sample server for History API.
|
||||||
|
license:
|
||||||
|
name: Apache 2.0
|
||||||
|
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
termsOfService: http://swagger.io/terms/
|
||||||
|
title: History API
|
||||||
|
version: "1.0"
|
||||||
|
paths:
|
||||||
|
/auth/refresh:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get a new access token using the user's current session/refresh
|
||||||
|
token
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Refresh access token
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
/auth/signin:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Authenticate user and return token data
|
||||||
|
parameters:
|
||||||
|
- description: Sign In request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_request.SignInDto'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
summary: Sign in an existing user
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
/auth/signup:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Create a new user account
|
||||||
|
parameters:
|
||||||
|
- description: Sign Up request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_request.SignUpDto'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
summary: Sign up a new user
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
/tiles/{z}/{x}/{y}:
|
||||||
|
get:
|
||||||
|
description: Fetch vector or raster map tile data by Z, X, Y coordinates
|
||||||
|
parameters:
|
||||||
|
- description: Zoom level (0-22)
|
||||||
|
in: path
|
||||||
|
name: z
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: X coordinate
|
||||||
|
in: path
|
||||||
|
name: x
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Y coordinate
|
||||||
|
in: path
|
||||||
|
name: "y"
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/octet-stream
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
type: file
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
summary: Get a map tile
|
||||||
|
tags:
|
||||||
|
- Tile
|
||||||
|
/tiles/metadata:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve map metadata
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
summary: Get tile metadata
|
||||||
|
tags:
|
||||||
|
- Tile
|
||||||
|
securityDefinitions:
|
||||||
|
BearerAuth:
|
||||||
|
description: Type "Bearer " followed by a space and JWT token.
|
||||||
|
in: header
|
||||||
|
name: Authorization
|
||||||
|
type: apiKey
|
||||||
|
swagger: "2.0"
|
||||||
26
go.mod
26
go.mod
@@ -3,21 +3,28 @@ module history-api
|
|||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/glebarez/go-sqlite v1.22.0
|
||||||
github.com/go-playground/validator/v10 v10.30.1
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
github.com/gofiber/contrib/v3/jwt v1.1.0
|
github.com/gofiber/contrib/v3/jwt v1.1.0
|
||||||
github.com/gofiber/contrib/v3/swaggerui v1.0.1
|
github.com/gofiber/contrib/v3/swaggerui v1.0.1
|
||||||
|
github.com/gofiber/contrib/v3/zerolog v1.0.1
|
||||||
github.com/gofiber/fiber/v3 v3.1.0
|
github.com/gofiber/fiber/v3 v3.1.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/jackc/pgx/v5 v5.8.0
|
github.com/jackc/pgx/v5 v5.8.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/redis/go-redis/v9 v9.18.0
|
github.com/redis/go-redis/v9 v9.18.0
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
|
golang.org/x/crypto v0.49.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/go-openapi/analysis v0.24.2 // indirect
|
github.com/go-openapi/analysis v0.24.2 // indirect
|
||||||
github.com/go-openapi/errors v0.22.6 // indirect
|
github.com/go-openapi/errors v0.22.6 // indirect
|
||||||
@@ -42,28 +49,33 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/gofiber/schema v1.7.0 // indirect
|
github.com/gofiber/schema v1.7.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/oklog/ulid v1.3.1 // indirect
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
modernc.org/libc v1.37.6 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/sqlite v1.28.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
55
go.sum
55
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
|
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
@@ -15,10 +17,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50=
|
github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50=
|
||||||
github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
|
github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
|
||||||
github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo=
|
github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo=
|
||||||
@@ -35,6 +41,7 @@ github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQ
|
|||||||
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||||
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
|
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
|
||||||
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
|
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||||
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
|
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
|
||||||
@@ -76,6 +83,8 @@ github.com/gofiber/contrib/v3/jwt v1.1.0 h1:RmQHJGNlOF+1hy69AJo+YiVcLTUKA2bXy/yj
|
|||||||
github.com/gofiber/contrib/v3/jwt v1.1.0/go.mod h1:THoVk0kTAkLJFunaNxk8fRO5WYfrwV4k/8oFAoa7UCM=
|
github.com/gofiber/contrib/v3/jwt v1.1.0/go.mod h1:THoVk0kTAkLJFunaNxk8fRO5WYfrwV4k/8oFAoa7UCM=
|
||||||
github.com/gofiber/contrib/v3/swaggerui v1.0.1 h1:o3EdD0VQjeL4rq1gBxQB5bUEYMNT3eGKxpg2hjPIigI=
|
github.com/gofiber/contrib/v3/swaggerui v1.0.1 h1:o3EdD0VQjeL4rq1gBxQB5bUEYMNT3eGKxpg2hjPIigI=
|
||||||
github.com/gofiber/contrib/v3/swaggerui v1.0.1/go.mod h1:tIqJ2SDnY7AfqsJHllyaF2vuhkKeBBCHtwWsZrFLCkk=
|
github.com/gofiber/contrib/v3/swaggerui v1.0.1/go.mod h1:tIqJ2SDnY7AfqsJHllyaF2vuhkKeBBCHtwWsZrFLCkk=
|
||||||
|
github.com/gofiber/contrib/v3/zerolog v1.0.1 h1:Sr9T4g0Z1DAOoo5X0tAKFd9uNKFu++vwaZOk1ymIq1s=
|
||||||
|
github.com/gofiber/contrib/v3/zerolog v1.0.1/go.mod h1:LgfsEgvOp0Yg4ra77kr3Md3UyQ2BjPO6T8LNV/gkCi4=
|
||||||
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
|
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
|
||||||
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
|
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
|
||||||
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
|
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
|
||||||
@@ -86,6 +95,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
|
|||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
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/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@@ -98,10 +109,10 @@ 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/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -125,6 +136,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
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/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
@@ -137,6 +150,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
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/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
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/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
@@ -155,19 +170,23 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -176,3 +195,11 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
|
||||||
|
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||||
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
|
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||||
|
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||||
|
|||||||
@@ -1 +1,122 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"history-api/internal/dtos/request"
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/services"
|
||||||
|
"history-api/pkg/validator"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthController struct {
|
||||||
|
service services.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthController(svc services.AuthService) *AuthController {
|
||||||
|
return &AuthController{service: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signin godoc
|
||||||
|
// @Summary Sign in an existing user
|
||||||
|
// @Description Authenticate user and return token data
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body request.SignInDto true "Sign In request"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /auth/signin [post]
|
||||||
|
func (h *AuthController) Signin(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
dto := &request.SignInDto{}
|
||||||
|
|
||||||
|
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.service.Signin(ctx, dto)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signup godoc
|
||||||
|
// @Summary Sign up a new user
|
||||||
|
// @Description Create a new user account
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body request.SignUpDto true "Sign Up request"
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /auth/signup [post]
|
||||||
|
func (h *AuthController) Signup(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
dto := &request.SignUpDto{}
|
||||||
|
|
||||||
|
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.service.Signup(ctx, dto)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken godoc
|
||||||
|
// @Summary Refresh access token
|
||||||
|
// @Description Get a new access token using the user's current session/refresh token
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /auth/refresh [post]
|
||||||
|
func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
res, err := h.service.RefreshToken(ctx, c.Locals("uid").(string))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
113
internal/controllers/tileController.go
Normal file
113
internal/controllers/tileController.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/services"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TileController struct {
|
||||||
|
service services.TileService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTileController(svc services.TileService) *TileController {
|
||||||
|
return &TileController{service: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadata godoc
|
||||||
|
// @Summary Get tile metadata
|
||||||
|
// @Description Retrieve map metadata
|
||||||
|
// @Tags Tile
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /tiles/metadata [get]
|
||||||
|
func (h *TileController) GetMetadata(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
res, err := h.service.GetMetadata(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTile godoc
|
||||||
|
// @Summary Get a map tile
|
||||||
|
// @Description Fetch vector or raster map tile data by Z, X, Y coordinates
|
||||||
|
// @Tags Tile
|
||||||
|
// @Produce application/octet-stream
|
||||||
|
// @Param z path int true "Zoom level (0-22)"
|
||||||
|
// @Param x path int true "X coordinate"
|
||||||
|
// @Param y path int true "Y coordinate"
|
||||||
|
// @Success 200 {file} byte
|
||||||
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 500 {object} response.CommonResponse
|
||||||
|
// @Router /tiles/{z}/{x}/{y} [get]
|
||||||
|
func (h *TileController) GetTile(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
z, x, y, err := h.parseTileParams(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data, headers, err := h.service.GetTile(ctx, z, x, y)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
c.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).Send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TileController) parseTileParams(c fiber.Ctx) (int, int, int, error) {
|
||||||
|
z, err := strconv.Atoi(c.Params("z"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("invalid z")
|
||||||
|
}
|
||||||
|
|
||||||
|
x, err := strconv.Atoi(c.Params("x"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("invalid x")
|
||||||
|
}
|
||||||
|
|
||||||
|
y, err := strconv.Atoi(c.Params("y"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("invalid y")
|
||||||
|
}
|
||||||
|
|
||||||
|
if z < 0 || x < 0 || y < 0 {
|
||||||
|
return 0, 0, 0, fmt.Errorf("coordinates must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if z > 22 {
|
||||||
|
return 0, 0, 0, fmt.Errorf("zoom level too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
return z, x, y, nil
|
||||||
|
}
|
||||||
11
internal/dtos/request/auth.go
Normal file
11
internal/dtos/request/auth.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
type SignUpDto struct {
|
||||||
|
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||||
|
Password string `json:"password" validate:"required,min=8,max=64"`
|
||||||
|
DisplayName string `json:"display_name" validate:"required,min=2,max=50"`
|
||||||
|
}
|
||||||
|
type SignInDto struct {
|
||||||
|
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||||
|
Password string `json:"password" validate:"required,min=8,max=64"`
|
||||||
|
}
|
||||||
16
internal/dtos/response/role.go
Normal file
16
internal/dtos/response/role.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type RoleSimpleResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
11
internal/dtos/response/token.go
Normal file
11
internal/dtos/response/token.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type TokenResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
TokenType int16 `json:"token_type"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
26
internal/dtos/response/user.go
Normal file
26
internal/dtos/response/user.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type UserResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Profile *UserProfileSimpleResponse `json:"profile"`
|
||||||
|
IsVerified bool `json:"is_verified"`
|
||||||
|
TokenVersion int32 `json:"token_version"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at"`
|
||||||
|
Roles []*RoleSimpleResponse `json:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProfileSimpleResponse struct {
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
AvatarUrl string `json:"avatar_url"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
}
|
||||||
@@ -11,26 +11,39 @@ import (
|
|||||||
type Role struct {
|
type Role struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
GoogleID pgtype.Text `json:"google_id"`
|
||||||
IsActive pgtype.Bool `json:"is_active"`
|
AuthProvider string `json:"auth_provider"`
|
||||||
IsVerified pgtype.Bool `json:"is_verified"`
|
IsVerified bool `json:"is_verified"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserProfile struct {
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
DisplayName pgtype.Text `json:"display_name"`
|
||||||
|
FullName pgtype.Text `json:"full_name"`
|
||||||
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
|
Bio pgtype.Text `json:"bio"`
|
||||||
|
Location pgtype.Text `json:"location"`
|
||||||
|
Website pgtype.Text `json:"website"`
|
||||||
|
CountryCode pgtype.Text `json:"country_code"`
|
||||||
|
Phone pgtype.Text `json:"phone"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserRole struct {
|
type UserRole struct {
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
RoleID pgtype.UUID `json:"role_id"`
|
RoleID pgtype.UUID `json:"role_id"`
|
||||||
@@ -40,7 +53,19 @@ type UserToken struct {
|
|||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
TokenType int16 `json:"token_type"`
|
TokenType int16 `json:"token_type"`
|
||||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserVerification struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
VerifyType int16 `json:"verify_type"`
|
||||||
|
DocumentUrl string `json:"document_url"`
|
||||||
|
Status int16 `json:"status"`
|
||||||
|
ReviewedBy pgtype.UUID `json:"reviewed_by"`
|
||||||
|
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,44 +11,36 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createUser = `-- name: CreateUser :one
|
const createUserProfile = `-- name: CreateUserProfile :one
|
||||||
INSERT INTO users (
|
INSERT INTO user_profiles (
|
||||||
name,
|
user_id,
|
||||||
email,
|
display_name,
|
||||||
password_hash,
|
|
||||||
avatar_url
|
avatar_url
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4
|
$1, $2, $3
|
||||||
)
|
)
|
||||||
RETURNING id, name, email, password_hash, avatar_url, is_active, is_verified, token_version, refresh_token, is_deleted, created_at, updated_at
|
RETURNING user_id, display_name, full_name, avatar_url, bio, location, website, country_code, phone, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserParams struct {
|
type CreateUserProfileParams struct {
|
||||||
Name string `json:"name"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
Email string `json:"email"`
|
DisplayName pgtype.Text `json:"display_name"`
|
||||||
PasswordHash string `json:"password_hash"`
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
func (q *Queries) CreateUserProfile(ctx context.Context, arg CreateUserProfileParams) (UserProfile, error) {
|
||||||
row := q.db.QueryRow(ctx, createUser,
|
row := q.db.QueryRow(ctx, createUserProfile, arg.UserID, arg.DisplayName, arg.AvatarUrl)
|
||||||
arg.Name,
|
var i UserProfile
|
||||||
arg.Email,
|
|
||||||
arg.PasswordHash,
|
|
||||||
arg.AvatarUrl,
|
|
||||||
)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.UserID,
|
||||||
&i.Name,
|
&i.DisplayName,
|
||||||
&i.Email,
|
&i.FullName,
|
||||||
&i.PasswordHash,
|
|
||||||
&i.AvatarUrl,
|
&i.AvatarUrl,
|
||||||
&i.IsActive,
|
&i.Bio,
|
||||||
&i.IsVerified,
|
&i.Location,
|
||||||
&i.TokenVersion,
|
&i.Website,
|
||||||
&i.RefreshToken,
|
&i.CountryCode,
|
||||||
&i.IsDeleted,
|
&i.Phone,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
@@ -58,8 +50,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||||||
const deleteUser = `-- name: DeleteUser :exec
|
const deleteUser = `-- name: DeleteUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
is_deleted = true,
|
is_deleted = true
|
||||||
updated_at = now()
|
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -68,59 +59,69 @@ func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const existsUserByEmail = `-- name: ExistsUserByEmail :one
|
const getTokenVersion = `-- name: GetTokenVersion :one
|
||||||
SELECT EXISTS (
|
SELECT token_version
|
||||||
SELECT 1 FROM users
|
FROM users
|
||||||
WHERE email = $1
|
WHERE id = $1 AND is_deleted = false
|
||||||
AND is_deleted = false
|
|
||||||
)
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ExistsUserByEmail(ctx context.Context, email string) (bool, error) {
|
func (q *Queries) GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error) {
|
||||||
row := q.db.QueryRow(ctx, existsUserByEmail, email)
|
row := q.db.QueryRow(ctx, getTokenVersion, id)
|
||||||
var exists bool
|
var token_version int32
|
||||||
err := row.Scan(&exists)
|
err := row.Scan(&token_version)
|
||||||
return exists, err
|
return token_version, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.name,
|
u.email,
|
||||||
u.email,
|
u.password_hash,
|
||||||
u.password_hash,
|
u.is_verified,
|
||||||
u.avatar_url,
|
u.token_version,
|
||||||
u.is_active,
|
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at,
|
u.updated_at,
|
||||||
COALESCE(
|
|
||||||
json_agg(
|
(
|
||||||
json_build_object('id', r.id, 'name', r.name)
|
SELECT json_build_object(
|
||||||
) FILTER (WHERE r.id IS NOT NULL),
|
'display_name', p.display_name,
|
||||||
'[]'
|
'full_name', p.full_name,
|
||||||
)::json AS roles
|
'avatar_url', p.avatar_url,
|
||||||
|
'bio', p.bio,
|
||||||
|
'location', p.location,
|
||||||
|
'website', p.website,
|
||||||
|
'country_code', p.country_code,
|
||||||
|
'phone', p.phone
|
||||||
|
)
|
||||||
|
FROM user_profiles p
|
||||||
|
WHERE p.user_id = u.id
|
||||||
|
) AS profile,
|
||||||
|
|
||||||
|
(
|
||||||
|
SELECT COALESCE(
|
||||||
|
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||||
|
'[]'
|
||||||
|
)::json
|
||||||
|
FROM user_roles ur
|
||||||
|
JOIN roles r ON ur.role_id = r.id
|
||||||
|
WHERE ur.user_id = u.id
|
||||||
|
) AS roles
|
||||||
|
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
|
||||||
LEFT JOIN roles r ON ur.role_id = r.id
|
|
||||||
WHERE u.email = $1 AND u.is_deleted = false
|
WHERE u.email = $1 AND u.is_deleted = false
|
||||||
GROUP BY u.id
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetUserByEmailRow struct {
|
type GetUserByEmailRow struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
IsVerified bool `json:"is_verified"`
|
||||||
IsActive pgtype.Bool `json:"is_active"`
|
|
||||||
IsVerified pgtype.Bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
Profile []byte `json:"profile"`
|
||||||
Roles []byte `json:"roles"`
|
Roles []byte `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,16 +130,14 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
|||||||
var i GetUserByEmailRow
|
var i GetUserByEmailRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.AvatarUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.IsVerified,
|
&i.IsVerified,
|
||||||
&i.TokenVersion,
|
&i.TokenVersion,
|
||||||
&i.IsDeleted,
|
&i.IsDeleted,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Profile,
|
||||||
&i.Roles,
|
&i.Roles,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
@@ -146,44 +145,58 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
|||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.name,
|
u.email,
|
||||||
u.email,
|
u.password_hash,
|
||||||
u.password_hash,
|
u.is_verified,
|
||||||
u.avatar_url,
|
u.token_version,
|
||||||
u.is_active,
|
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
|
||||||
u.refresh_token,
|
u.refresh_token,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at,
|
u.updated_at,
|
||||||
COALESCE(
|
|
||||||
json_agg(
|
-- profile JSON
|
||||||
json_build_object('id', r.id, 'name', r.name)
|
(
|
||||||
) FILTER (WHERE r.id IS NOT NULL),
|
SELECT json_build_object(
|
||||||
'[]'
|
'display_name', p.display_name,
|
||||||
)::json AS roles
|
'full_name', p.full_name,
|
||||||
|
'avatar_url', p.avatar_url,
|
||||||
|
'bio', p.bio,
|
||||||
|
'location', p.location,
|
||||||
|
'website', p.website,
|
||||||
|
'country_code', p.country_code,
|
||||||
|
'phone', p.phone
|
||||||
|
)
|
||||||
|
FROM user_profiles p
|
||||||
|
WHERE p.user_id = u.id
|
||||||
|
) AS profile,
|
||||||
|
|
||||||
|
-- roles JSON
|
||||||
|
(
|
||||||
|
SELECT COALESCE(
|
||||||
|
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||||
|
'[]'
|
||||||
|
)::json
|
||||||
|
FROM user_roles ur
|
||||||
|
JOIN roles r ON ur.role_id = r.id
|
||||||
|
WHERE ur.user_id = u.id
|
||||||
|
) AS roles
|
||||||
|
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
|
||||||
LEFT JOIN roles r ON ur.role_id = r.id
|
|
||||||
WHERE u.id = $1 AND u.is_deleted = false
|
WHERE u.id = $1 AND u.is_deleted = false
|
||||||
GROUP BY u.id
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetUserByIDRow struct {
|
type GetUserByIDRow struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
IsVerified bool `json:"is_verified"`
|
||||||
IsActive pgtype.Bool `json:"is_active"`
|
|
||||||
IsVerified pgtype.Bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
Profile []byte `json:"profile"`
|
||||||
Roles []byte `json:"roles"`
|
Roles []byte `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,17 +205,15 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
|||||||
var i GetUserByIDRow
|
var i GetUserByIDRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.AvatarUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.IsVerified,
|
&i.IsVerified,
|
||||||
&i.TokenVersion,
|
&i.TokenVersion,
|
||||||
&i.RefreshToken,
|
&i.RefreshToken,
|
||||||
&i.IsDeleted,
|
&i.IsDeleted,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Profile,
|
||||||
&i.Roles,
|
&i.Roles,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
@@ -210,44 +221,56 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
|||||||
|
|
||||||
const getUsers = `-- name: GetUsers :many
|
const getUsers = `-- name: GetUsers :many
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.name,
|
u.email,
|
||||||
u.email,
|
u.password_hash,
|
||||||
u.password_hash,
|
u.is_verified,
|
||||||
u.avatar_url,
|
u.token_version,
|
||||||
u.is_active,
|
u.refresh_token,
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
|
||||||
u.refresh_token,
|
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at,
|
u.updated_at,
|
||||||
COALESCE(
|
|
||||||
json_agg(
|
(
|
||||||
json_build_object('id', r.id, 'name', r.name)
|
SELECT json_build_object(
|
||||||
) FILTER (WHERE r.id IS NOT NULL),
|
'display_name', p.display_name,
|
||||||
'[]'
|
'full_name', p.full_name,
|
||||||
)::json AS roles
|
'avatar_url', p.avatar_url,
|
||||||
|
'bio', p.bio,
|
||||||
|
'location', p.location,
|
||||||
|
'website', p.website,
|
||||||
|
'country_code', p.country_code,
|
||||||
|
'phone', p.phone
|
||||||
|
)
|
||||||
|
FROM user_profiles p
|
||||||
|
WHERE p.user_id = u.id
|
||||||
|
) AS profile,
|
||||||
|
|
||||||
|
(
|
||||||
|
SELECT COALESCE(
|
||||||
|
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||||
|
'[]'
|
||||||
|
)::json
|
||||||
|
FROM user_roles ur
|
||||||
|
JOIN roles r ON ur.role_id = r.id
|
||||||
|
WHERE ur.user_id = u.id
|
||||||
|
) AS roles
|
||||||
|
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
|
||||||
LEFT JOIN roles r ON ur.role_id = r.id
|
|
||||||
WHERE u.is_deleted = false
|
WHERE u.is_deleted = false
|
||||||
GROUP BY u.id
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetUsersRow struct {
|
type GetUsersRow struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
IsVerified bool `json:"is_verified"`
|
||||||
IsActive pgtype.Bool `json:"is_active"`
|
|
||||||
IsVerified pgtype.Bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
Profile []byte `json:"profile"`
|
||||||
Roles []byte `json:"roles"`
|
Roles []byte `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,17 +285,15 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
|||||||
var i GetUsersRow
|
var i GetUsersRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.AvatarUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.IsVerified,
|
&i.IsVerified,
|
||||||
&i.TokenVersion,
|
&i.TokenVersion,
|
||||||
&i.RefreshToken,
|
&i.RefreshToken,
|
||||||
&i.IsDeleted,
|
&i.IsDeleted,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Profile,
|
||||||
&i.Roles,
|
&i.Roles,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -288,8 +309,7 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
|||||||
const restoreUser = `-- name: RestoreUser :exec
|
const restoreUser = `-- name: RestoreUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
is_deleted = false,
|
is_deleted = false
|
||||||
updated_at = now()
|
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -298,99 +318,33 @@ func (q *Queries) RestoreUser(ctx context.Context, id pgtype.UUID) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUser = `-- name: UpdateUser :one
|
const updateTokenVersion = `-- name: UpdateTokenVersion :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET token_version = $2
|
||||||
name = $1,
|
WHERE id = $1 AND is_deleted = false
|
||||||
avatar_url = $2,
|
|
||||||
is_active = $3,
|
|
||||||
is_verified = $4,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE users.id = $5 AND users.is_deleted = false
|
|
||||||
RETURNING
|
|
||||||
users.id,
|
|
||||||
users.name,
|
|
||||||
users.email,
|
|
||||||
users.password_hash,
|
|
||||||
users.avatar_url,
|
|
||||||
users.is_active,
|
|
||||||
users.is_verified,
|
|
||||||
users.token_version,
|
|
||||||
users.refresh_token,
|
|
||||||
users.is_deleted,
|
|
||||||
users.created_at,
|
|
||||||
users.updated_at,
|
|
||||||
(
|
|
||||||
SELECT COALESCE(json_agg(json_build_object('id', roles.id, 'name', roles.name)), '[]')::json
|
|
||||||
FROM user_roles
|
|
||||||
JOIN roles ON user_roles.role_id = roles.id
|
|
||||||
WHERE user_roles.user_id = users.id
|
|
||||||
) AS roles
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserParams struct {
|
type UpdateTokenVersionParams struct {
|
||||||
Name string `json:"name"`
|
ID pgtype.UUID `json:"id"`
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
IsActive pgtype.Bool `json:"is_active"`
|
|
||||||
IsVerified pgtype.Bool `json:"is_verified"`
|
|
||||||
ID pgtype.UUID `json:"id"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserRow struct {
|
func (q *Queries) UpdateTokenVersion(ctx context.Context, arg UpdateTokenVersionParams) error {
|
||||||
ID pgtype.UUID `json:"id"`
|
_, err := q.db.Exec(ctx, updateTokenVersion, arg.ID, arg.TokenVersion)
|
||||||
Name string `json:"name"`
|
return err
|
||||||
Email string `json:"email"`
|
|
||||||
PasswordHash string `json:"password_hash"`
|
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
|
||||||
IsActive pgtype.Bool `json:"is_active"`
|
|
||||||
IsVerified pgtype.Bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
Roles []byte `json:"roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, updateUser,
|
|
||||||
arg.Name,
|
|
||||||
arg.AvatarUrl,
|
|
||||||
arg.IsActive,
|
|
||||||
arg.IsVerified,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
var i UpdateUserRow
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Email,
|
|
||||||
&i.PasswordHash,
|
|
||||||
&i.AvatarUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.IsVerified,
|
|
||||||
&i.TokenVersion,
|
|
||||||
&i.RefreshToken,
|
|
||||||
&i.IsDeleted,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.Roles,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUserPassword = `-- name: UpdateUserPassword :exec
|
const updateUserPassword = `-- name: UpdateUserPassword :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
password_hash = $2,
|
password_hash = $2
|
||||||
updated_at = now()
|
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND is_deleted = false
|
AND is_deleted = false
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserPasswordParams struct {
|
type UpdateUserPasswordParams struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
|
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
|
||||||
@@ -398,11 +352,137 @@ func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPassword
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateUserProfile = `-- name: UpdateUserProfile :one
|
||||||
|
UPDATE user_profiles
|
||||||
|
SET
|
||||||
|
display_name = $1,
|
||||||
|
full_name = $2,
|
||||||
|
avatar_url = $3,
|
||||||
|
bio = $4,
|
||||||
|
location = $5,
|
||||||
|
website = $6,
|
||||||
|
country_code = $7,
|
||||||
|
phone = $8,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE user_id = $9
|
||||||
|
RETURNING user_id, display_name, full_name, avatar_url, bio, location, website, country_code, phone, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserProfileParams struct {
|
||||||
|
DisplayName pgtype.Text `json:"display_name"`
|
||||||
|
FullName pgtype.Text `json:"full_name"`
|
||||||
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
|
Bio pgtype.Text `json:"bio"`
|
||||||
|
Location pgtype.Text `json:"location"`
|
||||||
|
Website pgtype.Text `json:"website"`
|
||||||
|
CountryCode pgtype.Text `json:"country_code"`
|
||||||
|
Phone pgtype.Text `json:"phone"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (UserProfile, error) {
|
||||||
|
row := q.db.QueryRow(ctx, updateUserProfile,
|
||||||
|
arg.DisplayName,
|
||||||
|
arg.FullName,
|
||||||
|
arg.AvatarUrl,
|
||||||
|
arg.Bio,
|
||||||
|
arg.Location,
|
||||||
|
arg.Website,
|
||||||
|
arg.CountryCode,
|
||||||
|
arg.Phone,
|
||||||
|
arg.UserID,
|
||||||
|
)
|
||||||
|
var i UserProfile
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UserID,
|
||||||
|
&i.DisplayName,
|
||||||
|
&i.FullName,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.Bio,
|
||||||
|
&i.Location,
|
||||||
|
&i.Website,
|
||||||
|
&i.CountryCode,
|
||||||
|
&i.Phone,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserRefreshToken = `-- name: UpdateUserRefreshToken :exec
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
refresh_token = $2
|
||||||
|
WHERE id = $1
|
||||||
|
AND is_deleted = false
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserRefreshTokenParams struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserRefreshToken(ctx context.Context, arg UpdateUserRefreshTokenParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateUserRefreshToken, arg.ID, arg.RefreshToken)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertUser = `-- name: UpsertUser :one
|
||||||
|
INSERT INTO users (
|
||||||
|
email,
|
||||||
|
password_hash,
|
||||||
|
google_id,
|
||||||
|
auth_provider,
|
||||||
|
is_verified
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5
|
||||||
|
)
|
||||||
|
ON CONFLICT (email)
|
||||||
|
DO UPDATE SET
|
||||||
|
google_id = EXCLUDED.google_id,
|
||||||
|
auth_provider = EXCLUDED.auth_provider,
|
||||||
|
is_verified = users.is_verified OR EXCLUDED.is_verified,
|
||||||
|
updated_at = now()
|
||||||
|
RETURNING id, email, password_hash, google_id, auth_provider, is_verified, is_deleted, token_version, refresh_token, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertUserParams struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
|
GoogleID pgtype.Text `json:"google_id"`
|
||||||
|
AuthProvider string `json:"auth_provider"`
|
||||||
|
IsVerified bool `json:"is_verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, upsertUser,
|
||||||
|
arg.Email,
|
||||||
|
arg.PasswordHash,
|
||||||
|
arg.GoogleID,
|
||||||
|
arg.AuthProvider,
|
||||||
|
arg.IsVerified,
|
||||||
|
)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.GoogleID,
|
||||||
|
&i.AuthProvider,
|
||||||
|
&i.IsVerified,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.TokenVersion,
|
||||||
|
&i.RefreshToken,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const verifyUser = `-- name: VerifyUser :exec
|
const verifyUser = `-- name: VerifyUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
is_verified = true,
|
is_verified = true
|
||||||
updated_at = now()
|
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND is_deleted = false
|
AND is_deleted = false
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package middlewares
|
package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
"history-api/pkg/config"
|
"history-api/pkg/config"
|
||||||
"history-api/pkg/constant"
|
"history-api/pkg/constant"
|
||||||
"history-api/pkg/dtos/response"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
jwtware "github.com/gofiber/contrib/v3/jwt"
|
jwtware "github.com/gofiber/contrib/v3/jwt"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package middlewares
|
package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
"history-api/pkg/constant"
|
"history-api/pkg/constant"
|
||||||
"history-api/pkg/dtos/response"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|||||||
27
internal/models/profile.go
Normal file
27
internal/models/profile.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "history-api/internal/dtos/response"
|
||||||
|
|
||||||
|
type UserProfileSimple struct {
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
AvatarUrl string `json:"avatar_url"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserProfileSimple) ToResponse() *response.UserProfileSimpleResponse {
|
||||||
|
return &response.UserProfileSimpleResponse{
|
||||||
|
DisplayName: p.DisplayName,
|
||||||
|
FullName: p.FullName,
|
||||||
|
AvatarUrl: p.AvatarUrl,
|
||||||
|
Bio: p.Bio,
|
||||||
|
Location: p.Location,
|
||||||
|
Website: p.Website,
|
||||||
|
CountryCode: p.CountryCode,
|
||||||
|
Phone: p.Phone,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"history-api/pkg/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
"history-api/pkg/convert"
|
"history-api/pkg/constant"
|
||||||
|
"time"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoleSimple struct {
|
type RoleSimple struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RoleSimple) ToResponse() *response.RoleSimpleResponse {
|
func (r *RoleSimple) ToResponse() *response.RoleSimpleResponse {
|
||||||
return &response.RoleSimpleResponse{
|
return &response.RoleSimpleResponse{
|
||||||
ID: convert.UUIDToString(r.ID),
|
ID: r.ID,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,20 +27,20 @@ func RolesToResponse(rs []*RoleSimple) []*response.RoleSimpleResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RoleEntity struct {
|
type RoleEntity struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt *time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RoleEntity) ToResponse() *response.RoleResponse {
|
func (r *RoleEntity) ToResponse() *response.RoleResponse {
|
||||||
return &response.RoleResponse{
|
return &response.RoleResponse{
|
||||||
ID: convert.UUIDToString(r.ID),
|
ID: r.ID,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
IsDeleted: convert.BoolVal(r.IsDeleted),
|
IsDeleted: r.IsDeleted,
|
||||||
CreatedAt: convert.TimeToPtr(r.CreatedAt),
|
CreatedAt: r.CreatedAt,
|
||||||
UpdatedAt: convert.TimeToPtr(r.UpdatedAt),
|
UpdatedAt: r.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,3 +51,15 @@ func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RolesEntityToRoleConstant(rs []*RoleSimple) []constant.Role {
|
||||||
|
out := make([]constant.Role, len(rs))
|
||||||
|
for i := range rs {
|
||||||
|
data, ok := constant.ParseRole(rs[i].Name)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[i] = data
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
"history-api/pkg/convert"
|
"history-api/pkg/convert"
|
||||||
"history-api/pkg/dtos/response"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
61
internal/models/user.go
Normal file
61
internal/models/user.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserEntity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
Profile *UserProfileSimple `json:"profile"`
|
||||||
|
IsVerified bool `json:"is_verified"`
|
||||||
|
TokenVersion int32 `json:"token_version"`
|
||||||
|
GoogleID string `json:"google_id"`
|
||||||
|
AuthProvider string `json:"auth_provider"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at"`
|
||||||
|
Roles []*RoleSimple `json:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserEntity) ParseRoles(data []byte) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
u.Roles = []*RoleSimple{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &u.Roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserEntity) ParseProfile(data []byte) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
u.Profile = &UserProfileSimple{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &u.Profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserEntity) ToResponse() *response.UserResponse {
|
||||||
|
return &response.UserResponse{
|
||||||
|
ID: u.ID,
|
||||||
|
Email: u.Email,
|
||||||
|
IsVerified: u.IsVerified,
|
||||||
|
TokenVersion: u.TokenVersion,
|
||||||
|
IsDeleted: u.IsDeleted,
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
UpdatedAt: u.UpdatedAt,
|
||||||
|
Roles: RolesToResponse(u.Roles),
|
||||||
|
Profile: u.Profile.ToResponse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UsersEntityToResponse(rs []*UserEntity) []*response.UserResponse {
|
||||||
|
out := make([]*response.UserResponse, len(rs))
|
||||||
|
for i := range rs {
|
||||||
|
out[i] = rs[i].ToResponse()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -2,11 +2,15 @@ package repositories
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"history-api/internal/gen/sqlc"
|
"history-api/internal/gen/sqlc"
|
||||||
"history-api/pkg/models"
|
"history-api/internal/models"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/convert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoleRepository interface {
|
type RoleRepository interface {
|
||||||
@@ -25,43 +29,63 @@ type RoleRepository interface {
|
|||||||
|
|
||||||
type roleRepository struct {
|
type roleRepository struct {
|
||||||
q *sqlc.Queries
|
q *sqlc.Queries
|
||||||
|
c cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRoleRepository(db sqlc.DBTX) RoleRepository {
|
func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
|
||||||
return &roleRepository{
|
return &roleRepository{
|
||||||
q: sqlc.New(db),
|
q: sqlc.New(db),
|
||||||
|
c: c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) {
|
func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) {
|
||||||
|
cacheId := fmt.Sprintf("role:id:%s", convert.UUIDToString(id))
|
||||||
|
var role models.RoleEntity
|
||||||
|
err := r.c.Get(ctx, cacheId, &role)
|
||||||
|
if err == nil {
|
||||||
|
return &role, nil
|
||||||
|
}
|
||||||
|
|
||||||
row, err := r.q.GetRoleByID(ctx, id)
|
row, err := r.q.GetRoleByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
role := &models.RoleEntity{
|
role = models.RoleEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
return role, nil
|
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||||
|
|
||||||
|
return &role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.RoleEntity, error) {
|
func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.RoleEntity, error) {
|
||||||
|
cacheId := fmt.Sprintf("role:name:%s", name)
|
||||||
|
var role models.RoleEntity
|
||||||
|
err := r.c.Get(ctx, cacheId, &role)
|
||||||
|
if err == nil {
|
||||||
|
return &role, nil
|
||||||
|
}
|
||||||
row, err := r.q.GetRoleByName(ctx, name)
|
row, err := r.q.GetRoleByName(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
role := &models.RoleEntity{
|
role = models.RoleEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
return role, nil
|
|
||||||
|
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||||
|
|
||||||
|
return &role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleEntity, error) {
|
func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleEntity, error) {
|
||||||
@@ -69,14 +93,19 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
role := &models.RoleEntity{
|
role := models.RoleEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
return role, nil
|
mapCache := map[string]any{
|
||||||
|
fmt.Sprintf("role:name:%s", name): role,
|
||||||
|
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
|
||||||
|
}
|
||||||
|
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||||
|
return &role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) {
|
func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) {
|
||||||
@@ -84,14 +113,20 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
role := &models.RoleEntity{
|
role := models.RoleEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
return role, nil
|
|
||||||
|
mapCache := map[string]any{
|
||||||
|
fmt.Sprintf("role:name:%s", row.Name): role,
|
||||||
|
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
|
||||||
|
}
|
||||||
|
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||||
|
return &role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) {
|
func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) {
|
||||||
@@ -103,11 +138,11 @@ func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error)
|
|||||||
var users []*models.RoleEntity
|
var users []*models.RoleEntity
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
user := &models.RoleEntity{
|
user := &models.RoleEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
}
|
}
|
||||||
@@ -116,33 +151,44 @@ func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
||||||
err := r.q.DeleteRole(ctx, id)
|
role, err := r.GetByID(ctx, id)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.q.DeleteRole(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", role.ID), fmt.Sprintf("role:name:%s", role.Name))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) Restore(ctx context.Context, id pgtype.UUID) error {
|
func (r *roleRepository) Restore(ctx context.Context, id pgtype.UUID) error {
|
||||||
err := r.q.RestoreRole(ctx, id)
|
err := r.q.RestoreRole(ctx, id)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(id)))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error {
|
func (r *roleRepository) AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error {
|
||||||
err := r.q.AddUserRole(ctx, params)
|
err := r.q.AddUserRole(ctx, params)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error {
|
func (r *roleRepository) RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error {
|
||||||
err := r.q.RemoveUserRole(ctx, params)
|
err := r.q.RemoveUserRole(ctx, params)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error {
|
func (r *roleRepository) RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error {
|
||||||
err := r.q.RemoveAllUsersFromRole(ctx, roleId)
|
err := r.q.RemoveAllUsersFromRole(ctx, roleId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roleRepository) RemoveAllRolesFromUser(ctx context.Context, roleId pgtype.UUID) error {
|
func (r *roleRepository) RemoveAllRolesFromUser(ctx context.Context, roleId pgtype.UUID) error {
|
||||||
err := r.q.RemoveAllRolesFromUser(ctx, roleId)
|
err := r.q.RemoveAllRolesFromUser(ctx, roleId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
98
internal/repositories/tileRepository.go
Normal file
98
internal/repositories/tileRepository.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TileRepository interface {
|
||||||
|
GetMetadata(ctx context.Context) (map[string]string, error)
|
||||||
|
GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
c cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTileRepository(db *sql.DB, c cache.Cache) TileRepository {
|
||||||
|
return &tileRepository{
|
||||||
|
db: db,
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, error) {
|
||||||
|
cacheId := "mbtiles:metadata"
|
||||||
|
|
||||||
|
var cached map[string]string
|
||||||
|
err := r.c.Get(ctx, cacheId, &cached)
|
||||||
|
if err == nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, "SELECT name, value FROM metadata")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
metadata := make(map[string]string)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var name, value string
|
||||||
|
if err := rows.Scan(&name, &value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
metadata[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.Set(ctx, cacheId, metadata, 10*time.Minute)
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tileRepository) GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error) {
|
||||||
|
if z < 0 || x < 0 || y < 0 {
|
||||||
|
return nil, "", false, fmt.Errorf("invalid tile coordinates")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache key
|
||||||
|
cacheId := fmt.Sprintf("tile:%d:%d:%d", z, x, y)
|
||||||
|
|
||||||
|
var cached []byte
|
||||||
|
err := r.c.Get(ctx, cacheId, &cached)
|
||||||
|
if err == nil {
|
||||||
|
meta, _ := r.GetMetadata(ctx)
|
||||||
|
return cached, meta["format"], meta["format"] == "pbf", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XYZ -> TMS
|
||||||
|
tmsY := (1 << z) - 1 - y
|
||||||
|
|
||||||
|
var tileData []byte
|
||||||
|
|
||||||
|
err = r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT tile_data
|
||||||
|
FROM tiles
|
||||||
|
WHERE zoom_level = ?
|
||||||
|
AND tile_column = ?
|
||||||
|
AND tile_row = ?
|
||||||
|
`, z, x, tmsY).Scan(&tileData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := r.GetMetadata(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.Set(ctx, cacheId, tileData, 5*time.Minute)
|
||||||
|
|
||||||
|
return tileData, meta["format"], meta["format"] == "pbf", nil
|
||||||
|
}
|
||||||
@@ -2,136 +2,187 @@ package repositories
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"history-api/internal/gen/sqlc"
|
"history-api/internal/gen/sqlc"
|
||||||
"history-api/pkg/models"
|
"history-api/internal/models"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/convert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
||||||
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
|
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
|
||||||
All(ctx context.Context) ([]*models.UserEntity, error)
|
All(ctx context.Context) ([]*models.UserEntity, error)
|
||||||
Create(ctx context.Context, params sqlc.CreateUserParams) (*models.UserEntity, error)
|
UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error)
|
||||||
Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error)
|
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
|
||||||
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
|
UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error)
|
||||||
ExistEmail(ctx context.Context, email string) (bool, error)
|
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
|
||||||
Verify(ctx context.Context, id pgtype.UUID) error
|
UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error
|
||||||
Delete(ctx context.Context, id pgtype.UUID) error
|
GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error)
|
||||||
Restore(ctx context.Context, id pgtype.UUID) error
|
UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error
|
||||||
|
Verify(ctx context.Context, id pgtype.UUID) error
|
||||||
|
Delete(ctx context.Context, id pgtype.UUID) error
|
||||||
|
Restore(ctx context.Context, id pgtype.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type userRepository struct {
|
type userRepository struct {
|
||||||
q *sqlc.Queries
|
q *sqlc.Queries
|
||||||
|
c cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserRepository(db sqlc.DBTX) UserRepository {
|
func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
|
||||||
return &userRepository{
|
return &userRepository{
|
||||||
q: sqlc.New(db),
|
q: sqlc.New(db),
|
||||||
|
c: c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
|
func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
|
||||||
|
cacheId := fmt.Sprintf("user:id:%s", convert.UUIDToString(id))
|
||||||
|
var user models.UserEntity
|
||||||
|
err := r.c.Get(ctx, cacheId, &user)
|
||||||
|
if err == nil {
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
row, err := r.q.GetUserByID(ctx, id)
|
row, err := r.q.GetUserByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &models.UserEntity{
|
user = models.UserEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: row.PasswordHash,
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
AvatarUrl: row.AvatarUrl,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
IsVerified: row.IsVerified,
|
IsVerified: row.IsVerified,
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ParseRoles(row.Roles); err != nil {
|
if err := user.ParseRoles(row.Roles); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return user, nil
|
|
||||||
|
if err := user.ParseProfile(row.Profile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.UserEntity, error) {
|
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.UserEntity, error) {
|
||||||
|
cacheId := fmt.Sprintf("user:email:%s", email)
|
||||||
|
|
||||||
|
var user models.UserEntity
|
||||||
|
err := r.c.Get(ctx, cacheId, &user)
|
||||||
|
if err == nil {
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
row, err := r.q.GetUserByEmail(ctx, email)
|
row, err := r.q.GetUserByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
user := &models.UserEntity{
|
|
||||||
ID: row.ID,
|
user = models.UserEntity{
|
||||||
Name: row.Name,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: row.PasswordHash,
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
AvatarUrl: row.AvatarUrl,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
IsVerified: row.IsVerified,
|
IsVerified: row.IsVerified,
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ParseRoles(row.Roles); err != nil {
|
if err := user.ParseRoles(row.Roles); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return user, nil
|
|
||||||
|
if err := user.ParseProfile(row.Profile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) Create(ctx context.Context, params sqlc.CreateUserParams) (*models.UserEntity, error) {
|
func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error) {
|
||||||
row, err := r.q.CreateUser(ctx, params)
|
row, err := r.q.UpsertUser(ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.UserEntity{
|
return &models.UserEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: row.PasswordHash,
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
AvatarUrl: row.AvatarUrl,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
IsVerified: row.IsVerified,
|
IsVerified: row.IsVerified,
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
RefreshToken: row.RefreshToken,
|
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
Roles: make([]*models.RoleSimple, 0),
|
Roles: make([]*models.RoleSimple, 0),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error) {
|
func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error) {
|
||||||
row, err := r.q.UpdateUser(ctx, params)
|
user, err := r.GetByID(ctx, params.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
user := &models.UserEntity{
|
|
||||||
ID: row.ID,
|
row, err := r.q.UpdateUserProfile(ctx, params)
|
||||||
Name: row.Name,
|
if err != nil {
|
||||||
Email: row.Email,
|
return nil, err
|
||||||
PasswordHash: row.PasswordHash,
|
}
|
||||||
AvatarUrl: row.AvatarUrl,
|
profile := models.UserProfileSimple{
|
||||||
IsActive: row.IsActive,
|
DisplayName: convert.TextToString(row.DisplayName),
|
||||||
IsVerified: row.IsVerified,
|
FullName: convert.TextToString(row.FullName),
|
||||||
TokenVersion: row.TokenVersion,
|
AvatarUrl: convert.TextToString(row.AvatarUrl),
|
||||||
IsDeleted: row.IsDeleted,
|
Bio: convert.TextToString(row.Bio),
|
||||||
CreatedAt: row.CreatedAt,
|
Location: convert.TextToString(row.Location),
|
||||||
UpdatedAt: row.UpdatedAt,
|
Website: convert.TextToString(row.Website),
|
||||||
|
CountryCode: convert.TextToString(row.CountryCode),
|
||||||
|
Phone: convert.TextToString(row.Phone),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ParseRoles(row.Roles); err != nil {
|
user.Profile = &profile
|
||||||
return nil, err
|
mapCache := map[string]any{
|
||||||
|
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||||
|
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||||
}
|
}
|
||||||
|
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error) {
|
||||||
|
row, err := r.q.CreateUserProfile(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.UserProfileSimple{
|
||||||
|
DisplayName: convert.TextToString(row.DisplayName),
|
||||||
|
FullName: convert.TextToString(row.FullName),
|
||||||
|
AvatarUrl: convert.TextToString(row.AvatarUrl),
|
||||||
|
Bio: convert.TextToString(row.Bio),
|
||||||
|
Location: convert.TextToString(row.Location),
|
||||||
|
Website: convert.TextToString(row.Website),
|
||||||
|
CountryCode: convert.TextToString(row.CountryCode),
|
||||||
|
Phone: convert.TextToString(row.Phone),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
|
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
|
||||||
rows, err := r.q.GetUsers(ctx)
|
rows, err := r.q.GetUsers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -141,23 +192,24 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
|
|||||||
var users []*models.UserEntity
|
var users []*models.UserEntity
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
user := &models.UserEntity{
|
user := &models.UserEntity{
|
||||||
ID: row.ID,
|
ID: convert.UUIDToString(row.ID),
|
||||||
Name: row.Name,
|
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: row.PasswordHash,
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
AvatarUrl: row.AvatarUrl,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
IsVerified: row.IsVerified,
|
IsVerified: row.IsVerified,
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ParseRoles(row.Roles); err != nil {
|
if err := user.ParseRoles(row.Roles); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := user.ParseProfile(row.Profile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,29 +217,138 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
|
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
|
||||||
err := r.q.VerifyUser(ctx, id)
|
user, err := r.GetByID(ctx, id)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.q.VerifyUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.q.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
||||||
|
ID: id,
|
||||||
|
TokenVersion: user.TokenVersion + 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.IsVerified = true
|
||||||
|
user.TokenVersion += 1
|
||||||
|
|
||||||
|
mapCache := map[string]any{
|
||||||
|
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||||
|
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||||
|
}
|
||||||
|
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
||||||
err := r.q.DeleteUser(ctx, id)
|
user, err := r.GetByID(ctx, id)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.q.DeleteUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.Del(
|
||||||
|
ctx,
|
||||||
|
fmt.Sprintf("user:id:%s", user.ID),
|
||||||
|
fmt.Sprintf("user:email:%s", user.Email),
|
||||||
|
fmt.Sprintf("user:token:%s", user.ID),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) Restore(ctx context.Context, id pgtype.UUID) error {
|
func (r *userRepository) Restore(ctx context.Context, id pgtype.UUID) error {
|
||||||
err := r.q.RestoreUser(ctx, id)
|
err := r.q.RestoreUser(ctx, id)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.Del(ctx, fmt.Sprintf("user:id:%s", convert.UUIDToString(id)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error) {
|
||||||
|
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(id))
|
||||||
|
var token int32
|
||||||
|
err := r.c.Get(ctx, cacheId, &token)
|
||||||
|
if err == nil {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := r.q.GetTokenVersion(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.Set(ctx, cacheId, raw, 5*time.Minute)
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error {
|
||||||
|
err := r.q.UpdateTokenVersion(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID))
|
||||||
|
_ = r.c.Set(ctx, cacheId, params.TokenVersion, 5*time.Minute)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error {
|
func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error {
|
||||||
err := r.q.UpdateUserPassword(ctx, params)
|
user, err := r.GetByID(ctx, params.ID)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.q.UpdateUserPassword(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
||||||
|
ID: params.ID,
|
||||||
|
TokenVersion: user.TokenVersion + 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PasswordHash = convert.TextToString(params.PasswordHash)
|
||||||
|
user.TokenVersion += 1
|
||||||
|
mapCache := map[string]any{
|
||||||
|
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||||
|
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||||
|
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) ExistEmail(ctx context.Context, email string) (bool, error) {
|
func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error {
|
||||||
row, err := r.q.ExistsUserByEmail(ctx, email)
|
user, err := r.GetByID(ctx, params.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
return row, nil
|
err = r.q.UpdateUserRefreshToken(ctx, params)
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.RefreshToken = convert.TextToString(params.RefreshToken)
|
||||||
|
mapCache := map[string]any{
|
||||||
|
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||||
|
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||||
|
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package routers
|
|
||||||
15
internal/routes/authRoute.go
Normal file
15
internal/routes/authRoute.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"history-api/internal/controllers"
|
||||||
|
"history-api/internal/middlewares"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AuthRoutes(app *fiber.App, controller *controllers.AuthController) {
|
||||||
|
route := app.Group("/auth")
|
||||||
|
route.Post("/signin", controller.Signin)
|
||||||
|
route.Post("/signup", controller.Signup)
|
||||||
|
route.Post("/refresh", middlewares.JwtRefresh(), controller.RefreshToken)
|
||||||
|
}
|
||||||
18
internal/routes/notFoundRoute.go
Normal file
18
internal/routes/notFoundRoute.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NotFoundRoute(app *fiber.App) {
|
||||||
|
app.Use(
|
||||||
|
func(c fiber.Ctx) error {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: "sorry, endpoint is not found",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
13
internal/routes/tileRoute.go
Normal file
13
internal/routes/tileRoute.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"history-api/internal/controllers"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TileRoutes(app *fiber.App, controller *controllers.TileController) {
|
||||||
|
route := app.Group("/tiles")
|
||||||
|
route.Get("/metadata", controller.GetMetadata)
|
||||||
|
route.Get("/:z/:x/:y", controller.GetTile)
|
||||||
|
}
|
||||||
@@ -1 +1,294 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"history-api/internal/dtos/request"
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/gen/sqlc"
|
||||||
|
"history-api/internal/models"
|
||||||
|
"history-api/internal/repositories"
|
||||||
|
"history-api/pkg/config"
|
||||||
|
"history-api/pkg/constant"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthService interface {
|
||||||
|
Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error)
|
||||||
|
Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error)
|
||||||
|
ForgotPassword(ctx context.Context) error
|
||||||
|
VerifyToken(ctx context.Context) error
|
||||||
|
CreateToken(ctx context.Context) error
|
||||||
|
SigninWith3rd(ctx context.Context) error
|
||||||
|
RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authService struct {
|
||||||
|
userRepo repositories.UserRepository
|
||||||
|
roleRepo repositories.RoleRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService(
|
||||||
|
userRepo repositories.UserRepository,
|
||||||
|
roleRepo repositories.RoleRepository,
|
||||||
|
) AuthService {
|
||||||
|
return &authService{
|
||||||
|
userRepo: userRepo,
|
||||||
|
roleRepo: roleRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authService) genToken(Uid string, role []constant.Role) (*response.AuthResponse, error) {
|
||||||
|
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT_SECRET in environment")
|
||||||
|
}
|
||||||
|
jwtRefreshSecret, err := config.GetConfig("JWT_REFRESH_SECRET")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT_REFRESH_SECRET in environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwtSecret == "" || jwtRefreshSecret == "" {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT secrets in environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
claimsAccess := &response.JWTClaims{
|
||||||
|
UId: Uid,
|
||||||
|
Roles: role,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
claimsRefresh := &response.JWTClaims{
|
||||||
|
UId: Uid,
|
||||||
|
Roles: role,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * 24 * time.Hour)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsAccess)
|
||||||
|
at, err := accessToken.SignedString([]byte(jwtSecret))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsRefresh)
|
||||||
|
rt, err := refreshToken.SignedString([]byte(jwtRefreshSecret))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := response.AuthResponse{
|
||||||
|
AccessToken: at,
|
||||||
|
RefreshToken: rt,
|
||||||
|
}
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authService) saveNewRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error {
|
||||||
|
err := a.userRepo.UpdateRefreshToken(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||||
|
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := constant.ValidatePassword(dto.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.Password)); err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := a.genToken(user.ID, models.RolesEntityToRoleConstant(user.Roles))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
|
||||||
|
}
|
||||||
|
var pgID pgtype.UUID
|
||||||
|
err = pgID.Scan(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
err = a.saveNewRefreshToken(
|
||||||
|
ctx,
|
||||||
|
sqlc.UpdateUserRefreshTokenParams{
|
||||||
|
ID: pgID,
|
||||||
|
RefreshToken: pgtype.Text{
|
||||||
|
String: data.RefreshToken,
|
||||||
|
Valid: data.RefreshToken != "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authService) RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error) {
|
||||||
|
var pgID pgtype.UUID
|
||||||
|
err := pgID.Scan(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
user, err := a.userRepo.GetByID(ctx, pgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user data")
|
||||||
|
}
|
||||||
|
roles := models.RolesEntityToRoleConstant(user.Roles)
|
||||||
|
|
||||||
|
if slices.Contains(roles, constant.BANNED) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "User is banned!")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := a.genToken(id, roles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.saveNewRefreshToken(
|
||||||
|
ctx,
|
||||||
|
sqlc.UpdateUserRefreshTokenParams{
|
||||||
|
ID: pgID,
|
||||||
|
RefreshToken: pgtype.Text{
|
||||||
|
String: data.RefreshToken,
|
||||||
|
Valid: data.RefreshToken != "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error) {
|
||||||
|
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||||
|
}
|
||||||
|
err := constant.ValidatePassword(dto.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
if user != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "User already exists")
|
||||||
|
}
|
||||||
|
hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = a.userRepo.UpsertUser(
|
||||||
|
ctx,
|
||||||
|
sqlc.UpsertUserParams{
|
||||||
|
Email: dto.Email,
|
||||||
|
PasswordHash: pgtype.Text{
|
||||||
|
String: string(hashed),
|
||||||
|
Valid: len(hashed) != 0,
|
||||||
|
},
|
||||||
|
IsVerified: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
var userId pgtype.UUID
|
||||||
|
err = userId.Scan(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
_, err = a.userRepo.CreateProfile(
|
||||||
|
ctx,
|
||||||
|
sqlc.CreateUserProfileParams{
|
||||||
|
UserID: userId,
|
||||||
|
DisplayName: pgtype.Text{
|
||||||
|
String: dto.DisplayName,
|
||||||
|
Valid: dto.DisplayName != "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.roleRepo.AddUserRole(
|
||||||
|
ctx,
|
||||||
|
sqlc.AddUserRoleParams{
|
||||||
|
UserID: userId,
|
||||||
|
Name: constant.USER.String(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := a.genToken(user.ID, constant.USER.ToSlice())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.saveNewRefreshToken(
|
||||||
|
ctx,
|
||||||
|
sqlc.UpdateUserRefreshTokenParams{
|
||||||
|
ID: userId,
|
||||||
|
RefreshToken: pgtype.Text{
|
||||||
|
String: data.RefreshToken,
|
||||||
|
Valid: data.RefreshToken != "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForgotPassword implements [AuthService].
|
||||||
|
func (a *authService) ForgotPassword(ctx context.Context) error {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigninWith3rd implements [AuthService].
|
||||||
|
func (a *authService) SigninWith3rd(ctx context.Context) error {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateToken implements [AuthService].
|
||||||
|
func (a *authService) CreateToken(ctx context.Context) error {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify implements [AuthService].
|
||||||
|
func (a *authService) VerifyToken(ctx context.Context) error {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|||||||
57
internal/services/tileService.go
Normal file
57
internal/services/tileService.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"history-api/internal/repositories"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TileService interface {
|
||||||
|
GetMetadata(ctx context.Context) (map[string]string, error)
|
||||||
|
GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileService struct {
|
||||||
|
tileRepo repositories.TileRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTileService(
|
||||||
|
TileRepo repositories.TileRepository,
|
||||||
|
) TileService {
|
||||||
|
return &tileService{
|
||||||
|
tileRepo: TileRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tileService) GetMetadata(ctx context.Context) (map[string]string, error) {
|
||||||
|
metaData, err := t.tileRepo.GetMetadata(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return metaData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (t *tileService) GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, error) {
|
||||||
|
contentType := make(map[string]string)
|
||||||
|
|
||||||
|
data, format, isPBF, err := t.tileRepo.GetTile(ctx, z, x, y)
|
||||||
|
if err != nil {
|
||||||
|
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
contentType["Content-Type"] = "image/png"
|
||||||
|
if format == "jpg" {
|
||||||
|
contentType["Content-Type"] = "image/jpeg"
|
||||||
|
}
|
||||||
|
if format == "pbf" {
|
||||||
|
contentType["Content-Type"] = "application/x-protobuf"
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPBF {
|
||||||
|
contentType["Content-Encoding"] = "gzip"
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, contentType, nil
|
||||||
|
|
||||||
|
}
|
||||||
83
internal/services/userService.go
Normal file
83
internal/services/userService.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"history-api/internal/dtos/request"
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService interface {
|
||||||
|
//user
|
||||||
|
GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error)
|
||||||
|
UpdateProfile(ctx context.Context, id string) (*response.UserResponse, error)
|
||||||
|
ChangePassword(ctx context.Context, id string) (*response.UserResponse, error)
|
||||||
|
|
||||||
|
//admin
|
||||||
|
DeleteUser(ctx context.Context, id string) (*response.UserResponse, error)
|
||||||
|
ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error)
|
||||||
|
RestoreUser(ctx context.Context, id string) (*response.UserResponse, error)
|
||||||
|
GetUserByID(ctx context.Context, id string) (*response.UserResponse, error)
|
||||||
|
Search(ctx context.Context, id string) ([]*response.UserResponse, error)
|
||||||
|
GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type userService struct {
|
||||||
|
userRepo repositories.UserRepository
|
||||||
|
roleRepo repositories.RoleRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserService(
|
||||||
|
userRepo repositories.UserRepository,
|
||||||
|
roleRepo repositories.RoleRepository,
|
||||||
|
) UserService {
|
||||||
|
return &userService{
|
||||||
|
userRepo: userRepo,
|
||||||
|
roleRepo: roleRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword implements [UserService].
|
||||||
|
func (u *userService) ChangePassword(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeRoleUser implements [UserService].
|
||||||
|
func (u *userService) ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser implements [UserService].
|
||||||
|
func (u *userService) DeleteUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllUser implements [UserService].
|
||||||
|
func (u *userService) GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID implements [UserService].
|
||||||
|
func (u *userService) GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserCurrent implements [UserService].
|
||||||
|
func (u *userService) GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreUser implements [UserService].
|
||||||
|
func (u *userService) RestoreUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search implements [UserService].
|
||||||
|
func (u *userService) Search(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProfile implements [UserService].
|
||||||
|
func (u *userService) UpdateProfile(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
127
pkg/cache/redis.go
vendored
127
pkg/cache/redis.go
vendored
@@ -2,31 +2,136 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"history-api/pkg/config"
|
"history-api/pkg/config"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
var RI *redis.Client
|
type Cache interface {
|
||||||
|
Set(ctx context.Context, key string, value any, ttl time.Duration) error
|
||||||
|
Get(ctx context.Context, key string, dest any) error
|
||||||
|
Del(ctx context.Context, keys ...string) error
|
||||||
|
DelByPattern(ctx context.Context, pattern string) error
|
||||||
|
MGet(ctx context.Context, keys ...string) [][]byte
|
||||||
|
MSet(ctx context.Context, pairs map[string]any, ttl time.Duration) error
|
||||||
|
}
|
||||||
|
|
||||||
func Connect() error {
|
type RedisClient struct {
|
||||||
connectionURI, err := config.GetConfig("REDIS_CONNECTION_URI")
|
client *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedisClient() (Cache, error) {
|
||||||
|
uri, err := config.GetConfig("REDIS_CONNECTION_URI")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rdb := redis.NewClient(&redis.Options{
|
rdb := redis.NewClient(&redis.Options{
|
||||||
Addr: connectionURI,
|
Addr: uri,
|
||||||
Password: "",
|
MinIdleConns: 10,
|
||||||
DB: 0,
|
DialTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 3 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
|
||||||
|
MaxRetries: 3,
|
||||||
|
MinRetryBackoff: 8 * time.Millisecond,
|
||||||
|
MaxRetryBackoff: 512 * time.Millisecond,
|
||||||
|
|
||||||
|
DisableIdentity: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
||||||
return fmt.Errorf("Could not connect to Redis: %v", err)
|
return nil, fmt.Errorf("could not connect to Redis: %v", err)
|
||||||
}
|
}
|
||||||
|
return &RedisClient{client: rdb}, nil
|
||||||
RI = rdb
|
}
|
||||||
return nil
|
|
||||||
|
func (r *RedisClient) Del(ctx context.Context, keys ...string) error {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.client.Del(ctx, keys...).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RedisClient) DelByPattern(ctx context.Context, pattern string) error {
|
||||||
|
var cursor uint64
|
||||||
|
for {
|
||||||
|
keys, nextCursor, err := r.client.Scan(ctx, cursor, pattern, 100).Result()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error scanning keys with pattern %s: %v", pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) > 0 {
|
||||||
|
if err := r.client.Del(ctx, keys...).Err(); err != nil {
|
||||||
|
return fmt.Errorf("error deleting keys during scan: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = nextCursor
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RedisClient) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.client.Set(ctx, key, data, ttl).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RedisClient) Get(ctx context.Context, key string, dest any) error {
|
||||||
|
data, err := r.client.Get(ctx, key).Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RedisClient) MSet(ctx context.Context, pairs map[string]any, ttl time.Duration) error {
|
||||||
|
pipe := r.client.Pipeline()
|
||||||
|
for key, value := range pairs {
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal key %s: %v", key, err)
|
||||||
|
}
|
||||||
|
pipe.Set(ctx, key, data, ttl)
|
||||||
|
}
|
||||||
|
_, err := pipe.Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RedisClient) MGet(ctx context.Context, keys ...string) [][]byte {
|
||||||
|
res, err := r.client.MGet(ctx, keys...).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
results := make([][]byte, len(res))
|
||||||
|
for i, val := range res {
|
||||||
|
if val != nil {
|
||||||
|
results[i] = []byte(val.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMultiple[T any](ctx context.Context, c Cache, keys []string) ([]T, error) {
|
||||||
|
raws := c.MGet(ctx, keys...)
|
||||||
|
final := make([]T, 0)
|
||||||
|
for _, b := range raws {
|
||||||
|
if b == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var item T
|
||||||
|
if err := json.Unmarshal(b, &item); err == nil {
|
||||||
|
final = append(final, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return final, nil
|
||||||
}
|
}
|
||||||
|
|||||||
39
pkg/constant/regex.go
Normal file
39
pkg/constant/regex.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Password components (Go-compatible)
|
||||||
|
hasUpper = regexp.MustCompile(`[A-Z]`)
|
||||||
|
hasLower = regexp.MustCompile(`[a-z]`)
|
||||||
|
hasNumber = regexp.MustCompile(`\d`)
|
||||||
|
hasSpecial = regexp.MustCompile(`[!@#$%^&*()_+{}|:<>?~-]`)
|
||||||
|
|
||||||
|
// Standard Regexes
|
||||||
|
PHONE_NUMBER_REGEX = regexp.MustCompile(`^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$`)
|
||||||
|
EMAIL_REGEX = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
|
||||||
|
YOUTUBE_VIDEO_ID_REGEX = regexp.MustCompile(`(?:\/|v=|\/v\/|embed\/|watch\?v=|watch\?.+&v=)([\w-]{11})`)
|
||||||
|
BANK_INPUT = regexp.MustCompile(`[__]{2,}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidatePassword(password string) error {
|
||||||
|
if len(password) < 8 {
|
||||||
|
return errors.New("password must be at least 8 characters long")
|
||||||
|
}
|
||||||
|
if !hasUpper.MatchString(password) {
|
||||||
|
return errors.New("password must contain at least one uppercase letter")
|
||||||
|
}
|
||||||
|
if !hasLower.MatchString(password) {
|
||||||
|
return errors.New("password must contain at least one lowercase letter")
|
||||||
|
}
|
||||||
|
if !hasNumber.MatchString(password) {
|
||||||
|
return errors.New("password must contain at least one number")
|
||||||
|
}
|
||||||
|
if !hasSpecial.MatchString(password) {
|
||||||
|
return errors.New("password must contain at least one special character")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@ func CheckValidRole(r Role) bool {
|
|||||||
return r == ADMIN || r == MOD || r == HISTORIAN || r == USER || r == BANNED
|
return r == ADMIN || r == MOD || r == HISTORIAN || r == USER || r == BANNED
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Role) ToSlice() []string {
|
func ParseRole(s string) (Role, bool) {
|
||||||
return []string{r.String()}
|
r := Role(s)
|
||||||
|
if CheckValidRole(r) {
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
func (r Role) ToSlice() []Role {
|
||||||
|
return []Role{r}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package convert
|
package convert
|
||||||
|
|
||||||
import "github.com/jackc/pgx/v5/pgtype"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
func UUIDToString(v pgtype.UUID) string {
|
func UUIDToString(v pgtype.UUID) string {
|
||||||
if v.Valid {
|
if v.Valid {
|
||||||
@@ -23,10 +27,10 @@ func BoolVal(v pgtype.Bool) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func TimeToPtr(v pgtype.Timestamptz) *string {
|
func TimeToPtr(v pgtype.Timestamptz) *time.Time {
|
||||||
if v.Valid {
|
if !v.Valid {
|
||||||
t := v.Time.Format("2006-01-02T15:04:05Z07:00")
|
return nil
|
||||||
return &t
|
|
||||||
}
|
}
|
||||||
return nil
|
t := v.Time
|
||||||
|
return &t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Connect() (*pgxpool.Pool, error) {
|
func NewPostgresqlDB() (*pgxpool.Pool, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
connectionURI, err := config.GetConfig("PGX_CONNECTION_URI")
|
connectionURI, err := config.GetConfig("PGX_CONNECTION_URI")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package request
|
|
||||||
|
|
||||||
type SignUpDto struct {
|
|
||||||
Password string `json:"password" validate:"required"`
|
|
||||||
DiscordUserId string `json:"discord_user_id" validate:"required"`
|
|
||||||
Username string `json:"username" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginDto struct {
|
|
||||||
Username string `json:"username" validate:"required"`
|
|
||||||
Password string `json:"password" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package response
|
|
||||||
|
|
||||||
type RoleSimpleResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoleResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsDeleted bool `json:"is_deleted"`
|
|
||||||
CreatedAt *string `json:"created_at"`
|
|
||||||
UpdatedAt *string `json:"updated_at"`
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package response
|
|
||||||
|
|
||||||
type TokenResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
TokenType int16 `json:"token_type"`
|
|
||||||
ExpiresAt *string `json:"expires_at"`
|
|
||||||
CreatedAt *string `json:"created_at"`
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package response
|
|
||||||
|
|
||||||
type UserResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
AvatarUrl string `json:"avatar_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
|
||||||
IsDeleted bool `json:"is_deleted"`
|
|
||||||
CreatedAt *string `json:"created_at"`
|
|
||||||
UpdatedAt *string `json:"updated_at"`
|
|
||||||
Roles []*RoleSimpleResponse `json:"roles"`
|
|
||||||
}
|
|
||||||
26
pkg/mbtiles/db.go
Normal file
26
pkg/mbtiles/db.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package mbtiles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "github.com/glebarez/go-sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMBTilesDB(path string) (*sql.DB, error) {
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=ro&_journal_mode=off&_synchronous=off", path)
|
||||||
|
db, err := sql.Open("sqlite", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(10)
|
||||||
|
db.SetMaxIdleConns(5)
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"history-api/pkg/convert"
|
|
||||||
"history-api/pkg/dtos/response"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserEntity struct {
|
|
||||||
ID pgtype.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
PasswordHash string `json:"password_hash"`
|
|
||||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
|
||||||
IsActive pgtype.Bool `json:"is_active"`
|
|
||||||
IsVerified pgtype.Bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
|
||||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
Roles []*RoleSimple `json:"roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UserEntity) ParseRoles(data []byte) error {
|
|
||||||
if len(data) == 0 {
|
|
||||||
u.Roles = []*RoleSimple{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return json.Unmarshal(data, &u.Roles)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UserEntity) ToResponse() *response.UserResponse {
|
|
||||||
return &response.UserResponse{
|
|
||||||
ID: convert.UUIDToString(u.ID),
|
|
||||||
Name: u.Name,
|
|
||||||
Email: u.Email,
|
|
||||||
AvatarUrl: convert.TextToString(u.AvatarUrl),
|
|
||||||
IsActive: convert.BoolVal(u.IsActive),
|
|
||||||
IsVerified: convert.BoolVal(u.IsVerified),
|
|
||||||
TokenVersion: u.TokenVersion,
|
|
||||||
IsDeleted: convert.BoolVal(u.IsDeleted),
|
|
||||||
CreatedAt: convert.TimeToPtr(u.CreatedAt),
|
|
||||||
UpdatedAt: convert.TimeToPtr(u.UpdatedAt),
|
|
||||||
Roles: RolesToResponse(u.Roles),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func UsersEntityToResponse(rs []*UserEntity) []*response.UserResponse {
|
|
||||||
out := make([]*response.UserResponse, len(rs))
|
|
||||||
for i := range rs {
|
|
||||||
out[i] = rs[i].ToResponse()
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
@@ -41,8 +41,18 @@ func formatValidationError(err error) []ErrorResponse {
|
|||||||
element.FailedField = fieldError.Field()
|
element.FailedField = fieldError.Field()
|
||||||
element.Tag = fieldError.Tag()
|
element.Tag = fieldError.Tag()
|
||||||
element.Value = fieldError.Param()
|
element.Value = fieldError.Param()
|
||||||
element.Message = "Field " + fieldError.Field() + " failed validation on tag '" + fieldError.Tag() + "'"
|
switch fieldError.Tag() {
|
||||||
|
case "required":
|
||||||
|
element.Message = fieldError.Field() + " is required"
|
||||||
|
case "min":
|
||||||
|
element.Message = fieldError.Field() + " must be at least " + fieldError.Param() + " characters"
|
||||||
|
case "max":
|
||||||
|
element.Message = fieldError.Field() + " must be at most " + fieldError.Param() + " characters"
|
||||||
|
case "email":
|
||||||
|
element.Message = "Invalid email format"
|
||||||
|
default:
|
||||||
|
element.Message = fieldError.Error()
|
||||||
|
}
|
||||||
errorsList = append(errorsList, element)
|
errorsList = append(errorsList, element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,4 +89,4 @@ func ValidateBodyDto(c fiber.Ctx, dto any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user