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
|
||||
.idea
|
||||
*.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
|
||||
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 -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)" 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 generate
|
||||
|
||||
run:
|
||||
go run $(APP)
|
||||
@set GOARCH=amd64& set CGO_ENABLED=0&go run $(MAIN_APP)
|
||||
|
||||
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 (
|
||||
"context"
|
||||
// _ "history-api/docs"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"fmt"
|
||||
_ "history-api/docs"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/config"
|
||||
_ "history-api/pkg/log"
|
||||
|
||||
"fmt"
|
||||
"history-api/pkg/database"
|
||||
_ "history-api/pkg/log"
|
||||
"history-api/pkg/mbtiles"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -46,15 +44,21 @@ func StartServer() {
|
||||
log.Error().Msg(err.Error())
|
||||
panic(err)
|
||||
}
|
||||
pool, err := database.Connect()
|
||||
if err != nil {
|
||||
poolPg, err := database.NewPostgresqlDB()
|
||||
if err != nil {
|
||||
log.Error().Msg(err.Error())
|
||||
panic(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
queries := sqlc.New(pool)
|
||||
defer poolPg.Close()
|
||||
|
||||
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 {
|
||||
log.Error().Msg(err.Error())
|
||||
panic(err)
|
||||
@@ -71,7 +75,7 @@ func StartServer() {
|
||||
}
|
||||
|
||||
serverHttp := NewHttpServer()
|
||||
serverHttp.RegisterFiberRoutes()
|
||||
serverHttp.SetupServer(poolPg, sqlTile, redisClient)
|
||||
Singleton = serverHttp
|
||||
|
||||
done := make(chan bool, 1)
|
||||
@@ -90,15 +94,25 @@ func StartServer() {
|
||||
log.Info().Msg("Graceful shutdown complete.")
|
||||
}
|
||||
|
||||
// @title Firefly Manager API
|
||||
// @version 1.0
|
||||
// @description API to update Firefly Manager data
|
||||
// @host localhost:3344
|
||||
// @BasePath /
|
||||
// @title History API
|
||||
// @version 1.0
|
||||
// @description This is a sample server for History API.
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @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
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer " followed by a space and JWT token.
|
||||
func main() {
|
||||
StartServer()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// "history-api/internal/routes"
|
||||
// "history-api/internal/services"
|
||||
"database/sql"
|
||||
_ "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"
|
||||
middleware "github.com/gofiber/contrib/v3/zerolog"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,17 +36,21 @@ func NewHttpServer() *FiberServer {
|
||||
}
|
||||
cfg := swagger.Config{
|
||||
BasePath: "/",
|
||||
FilePath: "./docs/swagger.json",
|
||||
FileContent: docs.SwaggerJSON,
|
||||
Path: "swagger",
|
||||
Title: "Swagger API Docs",
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (s *FiberServer) RegisterFiberRoutes() {
|
||||
func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.Cache) {
|
||||
// Apply CORS middleware
|
||||
s.App.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
@@ -47,9 +60,21 @@ func (s *FiberServer) RegisterFiberRoutes() {
|
||||
MaxAge: 300,
|
||||
}))
|
||||
|
||||
// routes.UserRoutes(s.App)
|
||||
// routes.AuthRoutes(s.App)
|
||||
// routes.MediaRoute(s.App)
|
||||
// routes.NotFoundRoute(s.App)
|
||||
// repo setup
|
||||
userRepo := repositories.NewUserRepository(sqlPg, redis)
|
||||
roleRepo := repositories.NewRoleRepository(sqlPg, redis)
|
||||
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 (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
password_hash TEXT,
|
||||
google_id VARCHAR(255) UNIQUE,
|
||||
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||
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,
|
||||
verify_type SMALLINT NOT NULL, -- 1 = ID_CARD, 2 = EDUCATION, 3 = EXPERT
|
||||
document_url TEXT NOT NULL,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
status SMALLINT NOT NULL DEFAULT 1, -- 1 pending, 2 approved, 3 rejected
|
||||
reviewed_by UUID REFERENCES users(id),
|
||||
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_type ON user_verifications(user_id, verify_type);
|
||||
CREATE INDEX idx_user_verifications_status ON user_verifications(status);
|
||||
CREATE INDEX idx_user_verifications_user_id
|
||||
ON user_verifications(user_id)
|
||||
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
|
||||
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,
|
||||
token VARCHAR(255) NOT NULL UNIQUE,
|
||||
token_type SMALLINT NOT NULL,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
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
|
||||
ON user_tokens(user_id);
|
||||
ON user_tokens(user_id)
|
||||
WHERE is_deleted = false;
|
||||
|
||||
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
|
||||
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,
|
||||
thumbnail_url TEXT,
|
||||
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(),
|
||||
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
|
||||
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(),
|
||||
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id),
|
||||
title TEXT,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
note TEXT,
|
||||
content TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wiki_versions (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
wiki_id UUID REFERENCES wiki_pages(id) ON DELETE CASCADE,
|
||||
created_user UUID REFERENCES users(id),
|
||||
note TEXT,
|
||||
content TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
approved_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_wiki_entity
|
||||
ON wikis(entity_id)
|
||||
WHERE is_deleted = false;
|
||||
|
||||
CREATE INDEX idx_wiki_entity ON wiki_pages(entity_id);
|
||||
|
||||
CREATE TRIGGER trigger_wiki_pages_updated_at
|
||||
BEFORE UPDATE ON wiki_pages
|
||||
CREATE TRIGGER trigger_wikis_updated_at
|
||||
BEFORE UPDATE ON wikis
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
@@ -1,8 +1,11 @@
|
||||
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS geometries (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
geom GEOMETRY, -- point / polygon / line
|
||||
time_start INT,
|
||||
time_end INT,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
bbox GEOMETRY, -- optional
|
||||
created_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,
|
||||
created_user UUID REFERENCES users(id),
|
||||
geom GEOMETRY,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
note TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
approved_at TIMESTAMPTZ
|
||||
reviewed_by UUID REFERENCES users(id),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entity_geometries (
|
||||
@@ -24,8 +29,33 @@ CREATE TABLE IF NOT EXISTS entity_geometries (
|
||||
PRIMARY KEY (entity_id, geometry_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_geo_time ON geometries(time_start, time_end);
|
||||
CREATE INDEX idx_geom_spatial ON geometries USING GIST (geom);
|
||||
CREATE INDEX idx_geom_spatial_active
|
||||
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
|
||||
BEFORE UPDATE ON geometries
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
-- name: CreateUser :one
|
||||
-- name: UpsertUser :one
|
||||
INSERT INTO users (
|
||||
name,
|
||||
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 *;
|
||||
|
||||
-- name: CreateUserProfile :one
|
||||
INSERT INTO user_profiles (
|
||||
user_id,
|
||||
display_name,
|
||||
avatar_url
|
||||
) VALUES (
|
||||
$1, $2, $3, $4
|
||||
$1, $2, $3
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUser :one
|
||||
UPDATE users
|
||||
SET
|
||||
name = $1,
|
||||
avatar_url = $2,
|
||||
is_active = $3,
|
||||
is_verified = $4,
|
||||
-- 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 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;
|
||||
WHERE user_id = $9
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserPassword :exec
|
||||
UPDATE users
|
||||
@@ -52,7 +55,6 @@ SET
|
||||
WHERE id = $1
|
||||
AND is_deleted = false;
|
||||
|
||||
|
||||
-- name: VerifyUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
@@ -72,86 +74,133 @@ SET
|
||||
is_deleted = false
|
||||
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
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.avatar_url,
|
||||
u.is_active,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
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
|
||||
|
||||
-- profile JSON
|
||||
(
|
||||
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,
|
||||
|
||||
-- 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
|
||||
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
|
||||
GROUP BY u.id;
|
||||
WHERE u.id = $1 AND u.is_deleted = false;
|
||||
|
||||
-- name: GetTokenVersion :one
|
||||
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
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.avatar_url,
|
||||
u.is_active,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
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
|
||||
|
||||
(
|
||||
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
|
||||
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
|
||||
GROUP BY u.id;
|
||||
WHERE u.email = $1 AND u.is_deleted = false;
|
||||
|
||||
-- name: GetUsers :many
|
||||
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 (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
password_hash TEXT,
|
||||
google_id VARCHAR(255) UNIQUE,
|
||||
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,
|
||||
refresh_token TEXT,
|
||||
is_deleted BOOLEAN DEFAULT false,
|
||||
created_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,
|
||||
display_name TEXT,
|
||||
full_name TEXT,
|
||||
@@ -26,21 +26,10 @@ CREATE TABLE user_profiles (
|
||||
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 (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
is_deleted BOOLEAN DEFAULT false,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
created_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)
|
||||
);
|
||||
|
||||
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 (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
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,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
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
|
||||
container_name: history_db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./assets/resources/.env
|
||||
environment:
|
||||
POSTGRES_USER: history
|
||||
POSTGRES_PASSWORD: secret
|
||||
POSTGRES_DB: history_map
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- PGDATA=/var/lib/postgresql/data
|
||||
volumes:
|
||||
- ./pg_data:/var/lib/postgresql
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U history -d history_map"]
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- history-api-project
|
||||
|
||||
cache:
|
||||
image: redis:8.6.1-alpine
|
||||
container_name: history_redis
|
||||
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:
|
||||
- "6379:6379"
|
||||
- "3344:3344"
|
||||
networks:
|
||||
- history-api-project
|
||||
|
||||
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
|
||||
|
||||
require (
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/gofiber/contrib/v3/jwt v1.1.0
|
||||
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/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.49.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // 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/go-openapi/analysis v0.24.2 // 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/gofiber/schema v1.7.0 // 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/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // 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/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.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
|
||||
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/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||
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/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/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/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/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/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
|
||||
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/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
|
||||
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/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
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/swaggerui v1.0.1 h1:o3EdD0VQjeL4rq1gBxQB5bUEYMNT3eGKxpg2hjPIigI=
|
||||
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/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
|
||||
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
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.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||
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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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/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/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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
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/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
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.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
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.6.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.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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 {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type User 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"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
GoogleID pgtype.Text `json:"google_id"`
|
||||
AuthProvider string `json:"auth_provider"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
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"`
|
||||
}
|
||||
|
||||
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 {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
RoleID pgtype.UUID `json:"role_id"`
|
||||
@@ -40,7 +53,19 @@ type UserToken struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
TokenType int16 `json:"token_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_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"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
name,
|
||||
email,
|
||||
password_hash,
|
||||
const createUserProfile = `-- name: CreateUserProfile :one
|
||||
INSERT INTO user_profiles (
|
||||
user_id,
|
||||
display_name,
|
||||
avatar_url
|
||||
) 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 {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
type CreateUserProfileParams struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
DisplayName pgtype.Text `json:"display_name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||
row := q.db.QueryRow(ctx, createUser,
|
||||
arg.Name,
|
||||
arg.Email,
|
||||
arg.PasswordHash,
|
||||
arg.AvatarUrl,
|
||||
)
|
||||
var i User
|
||||
func (q *Queries) CreateUserProfile(ctx context.Context, arg CreateUserProfileParams) (UserProfile, error) {
|
||||
row := q.db.QueryRow(ctx, createUserProfile, arg.UserID, arg.DisplayName, arg.AvatarUrl)
|
||||
var i UserProfile
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.UserID,
|
||||
&i.DisplayName,
|
||||
&i.FullName,
|
||||
&i.AvatarUrl,
|
||||
&i.IsActive,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.Bio,
|
||||
&i.Location,
|
||||
&i.Website,
|
||||
&i.CountryCode,
|
||||
&i.Phone,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -58,8 +50,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
const deleteUser = `-- name: DeleteUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = true,
|
||||
updated_at = now()
|
||||
is_deleted = true
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
@@ -68,59 +59,69 @@ func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const existsUserByEmail = `-- name: ExistsUserByEmail :one
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE email = $1
|
||||
AND is_deleted = false
|
||||
)
|
||||
const getTokenVersion = `-- name: GetTokenVersion :one
|
||||
SELECT token_version
|
||||
FROM users
|
||||
WHERE id = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) ExistsUserByEmail(ctx context.Context, email string) (bool, error) {
|
||||
row := q.db.QueryRow(ctx, existsUserByEmail, email)
|
||||
var exists bool
|
||||
err := row.Scan(&exists)
|
||||
return exists, err
|
||||
func (q *Queries) GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, getTokenVersion, id)
|
||||
var token_version int32
|
||||
err := row.Scan(&token_version)
|
||||
return token_version, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.avatar_url,
|
||||
u.is_active,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
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
|
||||
|
||||
(
|
||||
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
|
||||
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
|
||||
GROUP BY u.id
|
||||
`
|
||||
|
||||
type GetUserByEmailRow 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"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Profile []byte `json:"profile"`
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
@@ -129,16 +130,14 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
||||
var i GetUserByEmailRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.AvatarUrl,
|
||||
&i.IsActive,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
)
|
||||
return i, err
|
||||
@@ -146,44 +145,58 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.avatar_url,
|
||||
u.is_active,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
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
|
||||
|
||||
-- profile JSON
|
||||
(
|
||||
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,
|
||||
|
||||
-- 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
|
||||
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
|
||||
GROUP BY u.id
|
||||
`
|
||||
|
||||
type GetUserByIDRow 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"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Profile []byte `json:"profile"`
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
@@ -192,17 +205,15 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
||||
var i GetUserByIDRow
|
||||
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.Profile,
|
||||
&i.Roles,
|
||||
)
|
||||
return i, err
|
||||
@@ -210,44 +221,56 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
||||
|
||||
const getUsers = `-- 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.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
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
|
||||
|
||||
(
|
||||
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
|
||||
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
|
||||
`
|
||||
|
||||
type GetUsersRow 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"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Profile []byte `json:"profile"`
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
@@ -262,17 +285,15 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
var i GetUsersRow
|
||||
if err := rows.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.Profile,
|
||||
&i.Roles,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -288,8 +309,7 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
const restoreUser = `-- name: RestoreUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = false,
|
||||
updated_at = now()
|
||||
is_deleted = false
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
@@ -298,99 +318,33 @@ func (q *Queries) RestoreUser(ctx context.Context, id pgtype.UUID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUser = `-- name: UpdateUser :one
|
||||
const updateTokenVersion = `-- name: UpdateTokenVersion :exec
|
||||
UPDATE users
|
||||
SET
|
||||
name = $1,
|
||||
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
|
||||
SET token_version = $2
|
||||
WHERE id = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
type UpdateUserParams struct {
|
||||
Name string `json:"name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
type UpdateTokenVersionParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
}
|
||||
|
||||
type UpdateUserRow 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 []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
|
||||
func (q *Queries) UpdateTokenVersion(ctx context.Context, arg UpdateTokenVersionParams) error {
|
||||
_, err := q.db.Exec(ctx, updateTokenVersion, arg.ID, arg.TokenVersion)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUserPassword = `-- name: UpdateUserPassword :exec
|
||||
UPDATE users
|
||||
SET
|
||||
password_hash = $2,
|
||||
updated_at = now()
|
||||
password_hash = $2
|
||||
WHERE id = $1
|
||||
AND is_deleted = false
|
||||
`
|
||||
|
||||
type UpdateUserPasswordParams struct {
|
||||
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 {
|
||||
@@ -398,11 +352,137 @@ func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPassword
|
||||
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
|
||||
UPDATE users
|
||||
SET
|
||||
is_verified = true,
|
||||
updated_at = now()
|
||||
is_verified = true
|
||||
WHERE id = $1
|
||||
AND is_deleted = false
|
||||
`
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/dtos/response"
|
||||
"slices"
|
||||
|
||||
jwtware "github.com/gofiber/contrib/v3/jwt"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/dtos/response"
|
||||
"slices"
|
||||
|
||||
"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
|
||||
|
||||
import (
|
||||
"history-api/pkg/dtos/response"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RoleSimple struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (r *RoleSimple) ToResponse() *response.RoleSimpleResponse {
|
||||
return &response.RoleSimpleResponse{
|
||||
ID: convert.UUIDToString(r.ID),
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
}
|
||||
}
|
||||
@@ -28,20 +27,20 @@ func RolesToResponse(rs []*RoleSimple) []*response.RoleSimpleResponse {
|
||||
}
|
||||
|
||||
type RoleEntity struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (r *RoleEntity) ToResponse() *response.RoleResponse {
|
||||
return &response.RoleResponse{
|
||||
ID: convert.UUIDToString(r.ID),
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
IsDeleted: convert.BoolVal(r.IsDeleted),
|
||||
CreatedAt: convert.TimeToPtr(r.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(r.UpdatedAt),
|
||||
IsDeleted: r.IsDeleted,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +51,15 @@ func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/convert"
|
||||
"history-api/pkg/dtos/response"
|
||||
|
||||
"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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/pkg/models"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type RoleRepository interface {
|
||||
@@ -25,43 +29,63 @@ type RoleRepository interface {
|
||||
|
||||
type roleRepository struct {
|
||||
q *sqlc.Queries
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewRoleRepository(db sqlc.DBTX) RoleRepository {
|
||||
func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
|
||||
return &roleRepository{
|
||||
q: sqlc.New(db),
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role = models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role = models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
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) {
|
||||
@@ -69,14 +93,19 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role := models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
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) {
|
||||
@@ -84,14 +113,20 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role := models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
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) {
|
||||
@@ -103,11 +138,11 @@ func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error)
|
||||
var users []*models.RoleEntity
|
||||
for _, row := range rows {
|
||||
user := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
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 {
|
||||
err := r.q.DeleteRole(ctx, id)
|
||||
return err
|
||||
role, err := r.GetByID(ctx, id)
|
||||
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 {
|
||||
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 {
|
||||
err := r.q.AddUserRole(ctx, params)
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *roleRepository) RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error {
|
||||
err := r.q.RemoveUserRole(ctx, params)
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *roleRepository) RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error {
|
||||
err := r.q.RemoveAllUsersFromRole(ctx, roleId)
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *roleRepository) RemoveAllRolesFromUser(ctx context.Context, roleId pgtype.UUID) error {
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/pkg/models"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
||||
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
|
||||
All(ctx context.Context) ([]*models.UserEntity, error)
|
||||
Create(ctx context.Context, params sqlc.CreateUserParams) (*models.UserEntity, error)
|
||||
Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error)
|
||||
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
|
||||
ExistEmail(ctx context.Context, email string) (bool, 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
|
||||
UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error)
|
||||
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
|
||||
UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error)
|
||||
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
|
||||
UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error
|
||||
GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, 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 {
|
||||
q *sqlc.Queries
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewUserRepository(db sqlc.DBTX) UserRepository {
|
||||
func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
|
||||
return &userRepository{
|
||||
q: sqlc.New(db),
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
user = models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
|
||||
user = models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
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) {
|
||||
row, err := r.q.CreateUser(ctx, params)
|
||||
func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error) {
|
||||
row, err := r.q.UpsertUser(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
RefreshToken: row.RefreshToken,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
Roles: make([]*models.RoleSimple, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error) {
|
||||
row, err := r.q.UpdateUser(ctx, params)
|
||||
func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error) {
|
||||
user, err := r.GetByID(ctx, params.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
|
||||
row, err := r.q.UpdateUserProfile(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile := 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),
|
||||
}
|
||||
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
user.Profile = &profile
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
rows, err := r.q.GetUsers(ctx)
|
||||
if err != nil {
|
||||
@@ -141,23 +192,24 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
|
||||
var users []*models.UserEntity
|
||||
for _, row := range rows {
|
||||
user := &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := user.ParseProfile(row.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
err := r.q.VerifyUser(ctx, id)
|
||||
return err
|
||||
user, err := r.GetByID(ctx, id)
|
||||
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 {
|
||||
err := r.q.DeleteUser(ctx, id)
|
||||
return err
|
||||
user, err := r.GetByID(ctx, id)
|
||||
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 {
|
||||
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 {
|
||||
err := r.q.UpdateUserPassword(ctx, params)
|
||||
return err
|
||||
user, err := r.GetByID(ctx, params.ID)
|
||||
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) {
|
||||
row, err := r.q.ExistsUserByEmail(ctx, email)
|
||||
func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error {
|
||||
user, err := r.GetByID(ctx, params.ID)
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"history-api/pkg/config"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
connectionURI, err := config.GetConfig("REDIS_CONNECTION_URI")
|
||||
type RedisClient struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func NewRedisClient() (Cache, error) {
|
||||
uri, err := config.GetConfig("REDIS_CONNECTION_URI")
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: connectionURI,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
Addr: uri,
|
||||
MinIdleConns: 10,
|
||||
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 {
|
||||
return fmt.Errorf("Could not connect to Redis: %v", err)
|
||||
return nil, fmt.Errorf("could not connect to Redis: %v", err)
|
||||
}
|
||||
|
||||
RI = rdb
|
||||
return nil
|
||||
return &RedisClient{client: rdb}, 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
|
||||
}
|
||||
|
||||
func (r Role) ToSlice() []string {
|
||||
return []string{r.String()}
|
||||
func ParseRole(s string) (Role, bool) {
|
||||
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
|
||||
|
||||
import "github.com/jackc/pgx/v5/pgtype"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func UUIDToString(v pgtype.UUID) string {
|
||||
if v.Valid {
|
||||
@@ -23,10 +27,10 @@ func BoolVal(v pgtype.Bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func TimeToPtr(v pgtype.Timestamptz) *string {
|
||||
if v.Valid {
|
||||
t := v.Time.Format("2006-01-02T15:04:05Z07:00")
|
||||
return &t
|
||||
func TimeToPtr(v pgtype.Timestamptz) *time.Time {
|
||||
if !v.Valid {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
t := v.Time
|
||||
return &t
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func Connect() (*pgxpool.Pool, error) {
|
||||
func NewPostgresqlDB() (*pgxpool.Pool, error) {
|
||||
ctx := context.Background()
|
||||
connectionURI, err := config.GetConfig("PGX_CONNECTION_URI")
|
||||
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.Tag = fieldError.Tag()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -79,4 +89,4 @@ func ValidateBodyDto(c fiber.Ctx, dto any) error {
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user