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 . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o history-api ./cmd/api
|
||||||
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 email-worker ./cmd/worker/email
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
@@ -20,9 +20,10 @@ ENV TZ=Asia/Ho_Chi_Minh
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/history-api .
|
COPY --from=builder /app/history-api .
|
||||||
|
COPY --from=builder /app/email-worker .
|
||||||
COPY data ./data
|
COPY data ./data
|
||||||
|
|
||||||
RUN chmod +x ./history-api
|
RUN chmod +x ./history-api ./email-worker
|
||||||
|
|
||||||
EXPOSE 3344
|
EXPOSE 3344
|
||||||
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -1,6 +1,6 @@
|
|||||||
DB_URL ?= postgres://history:secret@localhost:5432/history_map?sslmode=disable
|
DB_URL ?= postgres://history:secret@localhost:5432/history_map?sslmode=disable
|
||||||
APP_DIR = cmd/history-api
|
APP_DIR = cmd/api
|
||||||
MAIN_APP = ./cmd/history-api/
|
MAIN_APP = ./cmd/api/
|
||||||
MAIN_FILE = $(APP_DIR)/main.go
|
MAIN_FILE = $(APP_DIR)/main.go
|
||||||
DOCS_DIR = docs
|
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>
|
||||||
@@ -35,10 +35,10 @@ func NewHttpServer() *FiberServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
cfg := swagger.Config{
|
cfg := swagger.Config{
|
||||||
BasePath: "/",
|
BasePath: "/",
|
||||||
FileContent: docs.SwaggerJSON,
|
FileContent: docs.SwaggerJSON,
|
||||||
Path: "swagger",
|
Path: "swagger",
|
||||||
Title: "Swagger API Docs",
|
Title: "Swagger API Docs",
|
||||||
}
|
}
|
||||||
|
|
||||||
server.App.Use(swagger.New(cfg))
|
server.App.Use(swagger.New(cfg))
|
||||||
@@ -64,17 +64,21 @@ func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.
|
|||||||
userRepo := repositories.NewUserRepository(sqlPg, redis)
|
userRepo := repositories.NewUserRepository(sqlPg, redis)
|
||||||
roleRepo := repositories.NewRoleRepository(sqlPg, redis)
|
roleRepo := repositories.NewRoleRepository(sqlPg, redis)
|
||||||
tileRepo := repositories.NewTileRepository(sqlTile, redis)
|
tileRepo := repositories.NewTileRepository(sqlTile, redis)
|
||||||
|
tokenRepo := repositories.NewTokenRepository(redis)
|
||||||
|
|
||||||
// service setup
|
// service setup
|
||||||
authService := services.NewAuthService(userRepo, roleRepo)
|
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis)
|
||||||
|
userService := services.NewUserService(userRepo, roleRepo)
|
||||||
tileService := services.NewTileService(tileRepo)
|
tileService := services.NewTileService(tileRepo)
|
||||||
|
|
||||||
// controller setup
|
// controller setup
|
||||||
authController := controllers.NewAuthController(authService)
|
authController := controllers.NewAuthController(authService)
|
||||||
|
userController := controllers.NewUserController(userService)
|
||||||
tileController := controllers.NewTileController(tileService)
|
tileController := controllers.NewTileController(tileService)
|
||||||
|
|
||||||
// route setup
|
// 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.TileRoutes(s.App, tileController)
|
||||||
routes.NotFoundRoute(s.App)
|
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,
|
password_hash TEXT,
|
||||||
google_id VARCHAR(255) UNIQUE,
|
google_id VARCHAR(255) UNIQUE,
|
||||||
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||||
is_verified BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
token_version INT NOT NULL DEFAULT 1,
|
token_version INT NOT NULL DEFAULT 1,
|
||||||
refresh_token TEXT,
|
refresh_token TEXT,
|
||||||
@@ -22,10 +21,6 @@ CREATE INDEX idx_users_email_active
|
|||||||
ON users (email)
|
ON users (email)
|
||||||
WHERE is_deleted = false;
|
WHERE is_deleted = false;
|
||||||
|
|
||||||
CREATE INDEX idx_users_verified
|
|
||||||
ON users (is_verified)
|
|
||||||
WHERE is_deleted = false;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
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
|
SELECT id, name, is_deleted, created_at, updated_at FROM roles
|
||||||
WHERE id = $1 AND is_deleted = false;
|
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
|
-- name: AddUserRole :exec
|
||||||
INSERT INTO user_roles (user_id, role_id)
|
INSERT INTO user_roles (user_id, role_id)
|
||||||
SELECT $1, r.id
|
SELECT $1, unnest($2::uuid[])
|
||||||
FROM roles r
|
|
||||||
WHERE r.name = $2
|
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- name: RemoveUserRole :exec
|
-- name: RemoveUserRole :exec
|
||||||
|
|||||||
@@ -3,17 +3,14 @@ INSERT INTO users (
|
|||||||
email,
|
email,
|
||||||
password_hash,
|
password_hash,
|
||||||
google_id,
|
google_id,
|
||||||
auth_provider,
|
auth_provider
|
||||||
is_verified
|
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5
|
$1, $2, $3, $4
|
||||||
)
|
)
|
||||||
ON CONFLICT (email)
|
ON CONFLICT (email)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
google_id = EXCLUDED.google_id,
|
google_id = EXCLUDED.google_id,
|
||||||
auth_provider = EXCLUDED.auth_provider,
|
auth_provider = EXCLUDED.auth_provider
|
||||||
is_verified = users.is_verified OR EXCLUDED.is_verified,
|
|
||||||
updated_at = now()
|
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: CreateUserProfile :one
|
-- name: CreateUserProfile :one
|
||||||
@@ -55,12 +52,6 @@ SET
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND is_deleted = false;
|
AND is_deleted = false;
|
||||||
|
|
||||||
-- name: VerifyUser :exec
|
|
||||||
UPDATE users
|
|
||||||
SET
|
|
||||||
is_verified = true
|
|
||||||
WHERE id = $1
|
|
||||||
AND is_deleted = false;
|
|
||||||
|
|
||||||
-- name: DeleteUser :exec
|
-- name: DeleteUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
@@ -79,7 +70,6 @@ SELECT
|
|||||||
u.id,
|
u.id,
|
||||||
u.email,
|
u.email,
|
||||||
u.password_hash,
|
u.password_hash,
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
u.token_version,
|
||||||
u.refresh_token,
|
u.refresh_token,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
@@ -116,6 +106,47 @@ SELECT
|
|||||||
FROM users u
|
FROM users u
|
||||||
WHERE u.id = $1 AND u.is_deleted = false;
|
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
|
-- name: GetTokenVersion :one
|
||||||
SELECT token_version
|
SELECT token_version
|
||||||
FROM users
|
FROM users
|
||||||
@@ -131,7 +162,6 @@ SELECT
|
|||||||
u.id,
|
u.id,
|
||||||
u.email,
|
u.email,
|
||||||
u.password_hash,
|
u.password_hash,
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
u.token_version,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
@@ -170,7 +200,59 @@ SELECT
|
|||||||
u.id,
|
u.id,
|
||||||
u.email,
|
u.email,
|
||||||
u.password_hash,
|
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.token_version,
|
||||||
u.refresh_token,
|
u.refresh_token,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
@@ -203,4 +285,26 @@ SELECT
|
|||||||
) AS roles
|
) AS roles
|
||||||
|
|
||||||
FROM users u
|
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,
|
password_hash TEXT,
|
||||||
google_id VARCHAR(255) UNIQUE,
|
google_id VARCHAR(255) UNIQUE,
|
||||||
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
auth_provider VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||||
is_verified BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
token_version INT NOT NULL DEFAULT 1,
|
token_version INT NOT NULL DEFAULT 1,
|
||||||
refresh_token TEXT,
|
refresh_token TEXT,
|
||||||
@@ -49,14 +48,4 @@ CREATE TABLE IF NOT EXISTS user_verifications (
|
|||||||
reviewed_by UUID REFERENCES users(id),
|
reviewed_by UUID REFERENCES users(id),
|
||||||
reviewed_at TIMESTAMPTZ,
|
reviewed_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
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,44 +13,43 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- pg_data:/var/lib/postgresql/data
|
- pg_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
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
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- history-api-project
|
- history-api-project
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
image: redis:8.6.1-alpine
|
image: redis:8.6.2-alpine
|
||||||
container_name: history_redis
|
container_name: history_redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- history-api-project
|
- history-api-project
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
build:
|
image: migrate/migrate
|
||||||
context: .
|
|
||||||
dockerfile_inline: |
|
|
||||||
FROM migrate/migrate
|
|
||||||
COPY db/migrations /migrations
|
|
||||||
container_name: history_migrate
|
container_name: history_migrate
|
||||||
|
volumes:
|
||||||
|
- ./db/migrations:/migrations:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- ./assets/resources/.env
|
- ./assets/resources/.env
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
- |
|
- |
|
||||||
DB_URL="postgres://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@db:5432/$${POSTGRES_DB}?sslmode=disable"
|
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
|
echo "Running migrations..."
|
||||||
/migrate -path /migrations -database "$$DB_URL" up || \
|
# We skip the 'version' check loop because 'depends_on'
|
||||||
(/migrate -path /migrations -database "$$DB_URL" force 8 && \
|
# with 'service_healthy' already handles the wait.
|
||||||
/migrate -path /migrations -database "$$DB_URL" up)
|
/migrate -path /migrations -database "$$DB_URL" up
|
||||||
networks:
|
networks:
|
||||||
- history-api-project
|
- history-api-project
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
container_name: history_app
|
container_name: history_app
|
||||||
@@ -58,13 +57,32 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
migrate:
|
migrate:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
cache:
|
cache:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
env_file:
|
||||||
|
- ./assets/resources/.env
|
||||||
ports:
|
ports:
|
||||||
- "3344:3344"
|
- "3344:3344"
|
||||||
networks:
|
networks:
|
||||||
- history-api-project
|
- 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:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
|
|
||||||
|
|||||||
839
docs/docs.go
839
docs/docs.go
@@ -24,14 +24,9 @@ const docTemplate = `{
|
|||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/refresh": {
|
"/auth/forgot-password": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"description": "Initiate password recovery process for a user",
|
||||||
{
|
|
||||||
"BearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Get a new access token using the user's current session/refresh token",
|
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -41,44 +36,15 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Refresh access token",
|
"summary": "Handle forgotten password",
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/auth/signin": {
|
|
||||||
"post": {
|
|
||||||
"description": "Authenticate user and return token data",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Auth"
|
|
||||||
],
|
|
||||||
"summary": "Sign in an existing user",
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Sign In request",
|
"description": "Forgot Password request",
|
||||||
"name": "request",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"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": {
|
"post": {
|
||||||
"description": "Create a new user account",
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Generate a new access token using a valid refresh token from context",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -116,10 +87,97 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Auth"
|
"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": [
|
"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",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"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": {
|
"/tiles/metadata": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Retrieve map metadata",
|
"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": {
|
"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": {
|
"history-api_internal_dtos_request.SignInDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -260,7 +925,8 @@ const docTemplate = `{
|
|||||||
"required": [
|
"required": [
|
||||||
"display_name",
|
"display_name",
|
||||||
"email",
|
"email",
|
||||||
"password"
|
"password",
|
||||||
|
"token_id"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"display_name": {
|
"display_name": {
|
||||||
@@ -277,6 +943,75 @@ const docTemplate = `{
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 64,
|
"maxLength": 64,
|
||||||
"minLength": 8
|
"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"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"history-api_pkg_constants.TokenType": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"enum": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"TokenPasswordReset",
|
||||||
|
"TokenEmailVerify",
|
||||||
|
"TokenMagicLink",
|
||||||
|
"TokenRefreshToken"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
|||||||
@@ -22,14 +22,9 @@
|
|||||||
"host": "history-api.kain.id.vn",
|
"host": "history-api.kain.id.vn",
|
||||||
"basePath": "/",
|
"basePath": "/",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/refresh": {
|
"/auth/forgot-password": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"description": "Initiate password recovery process for a user",
|
||||||
{
|
|
||||||
"BearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Get a new access token using the user's current session/refresh token",
|
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -39,44 +34,15 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Refresh access token",
|
"summary": "Handle forgotten password",
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/auth/signin": {
|
|
||||||
"post": {
|
|
||||||
"description": "Authenticate user and return token data",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Auth"
|
|
||||||
],
|
|
||||||
"summary": "Sign in an existing user",
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Sign In request",
|
"description": "Forgot Password request",
|
||||||
"name": "request",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"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": {
|
"post": {
|
||||||
"description": "Create a new user account",
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Generate a new access token using a valid refresh token from context",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -114,10 +85,97 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Auth"
|
"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": [
|
"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",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"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": {
|
"/tiles/metadata": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Retrieve map metadata",
|
"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": {
|
"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": {
|
"history-api_internal_dtos_request.SignInDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -258,7 +923,8 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"display_name",
|
"display_name",
|
||||||
"email",
|
"email",
|
||||||
"password"
|
"password",
|
||||||
|
"token_id"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"display_name": {
|
"display_name": {
|
||||||
@@ -275,6 +941,75 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 64,
|
"maxLength": 64,
|
||||||
"minLength": 8
|
"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"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"history-api_pkg_constants.TokenType": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"enum": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"TokenPasswordReset",
|
||||||
|
"TokenEmailVerify",
|
||||||
|
"TokenMagicLink",
|
||||||
|
"TokenRefreshToken"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
|||||||
@@ -1,5 +1,65 @@
|
|||||||
basePath: /
|
basePath: /
|
||||||
definitions:
|
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:
|
history-api_internal_dtos_request.SignInDto:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
@@ -28,10 +88,59 @@ definitions:
|
|||||||
maxLength: 64
|
maxLength: 64
|
||||||
minLength: 8
|
minLength: 8
|
||||||
type: string
|
type: string
|
||||||
|
token_id:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- display_name
|
- display_name
|
||||||
- email
|
- email
|
||||||
- password
|
- 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
|
type: object
|
||||||
history-api_internal_dtos_response.CommonResponse:
|
history-api_internal_dtos_response.CommonResponse:
|
||||||
properties:
|
properties:
|
||||||
@@ -41,6 +150,19 @@ definitions:
|
|||||||
status:
|
status:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
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
|
host: history-api.kain.id.vn
|
||||||
info:
|
info:
|
||||||
contact:
|
contact:
|
||||||
@@ -55,12 +177,18 @@ info:
|
|||||||
title: History API
|
title: History API
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
paths:
|
paths:
|
||||||
/auth/refresh:
|
/auth/forgot-password:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Get a new access token using the user's current session/refresh
|
description: Initiate password recovery process for a user
|
||||||
token
|
parameters:
|
||||||
|
- description: Forgot Password request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_request.ForgotPasswordDto'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -68,22 +196,49 @@ paths:
|
|||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
$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":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
summary: Refresh access token
|
summary: Refresh session tokens
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
/auth/signin:
|
/auth/signin:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Authenticate user and return token data
|
description: Authenticate user credentials and return access/refresh tokens
|
||||||
parameters:
|
parameters:
|
||||||
- description: Sign In request
|
- description: Sign In credentials
|
||||||
in: body
|
in: body
|
||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
@@ -100,20 +255,24 @@ paths:
|
|||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"401":
|
||||||
|
description: Invalid credentials
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
summary: Sign in an existing user
|
summary: Sign in a user
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
/auth/signup:
|
/auth/signup:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Create a new user account
|
description: Create a new user account in the system
|
||||||
parameters:
|
parameters:
|
||||||
- description: Sign Up request
|
- description: Sign Up details
|
||||||
in: body
|
in: body
|
||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
@@ -134,7 +293,67 @@ paths:
|
|||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
$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:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
/tiles/{z}/{x}/{y}:
|
/tiles/{z}/{x}/{y}:
|
||||||
@@ -193,6 +412,281 @@ paths:
|
|||||||
summary: Get tile metadata
|
summary: Get tile metadata
|
||||||
tags:
|
tags:
|
||||||
- Tile
|
- 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:
|
schemes:
|
||||||
- https
|
- https
|
||||||
- http
|
- http
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -10,11 +10,13 @@ require (
|
|||||||
github.com/gofiber/contrib/v3/zerolog v1.0.1
|
github.com/gofiber/contrib/v3/zerolog v1.0.1
|
||||||
github.com/gofiber/fiber/v3 v3.1.0
|
github.com/gofiber/fiber/v3 v3.1.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.8.0
|
github.com/jackc/pgx/v5 v5.8.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/redis/go-redis/v9 v9.18.0
|
github.com/redis/go-redis/v9 v9.18.0
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
|
github.com/wneessen/go-mail v0.7.2
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.49.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,7 +51,6 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/gofiber/schema v1.7.0 // indirect
|
github.com/gofiber/schema v1.7.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
|||||||
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/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 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
|||||||
@@ -20,14 +20,15 @@ func NewAuthController(svc services.AuthService) *AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Signin godoc
|
// Signin godoc
|
||||||
// @Summary Sign in an existing user
|
// @Summary Sign in a user
|
||||||
// @Description Authenticate user and return token data
|
// @Description Authenticate user credentials and return access/refresh tokens
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce 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
|
// @Success 200 {object} response.CommonResponse
|
||||||
// @Failure 400 {object} response.CommonResponse
|
// @Failure 400 {object} response.CommonResponse
|
||||||
|
// @Failure 401 {object} response.CommonResponse "Invalid credentials"
|
||||||
// @Failure 500 {object} response.CommonResponse
|
// @Failure 500 {object} response.CommonResponse
|
||||||
// @Router /auth/signin [post]
|
// @Router /auth/signin [post]
|
||||||
func (h *AuthController) Signin(c fiber.Ctx) error {
|
func (h *AuthController) Signin(c fiber.Ctx) error {
|
||||||
@@ -57,12 +58,12 @@ func (h *AuthController) Signin(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Signup godoc
|
// Signup godoc
|
||||||
// @Summary Sign up a new user
|
// @Summary Register a new user
|
||||||
// @Description Create a new user account
|
// @Description Create a new user account in the system
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce 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
|
// @Success 200 {object} response.CommonResponse
|
||||||
// @Failure 400 {object} response.CommonResponse
|
// @Failure 400 {object} response.CommonResponse
|
||||||
// @Failure 500 {object} response.CommonResponse
|
// @Failure 500 {object} response.CommonResponse
|
||||||
@@ -94,13 +95,14 @@ func (h *AuthController) Signup(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RefreshToken godoc
|
// RefreshToken godoc
|
||||||
// @Summary Refresh access token
|
// @Summary Refresh session tokens
|
||||||
// @Description Get a new access token using the user's current session/refresh token
|
// @Description Generate a new access token using a valid refresh token from context
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {object} response.CommonResponse
|
// @Success 200 {object} response.CommonResponse
|
||||||
|
// @Failure 401 {object} response.CommonResponse "Unauthorized or expired refresh token"
|
||||||
// @Failure 500 {object} response.CommonResponse
|
// @Failure 500 {object} response.CommonResponse
|
||||||
// @Router /auth/refresh [post]
|
// @Router /auth/refresh [post]
|
||||||
func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
||||||
@@ -120,3 +122,116 @@ func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
|||||||
Data: res,
|
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
|
package request
|
||||||
|
|
||||||
|
import "history-api/pkg/constants"
|
||||||
|
|
||||||
type SignUpDto struct {
|
type SignUpDto struct {
|
||||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||||
Password string `json:"password" validate:"required,min=8,max=64"`
|
Password string `json:"password" validate:"required,min=8,max=64"`
|
||||||
DisplayName string `json:"display_name" validate:"required,min=2,max=50"`
|
DisplayName string `json:"display_name" validate:"required,min=2,max=50"`
|
||||||
|
TokenID string `json:"token_id" validate:"required,uuid"`
|
||||||
}
|
}
|
||||||
type SignInDto struct {
|
type SignInDto struct {
|
||||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||||
Password string `json:"password" validate:"required,min=8,max=64"`
|
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
|
package request
|
||||||
|
|
||||||
import "history-api/pkg/constant"
|
type UpdateProfileDto struct {
|
||||||
|
DisplayName string `json:"display_name" validate:"omitempty,min=2,max=50"`
|
||||||
type CreateUserDto struct {
|
FullName string `json:"full_name" validate:"omitempty,min=2,max=100"`
|
||||||
Username string `json:"username" validate:"required"`
|
AvatarUrl string `json:"avatar_url" validate:"omitempty,url"`
|
||||||
Password string `json:"password" validate:"required"`
|
Bio string `json:"bio" validate:"omitempty,max=255"`
|
||||||
DiscordUserId string `json:"discord_user_id" validate:"required"`
|
Location string `json:"location" validate:"omitempty,max=100"`
|
||||||
Role []constant.Role `json:"role" validate:"required"`
|
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 {
|
type ChangePasswordDto struct {
|
||||||
Password *string `json:"password" validate:"omitempty"`
|
OldPassword string `json:"old_password" validate:"required,min=8,max=64"`
|
||||||
DiscordUserId *string `json:"discord_user_id" validate:"omitempty"`
|
NewPassword string `json:"new_password" validate:"required,min=8,max=64,nefield=OldPassword"`
|
||||||
Role *[]constant.Role `json:"role" validate:"omitempty"`
|
}
|
||||||
|
|
||||||
|
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 {
|
type SearchUserDto struct {
|
||||||
Username *string `query:"username" validate:"omitempty"`
|
CursorPaginationDto
|
||||||
DiscordUserId *string `query:"discord_user_id" validate:"omitempty"`
|
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
|
||||||
Role *[]constant.Role `query:"role" validate:"omitempty"`
|
IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"`
|
||||||
SortBy string `query:"sort_by" default:"created_at" validate:"oneof=created_at updated_at"`
|
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"`
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ type AuthResponse struct {
|
|||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VerifyTokenResponse struct {
|
||||||
|
TokenID string `json:"token_id"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package response
|
package response
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"history-api/pkg/constant"
|
"history-api/pkg/constants"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
@@ -13,7 +13,18 @@ type CommonResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
UId string `json:"uid"`
|
UId string `json:"uid"`
|
||||||
Roles []constant.Role `json:"roles"`
|
Roles []constants.Role `json:"roles"`
|
||||||
|
TokenVersion int32 `json:"token_version"`
|
||||||
jwt.RegisteredClaims
|
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"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
GoogleID pgtype.Text `json:"google_id"`
|
GoogleID pgtype.Text `json:"google_id"`
|
||||||
AuthProvider string `json:"auth_provider"`
|
AuthProvider string `json:"auth_provider"`
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
IsDeleted bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||||
@@ -49,16 +48,6 @@ type UserRole struct {
|
|||||||
RoleID pgtype.UUID `json:"role_id"`
|
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 {
|
type UserVerification struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
|||||||
@@ -13,19 +13,17 @@ import (
|
|||||||
|
|
||||||
const addUserRole = `-- name: AddUserRole :exec
|
const addUserRole = `-- name: AddUserRole :exec
|
||||||
INSERT INTO user_roles (user_id, role_id)
|
INSERT INTO user_roles (user_id, role_id)
|
||||||
SELECT $1, r.id
|
SELECT $1, unnest($2::uuid[])
|
||||||
FROM roles r
|
|
||||||
WHERE r.name = $2
|
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddUserRoleParams struct {
|
type AddUserRoleParams struct {
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +127,38 @@ func (q *Queries) GetRoles(ctx context.Context) ([]Role, error) {
|
|||||||
return items, nil
|
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
|
const removeAllRolesFromUser = `-- name: RemoveAllRolesFromUser :exec
|
||||||
DELETE FROM user_roles
|
DELETE FROM user_roles
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ SELECT
|
|||||||
u.id,
|
u.id,
|
||||||
u.email,
|
u.email,
|
||||||
u.password_hash,
|
u.password_hash,
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
u.token_version,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
@@ -116,7 +115,6 @@ type GetUserByEmailRow struct {
|
|||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash pgtype.Text `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
IsDeleted bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
@@ -132,7 +130,6 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.IsVerified,
|
|
||||||
&i.TokenVersion,
|
&i.TokenVersion,
|
||||||
&i.IsDeleted,
|
&i.IsDeleted,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@@ -148,7 +145,6 @@ SELECT
|
|||||||
u.id,
|
u.id,
|
||||||
u.email,
|
u.email,
|
||||||
u.password_hash,
|
u.password_hash,
|
||||||
u.is_verified,
|
|
||||||
u.token_version,
|
u.token_version,
|
||||||
u.refresh_token,
|
u.refresh_token,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
@@ -190,7 +186,6 @@ type GetUserByIDRow struct {
|
|||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash pgtype.Text `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||||
IsDeleted bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
@@ -207,7 +202,79 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PasswordHash,
|
&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.TokenVersion,
|
||||||
&i.RefreshToken,
|
&i.RefreshToken,
|
||||||
&i.IsDeleted,
|
&i.IsDeleted,
|
||||||
@@ -224,7 +291,127 @@ SELECT
|
|||||||
u.id,
|
u.id,
|
||||||
u.email,
|
u.email,
|
||||||
u.password_hash,
|
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.token_version,
|
||||||
u.refresh_token,
|
u.refresh_token,
|
||||||
u.is_deleted,
|
u.is_deleted,
|
||||||
@@ -257,14 +444,44 @@ SELECT
|
|||||||
) AS roles
|
) AS roles
|
||||||
|
|
||||||
FROM users u
|
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"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash pgtype.Text `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||||
IsDeleted bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
@@ -274,20 +491,26 @@ type GetUsersRow struct {
|
|||||||
Roles []byte `json:"roles"`
|
Roles []byte `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]SearchUsersRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getUsers)
|
rows, err := q.db.Query(ctx, searchUsers,
|
||||||
|
arg.Cursor,
|
||||||
|
arg.IsDeleted,
|
||||||
|
arg.RoleIds,
|
||||||
|
arg.SearchID,
|
||||||
|
arg.SearchText,
|
||||||
|
arg.Limit,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
items := []GetUsersRow{}
|
items := []SearchUsersRow{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetUsersRow
|
var i SearchUsersRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.IsVerified,
|
|
||||||
&i.TokenVersion,
|
&i.TokenVersion,
|
||||||
&i.RefreshToken,
|
&i.RefreshToken,
|
||||||
&i.IsDeleted,
|
&i.IsDeleted,
|
||||||
@@ -306,18 +529,6 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
|||||||
return items, nil
|
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
|
const updateTokenVersion = `-- name: UpdateTokenVersion :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET token_version = $2
|
SET token_version = $2
|
||||||
@@ -432,18 +643,15 @@ INSERT INTO users (
|
|||||||
email,
|
email,
|
||||||
password_hash,
|
password_hash,
|
||||||
google_id,
|
google_id,
|
||||||
auth_provider,
|
auth_provider
|
||||||
is_verified
|
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5
|
$1, $2, $3, $4
|
||||||
)
|
)
|
||||||
ON CONFLICT (email)
|
ON CONFLICT (email)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
google_id = EXCLUDED.google_id,
|
google_id = EXCLUDED.google_id,
|
||||||
auth_provider = EXCLUDED.auth_provider,
|
auth_provider = EXCLUDED.auth_provider
|
||||||
is_verified = users.is_verified OR EXCLUDED.is_verified,
|
RETURNING id, email, password_hash, google_id, auth_provider, is_deleted, token_version, refresh_token, created_at, updated_at
|
||||||
updated_at = now()
|
|
||||||
RETURNING id, email, password_hash, google_id, auth_provider, is_verified, is_deleted, token_version, refresh_token, created_at, updated_at
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpsertUserParams struct {
|
type UpsertUserParams struct {
|
||||||
@@ -451,7 +659,6 @@ type UpsertUserParams struct {
|
|||||||
PasswordHash pgtype.Text `json:"password_hash"`
|
PasswordHash pgtype.Text `json:"password_hash"`
|
||||||
GoogleID pgtype.Text `json:"google_id"`
|
GoogleID pgtype.Text `json:"google_id"`
|
||||||
AuthProvider string `json:"auth_provider"`
|
AuthProvider string `json:"auth_provider"`
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, error) {
|
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.PasswordHash,
|
||||||
arg.GoogleID,
|
arg.GoogleID,
|
||||||
arg.AuthProvider,
|
arg.AuthProvider,
|
||||||
arg.IsVerified,
|
|
||||||
)
|
)
|
||||||
var i User
|
var i User
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -469,7 +675,6 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
|||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.GoogleID,
|
&i.GoogleID,
|
||||||
&i.AuthProvider,
|
&i.AuthProvider,
|
||||||
&i.IsVerified,
|
|
||||||
&i.IsDeleted,
|
&i.IsDeleted,
|
||||||
&i.TokenVersion,
|
&i.TokenVersion,
|
||||||
&i.RefreshToken,
|
&i.RefreshToken,
|
||||||
@@ -478,16 +683,3 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
|||||||
)
|
)
|
||||||
return i, err
|
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 (
|
import (
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/repositories"
|
||||||
"history-api/pkg/config"
|
"history-api/pkg/config"
|
||||||
"history-api/pkg/constant"
|
"history-api/pkg/constants"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
jwtware "github.com/gofiber/contrib/v3/jwt"
|
jwtware "github.com/gofiber/contrib/v3/jwt"
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/extractors"
|
"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")
|
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -20,13 +22,13 @@ func JwtAccess() fiber.Handler {
|
|||||||
return jwtware.New(jwtware.Config{
|
return jwtware.New(jwtware.Config{
|
||||||
SigningKey: jwtware.SigningKey{Key: []byte(jwtSecret)},
|
SigningKey: jwtware.SigningKey{Key: []byte(jwtSecret)},
|
||||||
ErrorHandler: jwtError,
|
ErrorHandler: jwtError,
|
||||||
SuccessHandler: jwtSuccess,
|
SuccessHandler: jwtSuccess(userRepo),
|
||||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
Extractor: extractors.FromAuthHeader("Bearer"),
|
||||||
Claims: &response.JWTClaims{},
|
Claims: &response.JWTClaims{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func JwtRefresh() fiber.Handler {
|
func JwtRefresh(userRepo repositories.UserRepository) fiber.Handler {
|
||||||
jwtRefreshSecret, err := config.GetConfig("JWT_REFRESH_SECRET")
|
jwtRefreshSecret, err := config.GetConfig("JWT_REFRESH_SECRET")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -35,41 +37,61 @@ func JwtRefresh() fiber.Handler {
|
|||||||
return jwtware.New(jwtware.Config{
|
return jwtware.New(jwtware.Config{
|
||||||
SigningKey: jwtware.SigningKey{Key: []byte(jwtRefreshSecret)},
|
SigningKey: jwtware.SigningKey{Key: []byte(jwtRefreshSecret)},
|
||||||
ErrorHandler: jwtError,
|
ErrorHandler: jwtError,
|
||||||
SuccessHandler: jwtSuccess,
|
SuccessHandler: jwtSuccess(userRepo),
|
||||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
Extractor: extractors.FromAuthHeader("Bearer"),
|
||||||
Claims: &response.JWTClaims{},
|
Claims: &response.JWTClaims{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func jwtSuccess(c fiber.Ctx) error {
|
func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
|
||||||
user := jwtware.FromContext(c)
|
return func(c fiber.Ctx) error {
|
||||||
unauthorized := func() error {
|
user := jwtware.FromContext(c)
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
|
||||||
Status: false,
|
unauthorized := func() error {
|
||||||
Message: "Invalid or missing token",
|
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||||
})
|
Status: false,
|
||||||
|
Message: "Invalid or missing token",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return unauthorized()
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := user.Claims.(*response.JWTClaims)
|
||||||
|
if !ok {
|
||||||
|
return unauthorized()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
return unauthorized()
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := user.Claims.(*response.JWTClaims)
|
|
||||||
if !ok {
|
|
||||||
return unauthorized()
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(claims.Roles, constant.BANNED) {
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(response.CommonResponse{
|
|
||||||
Status: false,
|
|
||||||
Message: "User account is banned",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Locals("uid", claims.UId)
|
|
||||||
c.Locals("user_claims", claims)
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
}
|
||||||
func jwtError(c fiber.Ctx, err error) error {
|
func jwtError(c fiber.Ctx, err error) error {
|
||||||
if err.Error() == "Missing or malformed JWT" {
|
if err.Error() == "Missing or malformed JWT" {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package middlewares
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
"history-api/pkg/constant"
|
"history-api/pkg/constants"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"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")
|
claimsVal := c.Locals("user_claims")
|
||||||
if claimsVal == nil {
|
if claimsVal == nil {
|
||||||
return nil, fiber.ErrUnauthorized
|
return nil, fiber.ErrUnauthorized
|
||||||
@@ -22,7 +22,7 @@ func getRoles(c fiber.Ctx) ([]constant.Role, error) {
|
|||||||
return claims.Roles, nil
|
return claims.Roles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RequireAnyRole(required ...constant.Role) fiber.Handler {
|
func RequireAnyRole(required ...constants.Role) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
userRoles, err := getRoles(c)
|
userRoles, err := getRoles(c)
|
||||||
if err != nil {
|
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 {
|
return func(c fiber.Ctx) error {
|
||||||
userRoles, err := getRoles(c)
|
userRoles, err := getRoles(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
"history-api/pkg/constant"
|
"history-api/pkg/constants"
|
||||||
"time"
|
"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 {
|
func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||||
out := make([]*response.RoleResponse, len(rs))
|
out := make([]*response.RoleResponse, len(rs))
|
||||||
for i := range rs {
|
for i := range rs {
|
||||||
@@ -52,10 +59,10 @@ func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func RolesEntityToRoleConstant(rs []*RoleSimple) []constant.Role {
|
func RolesEntityToRoleConstant(rs []*RoleSimple) []constants.Role {
|
||||||
out := make([]constant.Role, len(rs))
|
out := make([]constants.Role, len(rs))
|
||||||
for i := range rs {
|
for i := range rs {
|
||||||
data, ok := constant.ParseRole(rs[i].Name)
|
data, ok := constants.ParseRole(rs[i].Name)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import "history-api/pkg/constants"
|
||||||
"history-api/internal/dtos/response"
|
|
||||||
"history-api/pkg/convert"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TokenEntity struct {
|
type TokenEntity struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
Email string `json:"email"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
Token string `json:"token"`
|
||||||
Token string `json:"token"`
|
TokenType constants.TokenType `json:"token_type"`
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,22 @@ package repositories
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"history-api/internal/gen/sqlc"
|
"history-api/internal/gen/sqlc"
|
||||||
"history-api/internal/models"
|
"history-api/internal/models"
|
||||||
"history-api/pkg/cache"
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/constants"
|
||||||
"history-api/pkg/convert"
|
"history-api/pkg/convert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoleRepository interface {
|
type RoleRepository interface {
|
||||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error)
|
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)
|
GetByname(ctx context.Context, name string) (*models.RoleEntity, error)
|
||||||
All(ctx context.Context) ([]*models.RoleEntity, error)
|
All(ctx context.Context) ([]*models.RoleEntity, error)
|
||||||
Create(ctx context.Context, name string) (*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) {
|
func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) {
|
||||||
cacheId := fmt.Sprintf("role:id:%s", convert.UUIDToString(id))
|
cacheId := fmt.Sprintf("role:id:%s", convert.UUIDToString(id))
|
||||||
var role models.RoleEntity
|
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),
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
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
|
return &role, nil
|
||||||
}
|
}
|
||||||
@@ -83,7 +136,7 @@ func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.Ro
|
|||||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
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
|
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:name:%s", name): role,
|
||||||
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): 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
|
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:name:%s", row.Name): role,
|
||||||
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): 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
|
return &role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"history-api/pkg/cache"
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/constants"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, er
|
|||||||
metadata[name] = value
|
metadata[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = r.c.Set(ctx, cacheId, metadata, 10*time.Minute)
|
_ = r.c.Set(ctx, cacheId, metadata, constants.NormalCacheDuration)
|
||||||
|
|
||||||
return metadata, nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"history-api/internal/gen/sqlc"
|
"history-api/internal/gen/sqlc"
|
||||||
"history-api/internal/models"
|
"history-api/internal/models"
|
||||||
"history-api/pkg/cache"
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/constants"
|
||||||
"history-api/pkg/convert"
|
"history-api/pkg/convert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
||||||
|
GetByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
||||||
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
|
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
|
||||||
All(ctx context.Context) ([]*models.UserEntity, error)
|
All(ctx context.Context, 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)
|
UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error)
|
||||||
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
|
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
|
||||||
UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, 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
|
UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error
|
||||||
GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error)
|
GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error)
|
||||||
UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) 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
|
Delete(ctx context.Context, id pgtype.UUID) error
|
||||||
Restore(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) {
|
func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
|
||||||
cacheId := fmt.Sprintf("user:id:%s", convert.UUIDToString(id))
|
cacheId := fmt.Sprintf("user:id:%s", convert.UUIDToString(id))
|
||||||
var user models.UserEntity
|
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),
|
ID: convert.UUIDToString(row.ID),
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
IsVerified: row.IsVerified,
|
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
@@ -73,7 +121,43 @@ func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.U
|
|||||||
return nil, err
|
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
|
return &user, nil
|
||||||
}
|
}
|
||||||
@@ -96,7 +180,6 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
|
|||||||
ID: convert.UUIDToString(row.ID),
|
ID: convert.UUIDToString(row.ID),
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
IsVerified: row.IsVerified,
|
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
@@ -111,7 +194,7 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
@@ -121,12 +204,17 @@ func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserP
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
|
bgCtx := context.Background()
|
||||||
|
|
||||||
|
_ = r.c.DelByPattern(bgCtx, "user:all*")
|
||||||
|
_ = r.c.DelByPattern(bgCtx, "user:search*")
|
||||||
|
}()
|
||||||
|
|
||||||
return &models.UserEntity{
|
return &models.UserEntity{
|
||||||
ID: convert.UUIDToString(row.ID),
|
ID: convert.UUIDToString(row.ID),
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
IsVerified: row.IsVerified,
|
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
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:email:%s", user.Email): user,
|
||||||
fmt.Sprintf("user:id:%s", user.ID): 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
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,19 +271,27 @@ func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUs
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
|
func (r *userRepository) All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error) {
|
||||||
rows, err := r.q.GetUsers(ctx)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var users []*models.UserEntity
|
var users []*models.UserEntity
|
||||||
|
var ids []string
|
||||||
|
usersToCache := make(map[string]any)
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
user := &models.UserEntity{
|
user := &models.UserEntity{
|
||||||
ID: convert.UUIDToString(row.ID),
|
ID: convert.UUIDToString(row.ID),
|
||||||
Email: row.Email,
|
Email: row.Email,
|
||||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||||
IsVerified: row.IsVerified,
|
|
||||||
TokenVersion: row.TokenVersion,
|
TokenVersion: row.TokenVersion,
|
||||||
IsDeleted: row.IsDeleted,
|
IsDeleted: row.IsDeleted,
|
||||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
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 {
|
if err := user.ParseRoles(row.Roles); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ParseProfile(row.Profile); err != nil {
|
if err := user.ParseProfile(row.Profile); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
users = append(users, user)
|
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
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
|
func (r *userRepository) Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error) {
|
||||||
user, err := r.GetByID(ctx, id)
|
queryKey := r.generateQueryKey("user:search", params)
|
||||||
if err != nil {
|
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
err = r.q.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
|
||||||
ID: id,
|
|
||||||
TokenVersion: user.TokenVersion + 1,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.IsVerified = true
|
var users []*models.UserEntity
|
||||||
user.TokenVersion += 1
|
var ids []string
|
||||||
|
usersToCache := make(map[string]any)
|
||||||
|
|
||||||
mapCache := map[string]any{
|
for _, row := range rows {
|
||||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
user := &models.UserEntity{
|
||||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
users = append(users, user)
|
||||||
|
ids = append(ids, user.ID)
|
||||||
|
usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user
|
||||||
}
|
}
|
||||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
|
||||||
return nil
|
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 {
|
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
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = r.c.Set(ctx, cacheId, raw, 5*time.Minute)
|
_ = r.c.Set(ctx, cacheId, raw, constants.NormalCacheDuration)
|
||||||
return raw, nil
|
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))
|
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
|
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,
|
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
|
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,
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"history-api/internal/controllers"
|
"history-api/internal/controllers"
|
||||||
"history-api/internal/middlewares"
|
"history-api/internal/middlewares"
|
||||||
|
"history-api/internal/repositories"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"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 := app.Group("/auth")
|
||||||
route.Post("/signin", controller.Signin)
|
route.Post("/signin", controller.Signin)
|
||||||
route.Post("/signup", controller.Signup)
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
"history-api/internal/dtos/request"
|
"history-api/internal/dtos/request"
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
"history-api/internal/gen/sqlc"
|
"history-api/internal/gen/sqlc"
|
||||||
"history-api/internal/models"
|
"history-api/internal/models"
|
||||||
"history-api/internal/repositories"
|
"history-api/internal/repositories"
|
||||||
|
"history-api/pkg/cache"
|
||||||
"history-api/pkg/config"
|
"history-api/pkg/config"
|
||||||
"history-api/pkg/constant"
|
"history-api/pkg/constants"
|
||||||
|
"history-api/pkg/convert"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -22,29 +28,35 @@ import (
|
|||||||
type AuthService interface {
|
type AuthService interface {
|
||||||
Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error)
|
Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error)
|
||||||
Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error)
|
Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error)
|
||||||
ForgotPassword(ctx context.Context) error
|
ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error
|
||||||
VerifyToken(ctx context.Context) error
|
VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error)
|
||||||
CreateToken(ctx context.Context) error
|
CreateToken(ctx context.Context, dto *request.CreateTokenDto) error
|
||||||
SigninWith3rd(ctx context.Context) error
|
SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error
|
||||||
RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error)
|
RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type authService struct {
|
type authService struct {
|
||||||
userRepo repositories.UserRepository
|
userRepo repositories.UserRepository
|
||||||
roleRepo repositories.RoleRepository
|
roleRepo repositories.RoleRepository
|
||||||
|
tokenRepo repositories.TokenRepository
|
||||||
|
c cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(
|
func NewAuthService(
|
||||||
userRepo repositories.UserRepository,
|
userRepo repositories.UserRepository,
|
||||||
roleRepo repositories.RoleRepository,
|
roleRepo repositories.RoleRepository,
|
||||||
|
tokenRepo repositories.TokenRepository,
|
||||||
|
c cache.Cache,
|
||||||
) AuthService {
|
) AuthService {
|
||||||
return &authService{
|
return &authService{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
roleRepo: roleRepo,
|
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")
|
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT_SECRET in environment")
|
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{
|
claimsAccess := &response.JWTClaims{
|
||||||
UId: Uid,
|
UId: user.ID,
|
||||||
Roles: role,
|
Roles: models.RolesEntityToRoleConstant(user.Roles),
|
||||||
|
TokenVersion: user.TokenVersion,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(constants.AccessTokenDuration)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
claimsRefresh := &response.JWTClaims{
|
claimsRefresh := &response.JWTClaims{
|
||||||
UId: Uid,
|
UId: user.ID,
|
||||||
Roles: role,
|
Roles: models.RolesEntityToRoleConstant(user.Roles),
|
||||||
|
TokenVersion: user.TokenVersion,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
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) {
|
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")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := constant.ValidatePassword(dto.Password)
|
err := constants.ValidatePassword(dto.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
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!")
|
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 {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
|
||||||
}
|
}
|
||||||
var pgID pgtype.UUID
|
pgID, err := convert.StringToUUID(user.ID)
|
||||||
err = pgID.Scan(user.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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)
|
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!")
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "User is banned!")
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := a.genToken(id, roles)
|
data, err := a.genToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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) {
|
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")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||||
}
|
}
|
||||||
err := constant.ValidatePassword(dto.Password)
|
err := constants.ValidatePassword(dto.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
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)
|
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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 {
|
if user != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "User already exists")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "User already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost)
|
hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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),
|
String: string(hashed),
|
||||||
Valid: len(hashed) != 0,
|
Valid: len(hashed) != 0,
|
||||||
},
|
},
|
||||||
IsVerified: true,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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 {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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 {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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(
|
err = a.roleRepo.AddUserRole(
|
||||||
ctx,
|
ctx,
|
||||||
sqlc.AddUserRoleParams{
|
sqlc.AddUserRoleParams{
|
||||||
UserID: userId,
|
UserID: userId,
|
||||||
Name: constant.USER.String(),
|
Column2: []pgtype.UUID{roleId},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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 {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForgotPassword implements [AuthService].
|
func (a *authService) ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error {
|
||||||
func (a *authService) ForgotPassword(ctx context.Context) error {
|
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenPasswordReset, dto.TokenID)
|
||||||
panic("unimplemented")
|
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].
|
// SigninWith3rd implements [AuthService].
|
||||||
func (a *authService) SigninWith3rd(ctx context.Context) error {
|
func (a *authService) SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error {
|
||||||
panic("unimplemented")
|
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, dto *request.CreateTokenDto) error {
|
||||||
func (a *authService) CreateToken(ctx context.Context) error {
|
ok, err := a.tokenRepo.CheckCooldown(ctx, dto.Email, dto.TokenType)
|
||||||
panic("unimplemented")
|
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, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error) {
|
||||||
func (a *authService) VerifyToken(ctx context.Context) error {
|
token, err := a.tokenRepo.Get(ctx, dto.Email, dto.TokenType)
|
||||||
panic("unimplemented")
|
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"
|
"context"
|
||||||
"history-api/internal/dtos/request"
|
"history-api/internal/dtos/request"
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/gen/sqlc"
|
||||||
|
"history-api/internal/models"
|
||||||
"history-api/internal/repositories"
|
"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 {
|
type UserService interface {
|
||||||
//user
|
//user
|
||||||
GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error)
|
GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||||
UpdateProfile(ctx context.Context, id string) (*response.UserResponse, error)
|
UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error)
|
||||||
ChangePassword(ctx context.Context, id string) (*response.UserResponse, error)
|
ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error
|
||||||
|
|
||||||
//admin
|
//admin
|
||||||
DeleteUser(ctx context.Context, id string) (*response.UserResponse, error)
|
DeleteUser(ctx context.Context, userId string) error
|
||||||
ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error)
|
ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error)
|
||||||
RestoreUser(ctx context.Context, id string) (*response.UserResponse, error)
|
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||||
GetUserByID(ctx context.Context, id string) (*response.UserResponse, error)
|
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||||
Search(ctx context.Context, id string) ([]*response.UserResponse, error)
|
Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||||
GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type userService struct {
|
type userService struct {
|
||||||
@@ -37,47 +43,241 @@ func NewUserService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangePassword implements [UserService].
|
func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error {
|
||||||
func (u *userService) ChangePassword(ctx context.Context, id string) (*response.UserResponse, error) {
|
pgID, err := convert.StringToUUID(userId)
|
||||||
panic("unimplemented")
|
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, dto *request.ChangeRoleDto) (*response.UserResponse, error) {
|
||||||
func (u *userService) ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
userId, err := convert.StringToUUID(dto.UserID)
|
||||||
panic("unimplemented")
|
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, userId string) error {
|
||||||
func (u *userService) DeleteUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
pgID, err := convert.StringToUUID(userId)
|
||||||
panic("unimplemented")
|
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) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error) {
|
||||||
func (u *userService) GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
pgID, err := convert.StringToUUID(userId)
|
||||||
panic("unimplemented")
|
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) GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||||
func (u *userService) GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) {
|
pgID, err := convert.StringToUUID(userId)
|
||||||
panic("unimplemented")
|
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) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||||
func (u *userService) GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
pgID, err := convert.StringToUUID(userId)
|
||||||
panic("unimplemented")
|
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) Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||||
func (u *userService) RestoreUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
arg := sqlc.SearchUsersParams{
|
||||||
panic("unimplemented")
|
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) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||||
func (u *userService) Search(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
pgID, err := convert.StringToUUID(userId)
|
||||||
panic("unimplemented")
|
if err != nil {
|
||||||
}
|
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
// UpdateProfile implements [UserService].
|
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||||
func (u *userService) UpdateProfile(ctx context.Context, id string) (*response.UserResponse, error) {
|
if err != nil {
|
||||||
panic("unimplemented")
|
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||||
}
|
}
|
||||||
|
return user.ToResponse(), nil
|
||||||
|
}
|
||||||
83
pkg/cache/redis.go
vendored
83
pkg/cache/redis.go
vendored
@@ -5,18 +5,22 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"history-api/pkg/config"
|
"history-api/pkg/config"
|
||||||
|
"history-api/pkg/constants"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cache interface {
|
type Cache interface {
|
||||||
Set(ctx context.Context, key string, value any, ttl time.Duration) error
|
Set(ctx context.Context, key string, value any, ttl time.Duration) error
|
||||||
Get(ctx context.Context, key string, dest any) error
|
Get(ctx context.Context, key string, dest any) error
|
||||||
Del(ctx context.Context, keys ...string) error
|
Del(ctx context.Context, keys ...string) error
|
||||||
DelByPattern(ctx context.Context, pattern string) error
|
DelByPattern(ctx context.Context, pattern string) error
|
||||||
MGet(ctx context.Context, keys ...string) [][]byte
|
MGet(ctx context.Context, keys ...string) [][]byte
|
||||||
MSet(ctx context.Context, pairs map[string]any, ttl time.Duration) error
|
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 {
|
type RedisClient struct {
|
||||||
@@ -49,33 +53,45 @@ func NewRedisClient() (Cache, error) {
|
|||||||
return &RedisClient{client: rdb}, nil
|
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 {
|
func (r *RedisClient) Del(ctx context.Context, keys ...string) error {
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return r.client.Del(ctx, keys...).Err()
|
return r.client.Del(ctx, keys...).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RedisClient) DelByPattern(ctx context.Context, pattern string) error {
|
func (r *RedisClient) DelByPattern(ctx context.Context, pattern string) error {
|
||||||
var cursor uint64
|
var cursor uint64
|
||||||
for {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error scanning keys with pattern %s: %v", pattern, err)
|
return fmt.Errorf("error scanning keys with pattern %s: %v", pattern, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(keys) > 0 {
|
if len(keys) > 0 {
|
||||||
if err := r.client.Del(ctx, keys...).Err(); err != nil {
|
if err := r.client.Unlink(ctx, keys...).Err(); err != nil {
|
||||||
return fmt.Errorf("error deleting keys during scan: %v", err)
|
return fmt.Errorf("error unlinking keys during scan: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = nextCursor
|
cursor = nextCursor
|
||||||
if cursor == 0 {
|
if cursor == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RedisClient) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
|
func (r *RedisClient) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
|
||||||
@@ -121,6 +137,21 @@ func (r *RedisClient) MGet(ctx context.Context, keys ...string) [][]byte {
|
|||||||
return results
|
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) {
|
func GetMultiple[T any](ctx context.Context, c Cache, keys []string) ([]T, error) {
|
||||||
raws := c.MGet(ctx, keys...)
|
raws := c.MGet(ctx, keys...)
|
||||||
final := make([]T, 0)
|
final := make([]T, 0)
|
||||||
|
|||||||
@@ -34,3 +34,11 @@ func GetConfig(config string) (string, error) {
|
|||||||
|
|
||||||
return data, nil
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package constant
|
package constants
|
||||||
|
|
||||||
type Role string
|
type Role string
|
||||||
|
|
||||||
@@ -18,10 +18,15 @@ func (r Role) Compare(other Role) bool {
|
|||||||
return r == other
|
return r == other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r Role) IsValid() bool {
|
||||||
|
return CheckValidRole(r)
|
||||||
|
}
|
||||||
|
|
||||||
func CheckValidRole(r Role) bool {
|
func CheckValidRole(r Role) bool {
|
||||||
return r == ADMIN || r == MOD || r == HISTORIAN || r == USER || r == BANNED
|
return r == ADMIN || r == MOD || r == HISTORIAN || r == USER || r == BANNED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func ParseRole(s string) (Role, bool) {
|
func ParseRole(s string) (Role, bool) {
|
||||||
r := Role(s)
|
r := Role(s)
|
||||||
if CheckValidRole(r) {
|
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
|
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
|
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 {
|
func ParseTokenType(v int16) TokenType {
|
||||||
switch v {
|
switch v {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -37,4 +41,19 @@ func ParseTokenType(v int16) TokenType {
|
|||||||
default:
|
default:
|
||||||
return 0
|
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
|
type VerifyType int16
|
||||||
|
|
||||||
@@ -13,6 +13,15 @@ func UUIDToString(v pgtype.UUID) string {
|
|||||||
return ""
|
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 {
|
func TextToString(v pgtype.Text) string {
|
||||||
if v.Valid {
|
if v.Valid {
|
||||||
return v.String
|
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