UPDATE: Auth module, User module
Some checks failed
Build and Release / release (push) Failing after 1m25s
Some checks failed
Build and Release / release (push) Failing after 1m25s
This commit is contained in:
@@ -9,8 +9,8 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||||
go build -trimpath -ldflags="-s -w" -o history-api ./cmd/history-api
|
||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o history-api ./cmd/api
|
||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o email-worker ./cmd/worker/email
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
@@ -20,9 +20,10 @@ ENV TZ=Asia/Ho_Chi_Minh
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/history-api .
|
||||
COPY --from=builder /app/email-worker .
|
||||
COPY data ./data
|
||||
|
||||
RUN chmod +x ./history-api
|
||||
RUN chmod +x ./history-api ./email-worker
|
||||
|
||||
EXPOSE 3344
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -1,6 +1,6 @@
|
||||
DB_URL ?= postgres://history:secret@localhost:5432/history_map?sslmode=disable
|
||||
APP_DIR = cmd/history-api
|
||||
MAIN_APP = ./cmd/history-api/
|
||||
APP_DIR = cmd/api
|
||||
MAIN_APP = ./cmd/api/
|
||||
MAIN_FILE = $(APP_DIR)/main.go
|
||||
DOCS_DIR = docs
|
||||
|
||||
|
||||
33
assets/resources/email_verify.html
Normal file
33
assets/resources/email_verify.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Verification</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333333; }
|
||||
.container { max-width: 600px; margin: 40px auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); overflow: hidden; }
|
||||
.header { background-color: #4F46E5; padding: 30px; text-align: center; color: #ffffff; }
|
||||
.header h1 { margin: 0; font-size: 24px; font-weight: 600; }
|
||||
.content { padding: 40px 30px; text-align: center; }
|
||||
.content p { font-size: 16px; line-height: 1.6; color: #555555; margin-bottom: 25px; }
|
||||
.otp-box { display: inline-block; background-color: #F3F4F6; padding: 15px 30px; border-radius: 6px; letter-spacing: 5px; font-size: 32px; font-weight: bold; color: #111827; margin-bottom: 25px;}
|
||||
.footer { background-color: #f9fafb; padding: 20px; text-align: center; font-size: 13px; color: #9ca3af; border-top: 1px solid #eeeeee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Welcome!</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Thank you for registering. Please use the OTP below to verify your email address. This code will expire in a few minutes.</p>
|
||||
<div class="otp-box">{{OTP_CODE}}</div>
|
||||
<p>If you did not make this request, please safely ignore this email.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© 2026 Black Cat Studio. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
33
assets/resources/password_reset.html
Normal file
33
assets/resources/password_reset.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Password Reset</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333333; }
|
||||
.container { max-width: 600px; margin: 40px auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); overflow: hidden; }
|
||||
.header { background-color: #DC2626; padding: 30px; text-align: center; color: #ffffff; }
|
||||
.header h1 { margin: 0; font-size: 24px; font-weight: 600; }
|
||||
.content { padding: 40px 30px; text-align: center; }
|
||||
.content p { font-size: 16px; line-height: 1.6; color: #555555; margin-bottom: 25px; }
|
||||
.otp-box { display: inline-block; background-color: #FEF2F2; border: 1px dashed #F87171; padding: 15px 30px; border-radius: 6px; letter-spacing: 5px; font-size: 32px; font-weight: bold; color: #991B1B; margin-bottom: 25px;}
|
||||
.footer { background-color: #f9fafb; padding: 20px; text-align: center; font-size: 13px; color: #9ca3af; border-top: 1px solid #eeeeee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Password Reset</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>We received a request to reset the password for your account. Here is your secure OTP:</p>
|
||||
<div class="otp-box">{{OTP_CODE}}</div>
|
||||
<p><strong>Warning:</strong> Never share this code with anyone. If you didn't request a password reset, please ignore this email or contact support immediately.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© 2026 Black Cat Studio. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -64,17 +64,21 @@ func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.
|
||||
userRepo := repositories.NewUserRepository(sqlPg, redis)
|
||||
roleRepo := repositories.NewRoleRepository(sqlPg, redis)
|
||||
tileRepo := repositories.NewTileRepository(sqlTile, redis)
|
||||
tokenRepo := repositories.NewTokenRepository(redis)
|
||||
|
||||
// service setup
|
||||
authService := services.NewAuthService(userRepo, roleRepo)
|
||||
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis)
|
||||
userService := services.NewUserService(userRepo, roleRepo)
|
||||
tileService := services.NewTileService(tileRepo)
|
||||
|
||||
// controller setup
|
||||
authController := controllers.NewAuthController(authService)
|
||||
userController := controllers.NewUserController(userService)
|
||||
tileController := controllers.NewTileController(tileService)
|
||||
|
||||
// route setup
|
||||
routes.AuthRoutes(s.App, authController)
|
||||
routes.AuthRoutes(s.App, authController, userRepo)
|
||||
routes.UserRoutes(s.App, userController, userRepo)
|
||||
routes.TileRoutes(s.App, tileController)
|
||||
routes.NotFoundRoute(s.App)
|
||||
}
|
||||
111
cmd/worker/email/main.go
Normal file
111
cmd/worker/email/main.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/email"
|
||||
_ "history-api/pkg/log"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func runSingleWorker(ctx context.Context, rdb *redis.Client, consumerID int) {
|
||||
consumerName := "worker-" + strconv.Itoa(consumerID)
|
||||
|
||||
log.Info().Str("worker", consumerName).Msg("Worker started and ready")
|
||||
|
||||
for {
|
||||
entries, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
|
||||
Group: constants.GroupEmailName,
|
||||
Consumer: consumerName,
|
||||
Streams: []string{constants.StreamEmailName, ">"},
|
||||
Count: 1,
|
||||
Block: 0,
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("worker", consumerName).Msg("Failed to read stream")
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, stream := range entries {
|
||||
for _, message := range stream.Messages {
|
||||
taskType := message.Values["task_type"].(string)
|
||||
payloadStr := message.Values["payload"].(string)
|
||||
|
||||
if taskType == constants.TaskTypeSendEmailOTP.String() {
|
||||
var data models.TokenEntity
|
||||
if err := json.Unmarshal([]byte(payloadStr), &data); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to unmarshal payload")
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("worker", consumerName).
|
||||
Str("email", data.Email).
|
||||
Msg("Processing email task")
|
||||
|
||||
errSend := email.SendMailOTP(data.Email, data.Token, data.TokenType)
|
||||
if errSend != nil {
|
||||
log.Error().Err(errSend).Str("email", data.Email).Msg("Failed to send email")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
rdb.XAck(ctx, constants.StreamEmailName, constants.GroupEmailName, message.ID)
|
||||
log.Info().Str("msg_id", message.ID).Msg("Task acknowledged")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func main() {
|
||||
|
||||
config.LoadEnv()
|
||||
|
||||
workerCountStr := config.GetConfigWithDefault("EMAIL_WORKER_COUNT", "1")
|
||||
workerCount, err := strconv.Atoi(workerCountStr)
|
||||
if err != nil || workerCount <= 0 {
|
||||
workerCount = 1
|
||||
}
|
||||
|
||||
cacheInterface, err := cache.NewRedisClient()
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to connect to Redis")
|
||||
}
|
||||
|
||||
rdb := cacheInterface.GetRawClient()
|
||||
ctx := context.Background()
|
||||
|
||||
err = rdb.XGroupCreateMkStream(ctx, constants.StreamEmailName, constants.GroupEmailName, "$").Err()
|
||||
if err != nil && err.Error() != "BUSYGROUP Consumer Group name already exists" {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to create Redis Stream Group")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("worker_count", workerCount).
|
||||
Msg("Starting email worker system")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 1; i <= workerCount; i++ {
|
||||
wg.Go(func() {
|
||||
runSingleWorker(ctx, rdb, i)
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -6,7 +6,6 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
password_hash TEXT,
|
||||
google_id VARCHAR(255) UNIQUE,
|
||||
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
token_version INT NOT NULL DEFAULT 1,
|
||||
refresh_token TEXT,
|
||||
@@ -22,10 +21,6 @@ CREATE INDEX idx_users_email_active
|
||||
ON users (email)
|
||||
WHERE is_deleted = false;
|
||||
|
||||
CREATE INDEX idx_users_verified
|
||||
ON users (is_verified)
|
||||
WHERE is_deleted = false;
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS user_tokens;
|
||||
@@ -1,25 +0,0 @@
|
||||
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,
|
||||
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)
|
||||
WHERE is_deleted = false;
|
||||
|
||||
CREATE INDEX idx_user_tokens_user_id
|
||||
ON user_tokens(user_id)
|
||||
WHERE is_deleted = false;
|
||||
|
||||
CREATE INDEX idx_user_tokens_type
|
||||
ON user_tokens(token_type)
|
||||
WHERE is_deleted = false;
|
||||
|
||||
CREATE INDEX idx_user_tokens_expires_at
|
||||
ON user_tokens(expires_at)
|
||||
WHERE is_deleted = false;
|
||||
@@ -11,11 +11,14 @@ WHERE name = $1 AND is_deleted = false;
|
||||
SELECT id, name, is_deleted, created_at, updated_at FROM roles
|
||||
WHERE id = $1 AND is_deleted = false;
|
||||
|
||||
-- name: GetRolesByIDs :many
|
||||
SELECT id, name, is_deleted, created_at, updated_at
|
||||
FROM roles
|
||||
WHERE id = ANY($1::uuid[]) AND is_deleted = false;
|
||||
|
||||
-- name: AddUserRole :exec
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT $1, r.id
|
||||
FROM roles r
|
||||
WHERE r.name = $2
|
||||
SELECT $1, unnest($2::uuid[])
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- name: RemoveUserRole :exec
|
||||
|
||||
@@ -3,17 +3,14 @@ INSERT INTO users (
|
||||
email,
|
||||
password_hash,
|
||||
google_id,
|
||||
auth_provider,
|
||||
is_verified
|
||||
auth_provider
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
$1, $2, $3, $4
|
||||
)
|
||||
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()
|
||||
auth_provider = EXCLUDED.auth_provider
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateUserProfile :one
|
||||
@@ -55,12 +52,6 @@ SET
|
||||
WHERE id = $1
|
||||
AND is_deleted = false;
|
||||
|
||||
-- name: VerifyUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_verified = true
|
||||
WHERE id = $1
|
||||
AND is_deleted = false;
|
||||
|
||||
-- name: DeleteUser :exec
|
||||
UPDATE users
|
||||
@@ -79,7 +70,6 @@ SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
@@ -116,6 +106,47 @@ SELECT
|
||||
FROM users u
|
||||
WHERE u.id = $1 AND u.is_deleted = false;
|
||||
|
||||
-- name: GetUserByIDWithoutDeleted :one
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
|
||||
-- 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
|
||||
WHERE u.id = $1;
|
||||
|
||||
-- name: GetTokenVersion :one
|
||||
SELECT token_version
|
||||
FROM users
|
||||
@@ -131,7 +162,6 @@ SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
@@ -170,7 +200,59 @@ 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,
|
||||
|
||||
-- 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
|
||||
WHERE
|
||||
(sqlc.narg('cursor')::uuid IS NULL OR u.id > sqlc.narg('cursor')::uuid)
|
||||
AND (sqlc.narg('is_deleted')::boolean IS NULL OR u.is_deleted = sqlc.narg('is_deleted')::boolean)
|
||||
AND (
|
||||
sqlc.narg('role_ids')::uuid[] IS NULL OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_roles ur2
|
||||
WHERE ur2.user_id = u.id AND ur2.role_id = ANY(sqlc.narg('role_ids')::uuid[])
|
||||
)
|
||||
)
|
||||
ORDER BY u.id ASC
|
||||
LIMIT sqlc.arg('limit');
|
||||
|
||||
|
||||
-- name: SearchUsers :many
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
@@ -203,4 +285,26 @@ SELECT
|
||||
) AS roles
|
||||
|
||||
FROM users u
|
||||
WHERE u.is_deleted = false;
|
||||
WHERE
|
||||
(sqlc.narg('cursor')::uuid IS NULL OR u.id > sqlc.narg('cursor')::uuid)
|
||||
|
||||
AND (sqlc.narg('is_deleted')::boolean IS NULL OR u.is_deleted = sqlc.narg('is_deleted')::boolean)
|
||||
AND (
|
||||
sqlc.narg('role_ids')::uuid[] IS NULL OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_roles ur2
|
||||
WHERE ur2.user_id = u.id AND ur2.role_id = ANY(sqlc.narg('role_ids')::uuid[])
|
||||
)
|
||||
)
|
||||
|
||||
AND (sqlc.narg('search_id')::uuid IS NULL OR u.id = sqlc.narg('search_id')::uuid)
|
||||
AND (
|
||||
sqlc.narg('search_text')::text IS NULL OR
|
||||
u.email ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_profiles p
|
||||
WHERE p.user_id = u.id AND p.display_name ILIKE '%' || sqlc.narg('search_text')::text || '%'
|
||||
)
|
||||
)
|
||||
ORDER BY u.id ASC
|
||||
LIMIT sqlc.arg('limit');
|
||||
@@ -4,7 +4,6 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
password_hash TEXT,
|
||||
google_id VARCHAR(255) UNIQUE,
|
||||
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
token_version INT NOT NULL DEFAULT 1,
|
||||
refresh_token TEXT,
|
||||
@@ -50,13 +49,3 @@ CREATE TABLE IF NOT EXISTS user_verifications (
|
||||
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,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
token_type SMALLINT NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
@@ -13,27 +13,25 @@ services:
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- history-api-project
|
||||
|
||||
cache:
|
||||
image: redis:8.6.1-alpine
|
||||
image: redis:8.6.2-alpine
|
||||
container_name: history_redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- history-api-project
|
||||
|
||||
migrate:
|
||||
build:
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM migrate/migrate
|
||||
COPY db/migrations /migrations
|
||||
image: migrate/migrate
|
||||
container_name: history_migrate
|
||||
volumes:
|
||||
- ./db/migrations:/migrations:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -43,14 +41,15 @@ services:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
DB_URL="postgres://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@db:5432/$${POSTGRES_DB}?sslmode=disable"
|
||||
# Thêm ls để kiểm tra chắc chắn lúc chạy
|
||||
ls /migrations
|
||||
/migrate -path /migrations -database "$$DB_URL" up || \
|
||||
(/migrate -path /migrations -database "$$DB_URL" force 8 && \
|
||||
/migrate -path /migrations -database "$$DB_URL" up)
|
||||
DB_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable"
|
||||
|
||||
echo "Running migrations..."
|
||||
# We skip the 'version' check loop because 'depends_on'
|
||||
# with 'service_healthy' already handles the wait.
|
||||
/migrate -path /migrations -database "$$DB_URL" up
|
||||
networks:
|
||||
- history-api-project
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: history_app
|
||||
@@ -58,13 +57,32 @@ services:
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
db:
|
||||
condition: service_healthy
|
||||
cache:
|
||||
condition: service_started
|
||||
env_file:
|
||||
- ./assets/resources/.env
|
||||
ports:
|
||||
- "3344:3344"
|
||||
networks:
|
||||
- history-api-project
|
||||
|
||||
worker:
|
||||
build: .
|
||||
container_name: history_worker
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
cache:
|
||||
condition: service_started
|
||||
env_file:
|
||||
- ./assets/resources/.env
|
||||
command: ["./email-worker"]
|
||||
networks:
|
||||
- history-api-project
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
|
||||
|
||||
839
docs/docs.go
839
docs/docs.go
@@ -24,14 +24,9 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/auth/refresh": {
|
||||
"/auth/forgot-password": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get a new access token using the user's current session/refresh token",
|
||||
"description": "Initiate password recovery process for a user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -41,44 +36,15 @@ const docTemplate = `{
|
||||
"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",
|
||||
"summary": "Handle forgotten password",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sign In request",
|
||||
"description": "Forgot Password request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.SignInDto"
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.ForgotPasswordDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -104,9 +70,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/signup": {
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"description": "Create a new user account",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate a new access token using a valid refresh token from context",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -116,10 +87,97 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Sign up a new user",
|
||||
"summary": "Refresh session tokens",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized or expired refresh token",
|
||||
"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 credentials and return access/refresh tokens",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Sign in a user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sign Up request",
|
||||
"description": "Sign In credentials",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid credentials",
|
||||
"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 in the system",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Register a new user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sign Up details",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -150,6 +208,98 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/token/create": {
|
||||
"post": {
|
||||
"description": "Request a new token for specific actions like email confirmation",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Generate a new verification token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Token creation request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.CreateTokenDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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/token/verify": {
|
||||
"post": {
|
||||
"description": "Validate an OTP or email verification token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Verify a security token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Token verification data",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.VerifyTokenDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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",
|
||||
@@ -233,9 +383,524 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Search and filter users with pagination (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Search users",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "cursor",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "is_deleted",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string",
|
||||
"name": "order",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"name": "role_ids",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maxLength": 200,
|
||||
"minLength": 2,
|
||||
"type": "string",
|
||||
"name": "search",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"email",
|
||||
"display_name"
|
||||
],
|
||||
"type": "string",
|
||||
"name": "sort",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/current": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Retrieve the profile information of the currently authenticated user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get current user profile",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Retrieve details of a specific user (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get user by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update the profile details of the currently authenticated user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Update user profile",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update Profile request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.UpdateProfileDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Soft delete a user account (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Delete a user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}/password": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update the password for the currently authenticated user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Change user password",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Change Password request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.ChangePasswordDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}/restore": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Restore a soft-deleted user account (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Restore a deleted user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}/role": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update the role of a user (Admin only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Change user role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Change Role request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.ChangeRoleDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"history-api_internal_dtos_request.ChangePasswordDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_password",
|
||||
"old_password"
|
||||
],
|
||||
"properties": {
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
},
|
||||
"old_password": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.ChangeRoleDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"role_ids",
|
||||
"user_id"
|
||||
],
|
||||
"properties": {
|
||||
"role_ids": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.CreateTokenDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"token_type"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/history-api_pkg_constants.TokenType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.ForgotPasswordDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"new_password",
|
||||
"token_id"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 5
|
||||
},
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
},
|
||||
"token_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.SignInDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -260,7 +925,8 @@ const docTemplate = `{
|
||||
"required": [
|
||||
"display_name",
|
||||
"email",
|
||||
"password"
|
||||
"password",
|
||||
"token_id"
|
||||
],
|
||||
"properties": {
|
||||
"display_name": {
|
||||
@@ -277,6 +943,75 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
},
|
||||
"token_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.UpdateProfileDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"bio": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"country_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 2
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string",
|
||||
"maxLength": 100,
|
||||
"minLength": 2
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"maxLength": 20,
|
||||
"minLength": 8
|
||||
},
|
||||
"website": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.VerifyTokenDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"token",
|
||||
"token_type"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/history-api_pkg_constants.TokenType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -291,6 +1026,22 @@ const docTemplate = `{
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_pkg_constants.TokenType": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TokenPasswordReset",
|
||||
"TokenEmailVerify",
|
||||
"TokenMagicLink",
|
||||
"TokenRefreshToken"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
||||
@@ -22,14 +22,9 @@
|
||||
"host": "history-api.kain.id.vn",
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/auth/refresh": {
|
||||
"/auth/forgot-password": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get a new access token using the user's current session/refresh token",
|
||||
"description": "Initiate password recovery process for a user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -39,44 +34,15 @@
|
||||
"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",
|
||||
"summary": "Handle forgotten password",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sign In request",
|
||||
"description": "Forgot Password request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.SignInDto"
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.ForgotPasswordDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -102,9 +68,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/signup": {
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"description": "Create a new user account",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate a new access token using a valid refresh token from context",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -114,10 +85,97 @@
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Sign up a new user",
|
||||
"summary": "Refresh session tokens",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized or expired refresh token",
|
||||
"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 credentials and return access/refresh tokens",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Sign in a user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sign Up request",
|
||||
"description": "Sign In credentials",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid credentials",
|
||||
"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 in the system",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Register a new user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sign Up details",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -148,6 +206,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/token/create": {
|
||||
"post": {
|
||||
"description": "Request a new token for specific actions like email confirmation",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Generate a new verification token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Token creation request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.CreateTokenDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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/token/verify": {
|
||||
"post": {
|
||||
"description": "Validate an OTP or email verification token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Verify a security token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Token verification data",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.VerifyTokenDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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",
|
||||
@@ -231,9 +381,524 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Search and filter users with pagination (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Search users",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "cursor",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "is_deleted",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string",
|
||||
"name": "order",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"name": "role_ids",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maxLength": 200,
|
||||
"minLength": 2,
|
||||
"type": "string",
|
||||
"name": "search",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"email",
|
||||
"display_name"
|
||||
],
|
||||
"type": "string",
|
||||
"name": "sort",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/current": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Retrieve the profile information of the currently authenticated user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get current user profile",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Retrieve details of a specific user (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get user by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update the profile details of the currently authenticated user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Update user profile",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update Profile request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.UpdateProfileDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Soft delete a user account (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Delete a user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}/password": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update the password for the currently authenticated user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Change user password",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Change Password request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.ChangePasswordDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}/restore": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Restore a soft-deleted user account (Admin/Mod only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Restore a deleted user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}/role": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update the role of a user (Admin only)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Change user role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Change Role request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/history-api_internal_dtos_request.ChangeRoleDto"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"history-api_internal_dtos_request.ChangePasswordDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_password",
|
||||
"old_password"
|
||||
],
|
||||
"properties": {
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
},
|
||||
"old_password": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.ChangeRoleDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"role_ids",
|
||||
"user_id"
|
||||
],
|
||||
"properties": {
|
||||
"role_ids": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.CreateTokenDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"token_type"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/history-api_pkg_constants.TokenType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.ForgotPasswordDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"new_password",
|
||||
"token_id"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 5
|
||||
},
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
},
|
||||
"token_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.SignInDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -258,7 +923,8 @@
|
||||
"required": [
|
||||
"display_name",
|
||||
"email",
|
||||
"password"
|
||||
"password",
|
||||
"token_id"
|
||||
],
|
||||
"properties": {
|
||||
"display_name": {
|
||||
@@ -275,6 +941,75 @@
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 8
|
||||
},
|
||||
"token_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.UpdateProfileDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"bio": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"country_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 2
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string",
|
||||
"maxLength": 100,
|
||||
"minLength": 2
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"maxLength": 20,
|
||||
"minLength": 8
|
||||
},
|
||||
"website": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_internal_dtos_request.VerifyTokenDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"token",
|
||||
"token_type"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/history-api_pkg_constants.TokenType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -289,6 +1024,22 @@
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history-api_pkg_constants.TokenType": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TokenPasswordReset",
|
||||
"TokenEmailVerify",
|
||||
"TokenMagicLink",
|
||||
"TokenRefreshToken"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
basePath: /
|
||||
definitions:
|
||||
history-api_internal_dtos_request.ChangePasswordDto:
|
||||
properties:
|
||||
new_password:
|
||||
maxLength: 64
|
||||
minLength: 8
|
||||
type: string
|
||||
old_password:
|
||||
maxLength: 64
|
||||
minLength: 8
|
||||
type: string
|
||||
required:
|
||||
- new_password
|
||||
- old_password
|
||||
type: object
|
||||
history-api_internal_dtos_request.ChangeRoleDto:
|
||||
properties:
|
||||
role_ids:
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
type: array
|
||||
user_id:
|
||||
type: string
|
||||
required:
|
||||
- role_ids
|
||||
- user_id
|
||||
type: object
|
||||
history-api_internal_dtos_request.CreateTokenDto:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
token_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/history-api_pkg_constants.TokenType'
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
required:
|
||||
- email
|
||||
- token_type
|
||||
type: object
|
||||
history-api_internal_dtos_request.ForgotPasswordDto:
|
||||
properties:
|
||||
email:
|
||||
maxLength: 255
|
||||
minLength: 5
|
||||
type: string
|
||||
new_password:
|
||||
maxLength: 64
|
||||
minLength: 8
|
||||
type: string
|
||||
token_id:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- new_password
|
||||
- token_id
|
||||
type: object
|
||||
history-api_internal_dtos_request.SignInDto:
|
||||
properties:
|
||||
email:
|
||||
@@ -28,10 +88,59 @@ definitions:
|
||||
maxLength: 64
|
||||
minLength: 8
|
||||
type: string
|
||||
token_id:
|
||||
type: string
|
||||
required:
|
||||
- display_name
|
||||
- email
|
||||
- password
|
||||
- token_id
|
||||
type: object
|
||||
history-api_internal_dtos_request.UpdateProfileDto:
|
||||
properties:
|
||||
avatar_url:
|
||||
type: string
|
||||
bio:
|
||||
maxLength: 255
|
||||
type: string
|
||||
country_code:
|
||||
type: string
|
||||
display_name:
|
||||
maxLength: 50
|
||||
minLength: 2
|
||||
type: string
|
||||
full_name:
|
||||
maxLength: 100
|
||||
minLength: 2
|
||||
type: string
|
||||
location:
|
||||
maxLength: 100
|
||||
type: string
|
||||
phone:
|
||||
maxLength: 20
|
||||
minLength: 8
|
||||
type: string
|
||||
website:
|
||||
type: string
|
||||
type: object
|
||||
history-api_internal_dtos_request.VerifyTokenDto:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
token_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/history-api_pkg_constants.TokenType'
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
required:
|
||||
- email
|
||||
- token
|
||||
- token_type
|
||||
type: object
|
||||
history-api_internal_dtos_response.CommonResponse:
|
||||
properties:
|
||||
@@ -41,6 +150,19 @@ definitions:
|
||||
status:
|
||||
type: boolean
|
||||
type: object
|
||||
history-api_pkg_constants.TokenType:
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
format: int32
|
||||
type: integer
|
||||
x-enum-varnames:
|
||||
- TokenPasswordReset
|
||||
- TokenEmailVerify
|
||||
- TokenMagicLink
|
||||
- TokenRefreshToken
|
||||
host: history-api.kain.id.vn
|
||||
info:
|
||||
contact:
|
||||
@@ -55,12 +177,18 @@ info:
|
||||
title: History API
|
||||
version: "1.0"
|
||||
paths:
|
||||
/auth/refresh:
|
||||
/auth/forgot-password:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get a new access token using the user's current session/refresh
|
||||
token
|
||||
description: Initiate password recovery process for a user
|
||||
parameters:
|
||||
- description: Forgot Password request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_request.ForgotPasswordDto'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -68,22 +196,49 @@ paths:
|
||||
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: Handle forgotten password
|
||||
tags:
|
||||
- Auth
|
||||
/auth/refresh:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Generate a new access token using a valid refresh token from context
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||
"401":
|
||||
description: Unauthorized or expired refresh token
|
||||
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
|
||||
summary: Refresh session tokens
|
||||
tags:
|
||||
- Auth
|
||||
/auth/signin:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Authenticate user and return token data
|
||||
description: Authenticate user credentials and return access/refresh tokens
|
||||
parameters:
|
||||
- description: Sign In request
|
||||
- description: Sign In credentials
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
@@ -100,20 +255,24 @@ paths:
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||
"401":
|
||||
description: Invalid credentials
|
||||
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
|
||||
summary: Sign in a user
|
||||
tags:
|
||||
- Auth
|
||||
/auth/signup:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create a new user account
|
||||
description: Create a new user account in the system
|
||||
parameters:
|
||||
- description: Sign Up request
|
||||
- description: Sign Up details
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
@@ -134,7 +293,67 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||
summary: Sign up a new user
|
||||
summary: Register a new user
|
||||
tags:
|
||||
- Auth
|
||||
/auth/token/create:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Request a new token for specific actions like email confirmation
|
||||
parameters:
|
||||
- description: Token creation request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_request.CreateTokenDto'
|
||||
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: Generate a new verification token
|
||||
tags:
|
||||
- Auth
|
||||
/auth/token/verify:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Validate an OTP or email verification token
|
||||
parameters:
|
||||
- description: Token verification data
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_request.VerifyTokenDto'
|
||||
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: Verify a security token
|
||||
tags:
|
||||
- Auth
|
||||
/tiles/{z}/{x}/{y}:
|
||||
@@ -193,6 +412,281 @@ paths:
|
||||
summary: Get tile metadata
|
||||
tags:
|
||||
- Tile
|
||||
/users:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Search and filter users with pagination (Admin/Mod only)
|
||||
parameters:
|
||||
- in: query
|
||||
name: cursor
|
||||
type: string
|
||||
- in: query
|
||||
name: is_deleted
|
||||
type: boolean
|
||||
- in: query
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
name: limit
|
||||
required: true
|
||||
type: integer
|
||||
- enum:
|
||||
- asc
|
||||
- desc
|
||||
in: query
|
||||
name: order
|
||||
type: string
|
||||
- collectionFormat: csv
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: role_ids
|
||||
type: array
|
||||
- in: query
|
||||
maxLength: 200
|
||||
minLength: 2
|
||||
name: search
|
||||
type: string
|
||||
- enum:
|
||||
- created_at
|
||||
- updated_at
|
||||
- email
|
||||
- display_name
|
||||
in: query
|
||||
name: sort
|
||||
type: string
|
||||
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'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Search users
|
||||
tags:
|
||||
- Users
|
||||
/users/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Soft delete a user account (Admin/Mod only)
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
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: Delete a user
|
||||
tags:
|
||||
- Users
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Retrieve details of a specific user (Admin/Mod only)
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
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: Get user by ID
|
||||
tags:
|
||||
- Users
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update the profile details of the currently authenticated user
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Update Profile request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_request.UpdateProfileDto'
|
||||
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'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Update user profile
|
||||
tags:
|
||||
- Users
|
||||
/users/{id}/password:
|
||||
patch:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update the password for the currently authenticated user
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Change Password request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_request.ChangePasswordDto'
|
||||
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'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Change user password
|
||||
tags:
|
||||
- Users
|
||||
/users/{id}/restore:
|
||||
patch:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Restore a soft-deleted user account (Admin/Mod only)
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
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: Restore a deleted user
|
||||
tags:
|
||||
- Users
|
||||
/users/{id}/role:
|
||||
patch:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update the role of a user (Admin only)
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Change Role request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/history-api_internal_dtos_request.ChangeRoleDto'
|
||||
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'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Change user role
|
||||
tags:
|
||||
- Users
|
||||
/users/current:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Retrieve the profile information of the currently authenticated
|
||||
user
|
||||
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: Get current user profile
|
||||
tags:
|
||||
- Users
|
||||
schemes:
|
||||
- https
|
||||
- http
|
||||
|
||||
3
go.mod
3
go.mod
@@ -10,11 +10,13 @@ require (
|
||||
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/google/uuid v1.6.0
|
||||
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
|
||||
github.com/wneessen/go-mail v0.7.2
|
||||
golang.org/x/crypto v0.49.0
|
||||
)
|
||||
|
||||
@@ -49,7 +51,6 @@ 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/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
|
||||
|
||||
2
go.sum
2
go.sum
@@ -158,6 +158,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
|
||||
@@ -20,14 +20,15 @@ func NewAuthController(svc services.AuthService) *AuthController {
|
||||
}
|
||||
|
||||
// Signin godoc
|
||||
// @Summary Sign in an existing user
|
||||
// @Description Authenticate user and return token data
|
||||
// @Summary Sign in a user
|
||||
// @Description Authenticate user credentials and return access/refresh tokens
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.SignInDto true "Sign In request"
|
||||
// @Param request body request.SignInDto true "Sign In credentials"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 401 {object} response.CommonResponse "Invalid credentials"
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/signin [post]
|
||||
func (h *AuthController) Signin(c fiber.Ctx) error {
|
||||
@@ -57,12 +58,12 @@ func (h *AuthController) Signin(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Signup godoc
|
||||
// @Summary Sign up a new user
|
||||
// @Description Create a new user account
|
||||
// @Summary Register a new user
|
||||
// @Description Create a new user account in the system
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.SignUpDto true "Sign Up request"
|
||||
// @Param request body request.SignUpDto true "Sign Up details"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
@@ -94,13 +95,14 @@ func (h *AuthController) Signup(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary Refresh access token
|
||||
// @Description Get a new access token using the user's current session/refresh token
|
||||
// @Summary Refresh session tokens
|
||||
// @Description Generate a new access token using a valid refresh token from context
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 401 {object} response.CommonResponse "Unauthorized or expired refresh token"
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
||||
@@ -120,3 +122,116 @@ func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyToken godoc
|
||||
// @Summary Verify a security token
|
||||
// @Description Validate an OTP or email verification token
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.VerifyTokenDto true "Token verification data"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/token/verify [post]
|
||||
func (h *AuthController) VerifyToken(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.VerifyTokenDto{}
|
||||
|
||||
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.VerifyToken(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,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateToken godoc
|
||||
// @Summary Generate a new verification token
|
||||
// @Description Request a new token for specific actions like email confirmation
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.CreateTokenDto true "Token creation request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/token/create [post]
|
||||
func (h *AuthController) CreateToken(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.CreateTokenDto{}
|
||||
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
err := h.service.CreateToken(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: nil,
|
||||
Message: "Token created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ForgotPassword godoc
|
||||
// @Summary Handle forgotten password
|
||||
// @Description Initiate password recovery process for a user
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.ForgotPasswordDto true "Forgot Password request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/forgot-password [post]
|
||||
func (h *AuthController) ForgotPassword(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.ForgotPasswordDto{}
|
||||
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
err := h.service.ForgotPassword(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: nil,
|
||||
Message: "Password reset successfully",
|
||||
})
|
||||
}
|
||||
276
internal/controllers/userController.go
Normal file
276
internal/controllers/userController.go
Normal file
@@ -0,0 +1,276 @@
|
||||
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 UserController struct {
|
||||
service services.UserService
|
||||
}
|
||||
|
||||
func NewUserController(svc services.UserService) *UserController {
|
||||
return &UserController{service: svc}
|
||||
}
|
||||
|
||||
// GetUserCurrent godoc
|
||||
// @Summary Get current user profile
|
||||
// @Description Retrieve the profile information of the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/current [get]
|
||||
func (h *UserController) GetUserCurrent(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := h.service.GetUserCurrent(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,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfile godoc
|
||||
// @Summary Update user profile
|
||||
// @Description Update the profile details of the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.UpdateProfileDto true "Update Profile request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id} [put]
|
||||
func (h *UserController) UpdateProfile(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.UpdateProfileDto{}
|
||||
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.UpdateProfile(ctx, c.Locals("uid").(string), 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,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword godoc
|
||||
// @Summary Change user password
|
||||
// @Description Update the password for the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.ChangePasswordDto true "Change Password request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/password [patch]
|
||||
func (h *UserController) ChangePassword(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.ChangePasswordDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
err := h.service.ChangePassword(ctx, c.Locals("uid").(string), 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,
|
||||
Message: "Password changed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreUser godoc
|
||||
// @Summary Restore a deleted user
|
||||
// @Description Restore a soft-deleted user account (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/restore [patch]
|
||||
func (h *UserController) RestoreUser(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
userId := c.Params("id")
|
||||
res, err := h.service.RestoreUser(ctx, userId)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser godoc
|
||||
// @Summary Delete a user
|
||||
// @Description Soft delete a user account (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id} [delete]
|
||||
func (h *UserController) DeleteUser(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
userId := c.Params("id")
|
||||
err := h.service.DeleteUser(ctx, userId)
|
||||
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,
|
||||
Message: "User deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeRoleUser godoc
|
||||
// @Summary Change user role
|
||||
// @Description Update the role of a user (Admin only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.ChangeRoleDto true "Change Role request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/role [patch]
|
||||
func (h *UserController) ChangeRoleUser(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.ChangeRoleDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
user, err := h.service.ChangeRoleUser(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: user,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserById godoc
|
||||
// @Summary Get user by ID
|
||||
// @Description Retrieve details of a specific user (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id} [get]
|
||||
func (h *UserController) GetUserById(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
userId := c.Params("id")
|
||||
res, err := h.service.GetUserByID(ctx, userId)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
// @Summary Search users
|
||||
// @Description Search and filter users with pagination (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param query query request.SearchUserDto false "Search Query"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users [get]
|
||||
func (h *UserController) Search(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.SearchUserDto{}
|
||||
if err := validator.ValidateQueryDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
res, err := h.service.Search(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(res)
|
||||
}
|
||||
@@ -1,11 +1,36 @@
|
||||
package request
|
||||
|
||||
import "history-api/pkg/constants"
|
||||
|
||||
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"`
|
||||
TokenID string `json:"token_id" validate:"required,uuid"`
|
||||
}
|
||||
type SignInDto struct {
|
||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||
Password string `json:"password" validate:"required,min=8,max=64"`
|
||||
}
|
||||
|
||||
type CreateTokenDto struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
TokenType constants.TokenType `json:"token_type" validate:"required,oneof=1 2 3 4"`
|
||||
}
|
||||
|
||||
type VerifyTokenDto struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
TokenType constants.TokenType `json:"token_type" validate:"required,oneof=1 2 3 4"`
|
||||
Token string `json:"token" validate:"required,len=6,numeric"`
|
||||
}
|
||||
|
||||
type ForgotPasswordDto struct {
|
||||
TokenID string `json:"token_id" validate:"required,uuid"`
|
||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8,max=64"`
|
||||
}
|
||||
|
||||
type SigninWith3rdDto struct {
|
||||
Provider string `json:"provider" validate:"required,oneof=google github facebook"`
|
||||
AccessToken string `json:"access_token" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
package request
|
||||
|
||||
import "history-api/pkg/constant"
|
||||
|
||||
type CreateUserDto struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
DiscordUserId string `json:"discord_user_id" validate:"required"`
|
||||
Role []constant.Role `json:"role" validate:"required"`
|
||||
type UpdateProfileDto struct {
|
||||
DisplayName string `json:"display_name" validate:"omitempty,min=2,max=50"`
|
||||
FullName string `json:"full_name" validate:"omitempty,min=2,max=100"`
|
||||
AvatarUrl string `json:"avatar_url" validate:"omitempty,url"`
|
||||
Bio string `json:"bio" validate:"omitempty,max=255"`
|
||||
Location string `json:"location" validate:"omitempty,max=100"`
|
||||
Website string `json:"website" validate:"omitempty,url"`
|
||||
CountryCode string `json:"country_code" validate:"omitempty,len=2"`
|
||||
Phone string `json:"phone" validate:"omitempty,min=8,max=20"`
|
||||
}
|
||||
|
||||
type UpdateUserDto struct {
|
||||
Password *string `json:"password" validate:"omitempty"`
|
||||
DiscordUserId *string `json:"discord_user_id" validate:"omitempty"`
|
||||
Role *[]constant.Role `json:"role" validate:"omitempty"`
|
||||
type ChangePasswordDto struct {
|
||||
OldPassword string `json:"old_password" validate:"required,min=8,max=64"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8,max=64,nefield=OldPassword"`
|
||||
}
|
||||
|
||||
type ChangeRoleDto struct {
|
||||
UserID string `json:"user_id" validate:"required,uuid"`
|
||||
Roles []string `json:"role_ids" validate:"required,min=1,dive,required,uuid"`
|
||||
}
|
||||
|
||||
type GetAllUserDto struct {
|
||||
CursorPaginationDto
|
||||
IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"`
|
||||
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"`
|
||||
}
|
||||
|
||||
type CursorPaginationDto struct {
|
||||
Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
|
||||
Limit int `json:"limit" query:"limit" validate:"required,min=1,max=100"`
|
||||
Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=created_at updated_at email display_name"`
|
||||
Order string `json:"order" query:"order" validate:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
type SearchUserDto struct {
|
||||
Username *string `query:"username" validate:"omitempty"`
|
||||
DiscordUserId *string `query:"discord_user_id" validate:"omitempty"`
|
||||
Role *[]constant.Role `query:"role" validate:"omitempty"`
|
||||
SortBy string `query:"sort_by" default:"created_at" validate:"oneof=created_at updated_at"`
|
||||
Order string `query:"order" default:"desc" validate:"oneof=asc desc"`
|
||||
Page int `query:"page" default:"1" validate:"min=1"`
|
||||
Limit int `query:"limit" default:"10" validate:"min=1,max=100"`
|
||||
CursorPaginationDto
|
||||
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
|
||||
IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"`
|
||||
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"`
|
||||
}
|
||||
|
||||
@@ -4,3 +4,7 @@ type AuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type VerifyTokenResponse struct {
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
@@ -14,6 +14,17 @@ type CommonResponse struct {
|
||||
|
||||
type JWTClaims struct {
|
||||
UId string `json:"uid"`
|
||||
Roles []constant.Role `json:"roles"`
|
||||
Roles []constants.Role `json:"roles"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type PaginatedResponse struct {
|
||||
Data any `json:"data"`
|
||||
Status bool `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Pagination struct {
|
||||
NextCursor string `json:"next_cursor"`
|
||||
HasMore bool `json:"has_more"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@@ -22,7 +22,6 @@ type User struct {
|
||||
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"`
|
||||
@@ -49,16 +48,6 @@ type UserRole struct {
|
||||
RoleID pgtype.UUID `json:"role_id"`
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
@@ -13,19 +13,17 @@ import (
|
||||
|
||||
const addUserRole = `-- name: AddUserRole :exec
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT $1, r.id
|
||||
FROM roles r
|
||||
WHERE r.name = $2
|
||||
SELECT $1, unnest($2::uuid[])
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
|
||||
type AddUserRoleParams struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Column2 []pgtype.UUID `json:"column_2"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddUserRole(ctx context.Context, arg AddUserRoleParams) error {
|
||||
_, err := q.db.Exec(ctx, addUserRole, arg.UserID, arg.Name)
|
||||
_, err := q.db.Exec(ctx, addUserRole, arg.UserID, arg.Column2)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,6 +127,38 @@ func (q *Queries) GetRoles(ctx context.Context) ([]Role, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getRolesByIDs = `-- name: GetRolesByIDs :many
|
||||
SELECT id, name, is_deleted, created_at, updated_at
|
||||
FROM roles
|
||||
WHERE id = ANY($1::uuid[]) AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) GetRolesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Role, error) {
|
||||
rows, err := q.db.Query(ctx, getRolesByIDs, dollar_1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Role{}
|
||||
for rows.Next() {
|
||||
var i Role
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeAllRolesFromUser = `-- name: RemoveAllRolesFromUser :exec
|
||||
DELETE FROM user_roles
|
||||
WHERE user_id = $1
|
||||
|
||||
@@ -77,7 +77,6 @@ SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
@@ -116,7 +115,6 @@ type GetUserByEmailRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
@@ -132,7 +130,6 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
@@ -148,7 +145,6 @@ SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
@@ -190,7 +186,6 @@ type GetUserByIDRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
@@ -207,7 +202,79 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByIDWithoutDeleted = `-- name: GetUserByIDWithoutDeleted :one
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
|
||||
-- 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
|
||||
WHERE u.id = $1
|
||||
`
|
||||
|
||||
type GetUserByIDWithoutDeletedRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (GetUserByIDWithoutDeletedRow, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByIDWithoutDeleted, id)
|
||||
var i GetUserByIDWithoutDeletedRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
@@ -224,7 +291,127 @@ 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,
|
||||
|
||||
-- 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
|
||||
WHERE
|
||||
($1::uuid IS NULL OR u.id > $1::uuid)
|
||||
AND ($2::boolean IS NULL OR u.is_deleted = $2::boolean)
|
||||
AND (
|
||||
$3::uuid[] IS NULL OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_roles ur2
|
||||
WHERE ur2.user_id = u.id AND ur2.role_id = ANY($3::uuid[])
|
||||
)
|
||||
)
|
||||
ORDER BY u.id ASC
|
||||
LIMIT $4
|
||||
`
|
||||
|
||||
type GetUsersParams struct {
|
||||
Cursor pgtype.UUID `json:"cursor"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
RoleIds []pgtype.UUID `json:"role_ids"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetUsersRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, getUsers,
|
||||
arg.Cursor,
|
||||
arg.IsDeleted,
|
||||
arg.RoleIds,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetUsersRow{}
|
||||
for rows.Next() {
|
||||
var i GetUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const restoreUser = `-- name: RestoreUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = false
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RestoreUser(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, restoreUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const searchUsers = `-- name: SearchUsers :many
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
@@ -257,14 +444,44 @@ SELECT
|
||||
) AS roles
|
||||
|
||||
FROM users u
|
||||
WHERE u.is_deleted = false
|
||||
WHERE
|
||||
($1::uuid IS NULL OR u.id > $1::uuid)
|
||||
|
||||
AND ($2::boolean IS NULL OR u.is_deleted = $2::boolean)
|
||||
AND (
|
||||
$3::uuid[] IS NULL OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_roles ur2
|
||||
WHERE ur2.user_id = u.id AND ur2.role_id = ANY($3::uuid[])
|
||||
)
|
||||
)
|
||||
|
||||
AND ($4::uuid IS NULL OR u.id = $4::uuid)
|
||||
AND (
|
||||
$5::text IS NULL OR
|
||||
u.email ILIKE '%' || $5::text || '%' OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_profiles p
|
||||
WHERE p.user_id = u.id AND p.display_name ILIKE '%' || $5::text || '%'
|
||||
)
|
||||
)
|
||||
ORDER BY u.id ASC
|
||||
LIMIT $6
|
||||
`
|
||||
|
||||
type GetUsersRow struct {
|
||||
type SearchUsersParams struct {
|
||||
Cursor pgtype.UUID `json:"cursor"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
RoleIds []pgtype.UUID `json:"role_ids"`
|
||||
SearchID pgtype.UUID `json:"search_id"`
|
||||
SearchText pgtype.Text `json:"search_text"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type SearchUsersRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
@@ -274,20 +491,26 @@ type GetUsersRow struct {
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, getUsers)
|
||||
func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]SearchUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, searchUsers,
|
||||
arg.Cursor,
|
||||
arg.IsDeleted,
|
||||
arg.RoleIds,
|
||||
arg.SearchID,
|
||||
arg.SearchText,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetUsersRow{}
|
||||
items := []SearchUsersRow{}
|
||||
for rows.Next() {
|
||||
var i GetUsersRow
|
||||
var i SearchUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
@@ -306,18 +529,6 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const restoreUser = `-- name: RestoreUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = false
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RestoreUser(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, restoreUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTokenVersion = `-- name: UpdateTokenVersion :exec
|
||||
UPDATE users
|
||||
SET token_version = $2
|
||||
@@ -432,18 +643,15 @@ INSERT INTO users (
|
||||
email,
|
||||
password_hash,
|
||||
google_id,
|
||||
auth_provider,
|
||||
is_verified
|
||||
auth_provider
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
$1, $2, $3, $4
|
||||
)
|
||||
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
|
||||
auth_provider = EXCLUDED.auth_provider
|
||||
RETURNING id, email, password_hash, google_id, auth_provider, is_deleted, token_version, refresh_token, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpsertUserParams struct {
|
||||
@@ -451,7 +659,6 @@ type UpsertUserParams struct {
|
||||
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) {
|
||||
@@ -460,7 +667,6 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
||||
arg.PasswordHash,
|
||||
arg.GoogleID,
|
||||
arg.AuthProvider,
|
||||
arg.IsVerified,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
@@ -469,7 +675,6 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
||||
&i.PasswordHash,
|
||||
&i.GoogleID,
|
||||
&i.AuthProvider,
|
||||
&i.IsVerified,
|
||||
&i.IsDeleted,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
@@ -478,16 +683,3 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const verifyUser = `-- name: VerifyUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_verified = true
|
||||
WHERE id = $1
|
||||
AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) VerifyUser(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, verifyUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"slices"
|
||||
|
||||
jwtware "github.com/gofiber/contrib/v3/jwt"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/extractors"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func JwtAccess() fiber.Handler {
|
||||
func JwtAccess(userRepo repositories.UserRepository) fiber.Handler {
|
||||
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -20,13 +22,13 @@ func JwtAccess() fiber.Handler {
|
||||
return jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte(jwtSecret)},
|
||||
ErrorHandler: jwtError,
|
||||
SuccessHandler: jwtSuccess,
|
||||
SuccessHandler: jwtSuccess(userRepo),
|
||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
||||
Claims: &response.JWTClaims{},
|
||||
})
|
||||
}
|
||||
|
||||
func JwtRefresh() fiber.Handler {
|
||||
func JwtRefresh(userRepo repositories.UserRepository) fiber.Handler {
|
||||
jwtRefreshSecret, err := config.GetConfig("JWT_REFRESH_SECRET")
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -35,14 +37,16 @@ func JwtRefresh() fiber.Handler {
|
||||
return jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte(jwtRefreshSecret)},
|
||||
ErrorHandler: jwtError,
|
||||
SuccessHandler: jwtSuccess,
|
||||
SuccessHandler: jwtSuccess(userRepo),
|
||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
||||
Claims: &response.JWTClaims{},
|
||||
})
|
||||
}
|
||||
|
||||
func jwtSuccess(c fiber.Ctx) error {
|
||||
func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
user := jwtware.FromContext(c)
|
||||
|
||||
unauthorized := func() error {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
@@ -59,17 +63,35 @@ func jwtSuccess(c fiber.Ctx) error {
|
||||
return unauthorized()
|
||||
}
|
||||
|
||||
if slices.Contains(claims.Roles, constant.BANNED) {
|
||||
if slices.Contains(claims.Roles, constants.BANNED) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "User account is banned",
|
||||
})
|
||||
}
|
||||
|
||||
var pgID pgtype.UUID
|
||||
err := pgID.Scan(claims.UId)
|
||||
if err != nil {
|
||||
return unauthorized()
|
||||
}
|
||||
tokenVersion, err := userRepo.GetTokenVersion(c.Context(), pgID)
|
||||
if err != nil {
|
||||
return unauthorized()
|
||||
}
|
||||
|
||||
if tokenVersion != claims.TokenVersion {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "Token has been invalidated",
|
||||
})
|
||||
}
|
||||
|
||||
c.Locals("uid", claims.UId)
|
||||
c.Locals("user_claims", claims)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
func jwtError(c fiber.Ctx, err error) error {
|
||||
if err.Error() == "Missing or malformed JWT" {
|
||||
|
||||
@@ -2,13 +2,13 @@ package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"slices"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func getRoles(c fiber.Ctx) ([]constant.Role, error) {
|
||||
func getRoles(c fiber.Ctx) ([]constants.Role, error) {
|
||||
claimsVal := c.Locals("user_claims")
|
||||
if claimsVal == nil {
|
||||
return nil, fiber.ErrUnauthorized
|
||||
@@ -22,7 +22,7 @@ func getRoles(c fiber.Ctx) ([]constant.Role, error) {
|
||||
return claims.Roles, nil
|
||||
}
|
||||
|
||||
func RequireAnyRole(required ...constant.Role) fiber.Handler {
|
||||
func RequireAnyRole(required ...constants.Role) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
userRoles, err := getRoles(c)
|
||||
if err != nil {
|
||||
@@ -43,7 +43,7 @@ func RequireAnyRole(required ...constant.Role) fiber.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAllRoles(required ...constant.Role) fiber.Handler {
|
||||
func RequireAllRoles(required ...constants.Role) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
userRoles, err := getRoles(c)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -44,6 +44,13 @@ func (r *RoleEntity) ToResponse() *response.RoleResponse {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoleEntity) ToRoleSimple() *RoleSimple {
|
||||
return &RoleSimple{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||
out := make([]*response.RoleResponse, len(rs))
|
||||
for i := range rs {
|
||||
@@ -52,10 +59,10 @@ func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||
return out
|
||||
}
|
||||
|
||||
func RolesEntityToRoleConstant(rs []*RoleSimple) []constant.Role {
|
||||
out := make([]constant.Role, len(rs))
|
||||
func RolesEntityToRoleConstant(rs []*RoleSimple) []constants.Role {
|
||||
out := make([]constants.Role, len(rs))
|
||||
for i := range rs {
|
||||
data, ok := constant.ParseRole(rs[i].Name)
|
||||
data, ok := constants.ParseRole(rs[i].Name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,35 +1,9 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
import "history-api/pkg/constants"
|
||||
|
||||
type TokenEntity struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
TokenType int16 `json:"token_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (t *TokenEntity) ToResponse() *response.TokenResponse {
|
||||
return &response.TokenResponse{
|
||||
ID: convert.UUIDToString(t.ID),
|
||||
UserID: convert.UUIDToString(t.UserID),
|
||||
TokenType: t.TokenType,
|
||||
ExpiresAt: convert.TimeToPtr(t.ExpiresAt),
|
||||
CreatedAt: convert.TimeToPtr(t.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func TokensEntityToResponse(ts []*TokenEntity) []*response.TokenResponse {
|
||||
out := make([]*response.TokenResponse, len(ts))
|
||||
for i := range ts {
|
||||
out[i] = ts[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
TokenType constants.TokenType `json:"token_type"`
|
||||
}
|
||||
|
||||
@@ -2,19 +2,22 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type RoleRepository interface {
|
||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error)
|
||||
GetByname(ctx context.Context, name string) (*models.RoleEntity, error)
|
||||
All(ctx context.Context) ([]*models.RoleEntity, error)
|
||||
Create(ctx context.Context, name string) (*models.RoleEntity, error)
|
||||
@@ -39,6 +42,56 @@ func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *roleRepository) generateQueryKey(prefix string, params any) string {
|
||||
b, _ := json.Marshal(params)
|
||||
hash := fmt.Sprintf("%x", md5.Sum(b))
|
||||
return fmt.Sprintf("%s:%s", prefix, hash)
|
||||
}
|
||||
|
||||
func (r *roleRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*models.RoleEntity{}, nil
|
||||
}
|
||||
keys := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
keys[i] = fmt.Sprintf("role:id:%s", id)
|
||||
}
|
||||
raws := r.c.MGet(ctx, keys...)
|
||||
|
||||
var roles []*models.RoleEntity
|
||||
missingRolesToCache := make(map[string]any)
|
||||
|
||||
for i, b := range raws {
|
||||
if len(b) > 0 {
|
||||
var u models.RoleEntity
|
||||
if err := json.Unmarshal(b, &u); err == nil {
|
||||
roles = append(roles, &u)
|
||||
}
|
||||
} else {
|
||||
pgId := pgtype.UUID{}
|
||||
err := pgId.Scan(ids[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dbRole, err := r.GetByID(ctx, pgId)
|
||||
if err == nil && dbRole != nil {
|
||||
roles = append(roles, dbRole)
|
||||
missingRolesToCache[keys[i]] = dbRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingRolesToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, missingRolesToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
|
||||
return r.getByIDsWithFallback(ctx, ids)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -59,7 +112,7 @@ func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.R
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
@@ -83,7 +136,7 @@ func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.Ro
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
@@ -104,7 +157,7 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
|
||||
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)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
@@ -125,7 +178,7 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
|
||||
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)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,7 @@ func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, er
|
||||
metadata[name] = value
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, metadata, 10*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, metadata, constants.NormalCacheDuration)
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
79
internal/repositories/tokenRepository.go
Normal file
79
internal/repositories/tokenRepository.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
)
|
||||
|
||||
type TokenRepository interface {
|
||||
CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error)
|
||||
Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error)
|
||||
Create(ctx context.Context, token *models.TokenEntity) error
|
||||
Delete(ctx context.Context, email string, tokenType constants.TokenType) error
|
||||
CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error)
|
||||
CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||
DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||
}
|
||||
|
||||
type tokenRepository struct {
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewTokenRepository(c cache.Cache) TokenRepository {
|
||||
return &tokenRepository{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tokenRepository) CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
return t.c.Set(ctx, cacheKey, true, constants.TokenVerifiedDuration)
|
||||
}
|
||||
|
||||
func (t *tokenRepository) DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
return t.c.Del(ctx, cacheKey)
|
||||
}
|
||||
|
||||
|
||||
func (t *tokenRepository) CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
||||
exists, err := t.c.Exists(ctx, cacheKey)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (t *tokenRepository) CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
exists, err := t.c.Exists(ctx, cacheKey)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (t *tokenRepository) Create(ctx context.Context, token *models.TokenEntity) error {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s", token.TokenType.Value(), token.Email)
|
||||
err := t.c.Set(ctx, cacheKey, token, constants.TokenExpirationDuration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cooldownKey := fmt.Sprintf("token:cooldown:%d:%s", token.TokenType.Value(), token.Email)
|
||||
return t.c.Set(ctx, cooldownKey, true, constants.TokenCooldownDuration)
|
||||
}
|
||||
|
||||
func (t *tokenRepository) Delete(ctx context.Context, email string, tokenType constants.TokenType) error {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s", tokenType.Value(), email)
|
||||
cooldownKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
||||
_ = t.c.Del(ctx, cooldownKey)
|
||||
return t.c.Del(ctx, cacheKey)
|
||||
}
|
||||
|
||||
func (t *tokenRepository) Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error) {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s", tokenType.Value(), email)
|
||||
var token models.TokenEntity
|
||||
err := t.c.Get(ctx, cacheKey, &token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
@@ -2,21 +2,25 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
||||
GetByIDWithoutDeleted(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)
|
||||
All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error)
|
||||
Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, 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)
|
||||
@@ -24,7 +28,6 @@ type UserRepository interface {
|
||||
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
|
||||
}
|
||||
@@ -41,6 +44,52 @@ func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *userRepository) generateQueryKey(prefix string, params any) string {
|
||||
b, _ := json.Marshal(params)
|
||||
hash := fmt.Sprintf("%x", md5.Sum(b))
|
||||
return fmt.Sprintf("%s:%s", prefix, hash)
|
||||
}
|
||||
|
||||
func (r *userRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.UserEntity, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*models.UserEntity{}, nil
|
||||
}
|
||||
keys := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
keys[i] = fmt.Sprintf("user:id:%s", id)
|
||||
}
|
||||
raws := r.c.MGet(ctx, keys...)
|
||||
|
||||
var users []*models.UserEntity
|
||||
missingUsersToCache := make(map[string]any)
|
||||
|
||||
for i, b := range raws {
|
||||
if len(b) > 0 {
|
||||
var u models.UserEntity
|
||||
if err := json.Unmarshal(b, &u); err == nil {
|
||||
users = append(users, &u)
|
||||
}
|
||||
} else {
|
||||
pgId := pgtype.UUID{}
|
||||
err := pgId.Scan(ids[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dbUser, err := r.GetByID(ctx, pgId)
|
||||
if err == nil && dbUser != nil {
|
||||
users = append(users, dbUser)
|
||||
missingUsersToCache[keys[i]] = dbUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingUsersToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, missingUsersToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
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
|
||||
@@ -58,7 +107,6 @@ func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.U
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -73,7 +121,43 @@ func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.U
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
|
||||
cacheId := fmt.Sprintf("user:deleted: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: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
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
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -96,7 +180,6 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -111,7 +194,7 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -121,12 +204,17 @@ func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserP
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
bgCtx := context.Background()
|
||||
|
||||
_ = r.c.DelByPattern(bgCtx, "user:all*")
|
||||
_ = r.c.DelByPattern(bgCtx, "user:search*")
|
||||
}()
|
||||
|
||||
return &models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -161,7 +249,7 @@ func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUs
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
}
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -183,19 +271,27 @@ func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUs
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
|
||||
rows, err := r.q.GetUsers(ctx)
|
||||
func (r *userRepository) All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error) {
|
||||
queryKey := r.generateQueryKey("user:all", params)
|
||||
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
rows, err := r.q.GetUsers(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []*models.UserEntity
|
||||
var ids []string
|
||||
usersToCache := make(map[string]any)
|
||||
|
||||
for _, row := range rows {
|
||||
user := &models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -205,44 +301,75 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
|
||||
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)
|
||||
ids = append(ids, user.ID)
|
||||
|
||||
usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user
|
||||
}
|
||||
|
||||
if len(usersToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, usersToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
func (r *userRepository) Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error) {
|
||||
queryKey := r.generateQueryKey("user:search", params)
|
||||
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
|
||||
err = r.q.VerifyUser(ctx, id)
|
||||
rows, err := r.q.SearchUsers(ctx, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.q.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
||||
ID: id,
|
||||
TokenVersion: user.TokenVersion + 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.IsVerified = true
|
||||
user.TokenVersion += 1
|
||||
var users []*models.UserEntity
|
||||
var ids []string
|
||||
usersToCache := make(map[string]any)
|
||||
|
||||
mapCache := map[string]any{
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
for _, row := range rows {
|
||||
user := &models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
return nil
|
||||
|
||||
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)
|
||||
ids = append(ids, user.ID)
|
||||
usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user
|
||||
}
|
||||
|
||||
if len(usersToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, usersToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
||||
@@ -288,7 +415,7 @@ func (r *userRepository) GetTokenVersion(ctx context.Context, id pgtype.UUID) (i
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, raw, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, raw, constants.NormalCacheDuration)
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
@@ -299,7 +426,7 @@ func (r *userRepository) UpdateTokenVersion(ctx context.Context, params sqlc.Upd
|
||||
}
|
||||
|
||||
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID))
|
||||
_ = r.c.Set(ctx, cacheId, params.TokenVersion, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, params.TokenVersion, constants.NormalCacheDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -328,7 +455,7 @@ func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateU
|
||||
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||
}
|
||||
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -349,6 +476,6 @@ func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.Upd
|
||||
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||
}
|
||||
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,13 +3,17 @@ package routes
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
"history-api/internal/repositories"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func AuthRoutes(app *fiber.App, controller *controllers.AuthController) {
|
||||
func AuthRoutes(app *fiber.App, controller *controllers.AuthController, userRepo repositories.UserRepository) {
|
||||
route := app.Group("/auth")
|
||||
route.Post("/signin", controller.Signin)
|
||||
route.Post("/signup", controller.Signup)
|
||||
route.Post("/refresh", middlewares.JwtRefresh(), controller.RefreshToken)
|
||||
route.Post("/refresh", middlewares.JwtRefresh(userRepo), controller.RefreshToken)
|
||||
route.Post("/token/create", controller.CreateToken)
|
||||
route.Post("/token/verify", controller.VerifyToken)
|
||||
route.Post("/forgot-password", controller.ForgotPassword)
|
||||
}
|
||||
|
||||
65
internal/routes/userRoute.go
Normal file
65
internal/routes/userRoute.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/constants"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo repositories.UserRepository) {
|
||||
route := app.Group("/users")
|
||||
|
||||
route.Get(
|
||||
"/",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.Search,
|
||||
)
|
||||
route.Get(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.Search,
|
||||
)
|
||||
|
||||
route.Delete(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.DeleteUser,
|
||||
)
|
||||
route.Patch(
|
||||
"/:id/restore",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.RestoreUser,
|
||||
)
|
||||
|
||||
route.Patch(
|
||||
"/:id/role",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN),
|
||||
controller.ChangeRoleUser,
|
||||
)
|
||||
|
||||
route.Patch(
|
||||
"/:id/password",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.ChangePassword,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/current",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.GetUserCurrent,
|
||||
)
|
||||
|
||||
route.Put(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.UpdateProfile,
|
||||
)
|
||||
}
|
||||
@@ -2,19 +2,25 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"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/cache"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
"math/big"
|
||||
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -22,29 +28,35 @@ import (
|
||||
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
|
||||
ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error
|
||||
VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error)
|
||||
CreateToken(ctx context.Context, dto *request.CreateTokenDto) error
|
||||
SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error
|
||||
RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error)
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
userRepo repositories.UserRepository
|
||||
roleRepo repositories.RoleRepository
|
||||
tokenRepo repositories.TokenRepository
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewAuthService(
|
||||
userRepo repositories.UserRepository,
|
||||
roleRepo repositories.RoleRepository,
|
||||
tokenRepo repositories.TokenRepository,
|
||||
c cache.Cache,
|
||||
) AuthService {
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
roleRepo: roleRepo,
|
||||
tokenRepo: tokenRepo,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authService) genToken(Uid string, role []constant.Role) (*response.AuthResponse, error) {
|
||||
func (a *authService) genToken(user *models.UserEntity) (*response.AuthResponse, error) {
|
||||
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT_SECRET in environment")
|
||||
@@ -59,18 +71,20 @@ func (a *authService) genToken(Uid string, role []constant.Role) (*response.Auth
|
||||
}
|
||||
|
||||
claimsAccess := &response.JWTClaims{
|
||||
UId: Uid,
|
||||
Roles: role,
|
||||
UId: user.ID,
|
||||
Roles: models.RolesEntityToRoleConstant(user.Roles),
|
||||
TokenVersion: user.TokenVersion,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(constants.AccessTokenDuration)),
|
||||
},
|
||||
}
|
||||
|
||||
claimsRefresh := &response.JWTClaims{
|
||||
UId: Uid,
|
||||
Roles: role,
|
||||
UId: user.ID,
|
||||
Roles: models.RolesEntityToRoleConstant(user.Roles),
|
||||
TokenVersion: user.TokenVersion,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * 24 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(constants.RefreshTokenDuration)),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -102,11 +116,11 @@ func (a *authService) saveNewRefreshToken(ctx context.Context, params sqlc.Updat
|
||||
}
|
||||
|
||||
func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
if !constants.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||
}
|
||||
|
||||
err := constant.ValidatePassword(dto.Password)
|
||||
err := constants.ValidatePassword(dto.Password)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
@@ -120,13 +134,12 @@ func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*resp
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
|
||||
}
|
||||
|
||||
data, err := a.genToken(user.ID, models.RolesEntityToRoleConstant(user.Roles))
|
||||
data, err := a.genToken(user)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
|
||||
}
|
||||
var pgID pgtype.UUID
|
||||
err = pgID.Scan(user.ID)
|
||||
pgID, err := convert.StringToUUID(user.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -160,11 +173,11 @@ func (a *authService) RefreshToken(ctx context.Context, id string) (*response.Au
|
||||
}
|
||||
roles := models.RolesEntityToRoleConstant(user.Roles)
|
||||
|
||||
if slices.Contains(roles, constant.BANNED) {
|
||||
if slices.Contains(roles, constants.BANNED) {
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "User is banned!")
|
||||
}
|
||||
|
||||
data, err := a.genToken(id, roles)
|
||||
data, err := a.genToken(user)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -187,14 +200,23 @@ func (a *authService) RefreshToken(ctx context.Context, id string) (*response.Au
|
||||
}
|
||||
|
||||
func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error) {
|
||||
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
if !constants.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||
}
|
||||
err := constant.ValidatePassword(dto.Password)
|
||||
err := constants.ValidatePassword(dto.Password)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenEmailVerify, dto.TokenID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
|
||||
}
|
||||
|
||||
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
@@ -202,6 +224,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
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())
|
||||
@@ -215,14 +238,13 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
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)
|
||||
|
||||
userId, err := convert.StringToUUID(user.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -239,19 +261,28 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
role, err := a.roleRepo.GetByname(ctx, constants.USER.String())
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
roleId, err := convert.StringToUUID(role.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = a.roleRepo.AddUserRole(
|
||||
ctx,
|
||||
sqlc.AddUserRoleParams{
|
||||
UserID: userId,
|
||||
Name: constant.USER.String(),
|
||||
Column2: []pgtype.UUID{roleId},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
data, err := a.genToken(user.ID, constant.USER.ToSlice())
|
||||
data, err := a.genToken(user)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -273,22 +304,101 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ForgotPassword implements [AuthService].
|
||||
func (a *authService) ForgotPassword(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
func (a *authService) ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error {
|
||||
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenPasswordReset, dto.TokenID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
|
||||
}
|
||||
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "User not found")
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
userId, err := convert.StringToUUID(user.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
err = a.userRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
|
||||
ID: userId,
|
||||
PasswordHash: pgtype.Text{
|
||||
String: string(hashed),
|
||||
Valid: len(hashed) != 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SigninWith3rd implements [AuthService].
|
||||
func (a *authService) SigninWith3rd(ctx context.Context) error {
|
||||
func (a *authService) SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
func (a *authService) GenerateOTP() (string, error) {
|
||||
max := big.NewInt(900000)
|
||||
n, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
otp := n.Int64() + 100000
|
||||
return fmt.Sprintf("%06d", otp), nil
|
||||
}
|
||||
|
||||
// CreateToken implements [AuthService].
|
||||
func (a *authService) CreateToken(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
func (a *authService) CreateToken(ctx context.Context, dto *request.CreateTokenDto) error {
|
||||
ok, err := a.tokenRepo.CheckCooldown(ctx, dto.Email, dto.TokenType)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if ok {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Please wait before requesting another token")
|
||||
}
|
||||
|
||||
otp, err := a.GenerateOTP()
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
token := &models.TokenEntity{
|
||||
Email: dto.Email,
|
||||
Token: otp,
|
||||
TokenType: dto.TokenType,
|
||||
}
|
||||
|
||||
err = a.tokenRepo.Create(ctx, token)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
a.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeSendEmailOTP, token)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify implements [AuthService].
|
||||
func (a *authService) VerifyToken(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
func (a *authService) VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error) {
|
||||
token, err := a.tokenRepo.Get(ctx, dto.Email, dto.TokenType)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if token == nil || token.Token != dto.Token {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid token")
|
||||
}
|
||||
tokenId := uuid.New().String()
|
||||
err = a.tokenRepo.CreateVerified(ctx, dto.Email, dto.TokenType, tokenId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return &response.VerifyTokenResponse{
|
||||
TokenID: tokenId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -4,22 +4,28 @@ 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/convert"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
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)
|
||||
GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error)
|
||||
ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) 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)
|
||||
DeleteUser(ctx context.Context, userId string) error
|
||||
ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error)
|
||||
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
@@ -37,47 +43,241 @@ func NewUserService(
|
||||
}
|
||||
}
|
||||
|
||||
// ChangePassword implements [UserService].
|
||||
func (u *userService) ChangePassword(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
|
||||
}
|
||||
|
||||
hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = u.userRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
|
||||
ID: pgID,
|
||||
PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeRoleUser implements [UserService].
|
||||
func (u *userService) ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error) {
|
||||
userId, err := convert.StringToUUID(dto.UserID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
user, err := u.userRepo.GetByID(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
roleIdstr, err := u.roleRepo.GetByIDs(ctx, dto.Roles)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user.Roles = make([]*models.RoleSimple, 0)
|
||||
roleIdList := make([]pgtype.UUID, 0)
|
||||
for _, role := range roleIdstr {
|
||||
roleID, err := convert.StringToUUID(role.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
roleIdList = append(roleIdList, roleID)
|
||||
user.Roles = append(user.Roles, role.ToRoleSimple())
|
||||
}
|
||||
|
||||
err = u.roleRepo.RemoveAllRolesFromUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = u.roleRepo.AddUserRole(ctx, sqlc.AddUserRoleParams{
|
||||
UserID: userId,
|
||||
Column2: roleIdList,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return user.ToResponse(), nil
|
||||
|
||||
}
|
||||
|
||||
// DeleteUser implements [UserService].
|
||||
func (u *userService) DeleteUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) DeleteUser(ctx context.Context, userId string) error {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
err = u.userRepo.Delete(ctx, pgID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllUser implements [UserService].
|
||||
func (u *userService) GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
newUser, err := u.userRepo.UpdateProfile(
|
||||
ctx,
|
||||
sqlc.UpdateUserProfileParams{
|
||||
DisplayName: pgtype.Text{String: dto.DisplayName, Valid: len(dto.DisplayName) > 0},
|
||||
FullName: pgtype.Text{String: dto.FullName, Valid: len(dto.FullName) > 0},
|
||||
AvatarUrl: pgtype.Text{String: dto.AvatarUrl, Valid: len(dto.AvatarUrl) > 0},
|
||||
Bio: pgtype.Text{String: dto.Bio, Valid: len(dto.Bio) > 0},
|
||||
Location: pgtype.Text{String: dto.Location, Valid: len(dto.Location) > 0},
|
||||
Website: pgtype.Text{String: dto.Website, Valid: len(dto.Website) > 0},
|
||||
CountryCode: pgtype.Text{String: dto.CountryCode, Valid: len(dto.CountryCode) > 0},
|
||||
Phone: pgtype.Text{String: dto.Phone, Valid: len(dto.Phone) > 0},
|
||||
UserID: pgID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return newUser.ToResponse(), nil
|
||||
}
|
||||
|
||||
// GetUserByID implements [UserService].
|
||||
func (u *userService) GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
// GetUserCurrent implements [UserService].
|
||||
func (u *userService) GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
user, err := u.userRepo.GetByIDWithoutDeleted(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
err = u.userRepo.Restore(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user.IsDeleted = false
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
// RestoreUser implements [UserService].
|
||||
func (u *userService) RestoreUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||
arg := sqlc.SearchUsersParams{
|
||||
Limit: int32(dto.Limit + 1),
|
||||
}
|
||||
|
||||
if dto.Cursor != "" {
|
||||
pgID, err := convert.StringToUUID(dto.Cursor)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
|
||||
}
|
||||
arg.Cursor = pgID
|
||||
}
|
||||
|
||||
if dto.Search != "" {
|
||||
pgID, err := convert.StringToUUID(dto.Search)
|
||||
if err == nil {
|
||||
arg.SearchID = pgID
|
||||
} else {
|
||||
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
if dto.IsDeleted != nil {
|
||||
arg.IsDeleted = pgtype.Bool{Bool: *dto.IsDeleted, Valid: true}
|
||||
}
|
||||
if len(dto.RoleIDs) > 0 {
|
||||
var pgRoleIDs []pgtype.UUID
|
||||
for _, idStr := range dto.RoleIDs {
|
||||
pgID, err := convert.StringToUUID(idStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pgRoleIDs = append(pgRoleIDs, pgID)
|
||||
}
|
||||
arg.RoleIds = pgRoleIDs
|
||||
}
|
||||
|
||||
rows, err := u.userRepo.Search(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasMore := false
|
||||
var nextCursor string
|
||||
|
||||
if len(rows) > dto.Limit {
|
||||
hasMore = true
|
||||
nextCursor = rows[dto.Limit-1].ID
|
||||
rows = rows[:dto.Limit]
|
||||
}
|
||||
|
||||
res := &response.PaginatedResponse{
|
||||
Data: rows,
|
||||
Status: true,
|
||||
Message: "",
|
||||
}
|
||||
res.Pagination.HasMore = hasMore
|
||||
res.Pagination.NextCursor = nextCursor
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
37
pkg/cache/redis.go
vendored
37
pkg/cache/redis.go
vendored
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constants"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -17,6 +18,9 @@ type Cache interface {
|
||||
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
|
||||
Exists(ctx context.Context, key string) (bool, error)
|
||||
GetRawClient() *redis.Client
|
||||
PublishTask(ctx context.Context, streamName string, taskType constants.TaskType, payload any) error
|
||||
}
|
||||
|
||||
type RedisClient struct {
|
||||
@@ -49,6 +53,18 @@ func NewRedisClient() (Cache, error) {
|
||||
return &RedisClient{client: rdb}, nil
|
||||
}
|
||||
|
||||
func (r *RedisClient) GetRawClient() *redis.Client {
|
||||
return r.client
|
||||
}
|
||||
|
||||
func (r *RedisClient) Exists(ctx context.Context, key string) (bool, error) {
|
||||
count, err := r.client.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *RedisClient) Del(ctx context.Context, keys ...string) error {
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
@@ -59,14 +75,14 @@ func (r *RedisClient) Del(ctx context.Context, keys ...string) error {
|
||||
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()
|
||||
keys, nextCursor, err := r.client.Scan(ctx, cursor, pattern, 1000).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)
|
||||
if err := r.client.Unlink(ctx, keys...).Err(); err != nil {
|
||||
return fmt.Errorf("error unlinking keys during scan: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +137,21 @@ func (r *RedisClient) MGet(ctx context.Context, keys ...string) [][]byte {
|
||||
return results
|
||||
}
|
||||
|
||||
func (r *RedisClient) PublishTask(ctx context.Context, streamName string, taskType constants.TaskType, payload any) error {
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.client.XAdd(ctx, &redis.XAddArgs{
|
||||
Stream: streamName,
|
||||
Values: map[string]interface{}{
|
||||
"task_type": taskType.String(),
|
||||
"payload": string(payloadBytes),
|
||||
},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func GetMultiple[T any](ctx context.Context, c Cache, keys []string) ([]T, error) {
|
||||
raws := c.MGet(ctx, keys...)
|
||||
final := make([]T, 0)
|
||||
|
||||
@@ -34,3 +34,11 @@ func GetConfig(config string) (string, error) {
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GetConfigWithDefault(config, defaultValue string) string {
|
||||
var data string = os.Getenv(config)
|
||||
if data == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package constants
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package constants
|
||||
|
||||
type Role string
|
||||
|
||||
@@ -18,10 +18,15 @@ func (r Role) Compare(other Role) bool {
|
||||
return r == other
|
||||
}
|
||||
|
||||
func (r Role) IsValid() bool {
|
||||
return CheckValidRole(r)
|
||||
}
|
||||
|
||||
func CheckValidRole(r Role) bool {
|
||||
return r == ADMIN || r == MOD || r == HISTORIAN || r == USER || r == BANNED
|
||||
}
|
||||
|
||||
|
||||
func ParseRole(s string) (Role, bool) {
|
||||
r := Role(s)
|
||||
if CheckValidRole(r) {
|
||||
6
pkg/constants/sream.go
Normal file
6
pkg/constants/sream.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
StreamEmailName = "stream:email_tasks"
|
||||
GroupEmailName = "email_workers_group"
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package constants
|
||||
|
||||
type StatusType int16
|
||||
|
||||
11
pkg/constants/task.go
Normal file
11
pkg/constants/task.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package constants
|
||||
|
||||
type TaskType string
|
||||
|
||||
const (
|
||||
TaskTypeSendEmailOTP TaskType = "SEND_EMAIL_OTP"
|
||||
)
|
||||
|
||||
func (t TaskType) String() string {
|
||||
return string(t)
|
||||
}
|
||||
13
pkg/constants/time.go
Normal file
13
pkg/constants/time.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
TokenCooldownDuration = 1 * time.Minute
|
||||
TokenExpirationDuration = 20 * time.Minute
|
||||
NormalCacheDuration = 15 * time.Minute
|
||||
ListCacheDuration = 10 * time.Minute
|
||||
AccessTokenDuration = 15 * time.Minute
|
||||
RefreshTokenDuration = 7 * 24 * time.Hour
|
||||
TokenVerifiedDuration = 10 * time.Minute
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package constants
|
||||
|
||||
type TokenType int16
|
||||
|
||||
@@ -24,6 +24,10 @@ func (t TokenType) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (t TokenType) Value() int16 {
|
||||
return int16(t)
|
||||
}
|
||||
|
||||
func ParseTokenType(v int16) TokenType {
|
||||
switch v {
|
||||
case 1:
|
||||
@@ -38,3 +42,18 @@ func ParseTokenType(v int16) TokenType {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTokenTypeFromString(s string) TokenType {
|
||||
switch s {
|
||||
case "PASSWORD_RESET":
|
||||
return TokenPasswordReset
|
||||
case "EMAIL_VERIFY":
|
||||
return TokenEmailVerify
|
||||
case "LOGIN_MAGIC_LINK":
|
||||
return TokenMagicLink
|
||||
case "REFRESH_TOKEN":
|
||||
return TokenRefreshToken
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package constants
|
||||
|
||||
type VerifyType int16
|
||||
|
||||
@@ -13,6 +13,15 @@ func UUIDToString(v pgtype.UUID) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func StringToUUID(s string) (pgtype.UUID, error) {
|
||||
var pgId pgtype.UUID
|
||||
err := pgId.Scan(s)
|
||||
if err != nil {
|
||||
return pgtype.UUID{}, err
|
||||
}
|
||||
return pgId, nil
|
||||
}
|
||||
|
||||
func TextToString(v pgtype.Text) string {
|
||||
if v.Valid {
|
||||
return v.String
|
||||
|
||||
70
pkg/email/email.go
Normal file
70
pkg/email/email.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"history-api/assets"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constants"
|
||||
"strings"
|
||||
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
func SendMailOTP(toEmail, otpCode string, tokenType constants.TokenType) error {
|
||||
userSmtp, err := config.GetConfig("SMTP_USER")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passSmtp, err := config.GetConfig("SMTP_PASS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var subject string
|
||||
var templatePath string
|
||||
|
||||
switch tokenType {
|
||||
case constants.TokenPasswordReset:
|
||||
subject = "Your Password Reset Code"
|
||||
templatePath = "resources/password_reset.html"
|
||||
case constants.TokenEmailVerify:
|
||||
subject = "Verify your email address"
|
||||
templatePath = "resources/email_verify.html"
|
||||
default:
|
||||
return fmt.Errorf("invalid token type: %v", tokenType)
|
||||
}
|
||||
htmlTemplate, err := assets.GetFileContent(templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read email template: %s", err)
|
||||
}
|
||||
|
||||
message := mail.NewMsg()
|
||||
if err := message.From(userSmtp); err != nil {
|
||||
return fmt.Errorf("failed to set From email address: %s", err)
|
||||
}
|
||||
if err := message.To(toEmail); err != nil {
|
||||
return fmt.Errorf("failed to set To email address: %s", err)
|
||||
}
|
||||
|
||||
finalHTML := strings.ReplaceAll(htmlTemplate, "{{OTP_CODE}}", otpCode)
|
||||
|
||||
message.Subject(subject)
|
||||
message.SetBodyString(mail.TypeTextHTML, finalHTML)
|
||||
client, err := mail.NewClient(
|
||||
"smtp.gmail.com",
|
||||
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover),
|
||||
mail.WithTLSPortPolicy(mail.TLSMandatory),
|
||||
mail.WithUsername(userSmtp),
|
||||
mail.WithPassword(passSmtp),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create mail client: %s", err)
|
||||
}
|
||||
|
||||
err = client.DialAndSend(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send mail: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user