refactor: migrate to clean architecture with trait-based DI (v0.2.0)
Complete architectural overhaul across all 12 crates: - Replace validator crate with zod-rs for all DTO validation - Replace manual pagination with paginator-rs/paginator-sea-orm - Migrate all modules (iam, cms, gacha, dimentorin) to clean architecture: domain → application → infrastructure layers - Introduce trait-based DI (Arc<dyn Trait>) at every layer for repositories and services - Delete all v1/ legacy SurrealDB-era code across every crate - Replace opaque response helpers with typed IntoResponse structs (ApiSuccess, ApiCreated, ApiPaginated, ApiMessage) - Remove dual_mode_repository, migration_validation_errors, validator.rs dead code - Zero cargo clippy warnings; release build clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1b3366d735
commit
e432a1a743
@@ -1,28 +1,3 @@
|
||||
# RUST_ENV : Mode environment aplikasi (misal: development, production)
|
||||
# RUST_LOG : Level logging aplikasi Rust (misal: debug, info, warn, error)
|
||||
# PORT : Port yang digunakan backend service
|
||||
# SURREALDB_URL : URL koneksi ke SurrealDB (gunakan ws:// untuk WebSocket)
|
||||
# SURREALDB_USERNAME : Username untuk SurrealDB
|
||||
# SURREALDB_PASSWORD : Password untuk SurrealDB
|
||||
# SURREALDB_NAMESPACE : Namespace SurrealDB yang digunakan
|
||||
# SURREALDB_DBNAME : Nama database SurrealDB
|
||||
# ACCESS_TOKEN_SECRET : Secret key untuk JWT access token (ganti dengan nilai rahasia)
|
||||
# REFRESH_TOKEN_SECRET : Secret key untuk JWT refresh token (ganti dengan nilai rahasia)
|
||||
# SMTP_EMAIL : Email yang digunakan untuk mengirim email (SMTP)
|
||||
# SMTP_PASSWORD : Password email SMTP
|
||||
# SMTP_NAME : Nama pengirim email (misal: nama aplikasi)
|
||||
# SMTP_HOST : Host SMTP server (misal: smtp.gmail.com)
|
||||
# REDISDB_URL : URL/host Redis database
|
||||
# FE_URL : URL frontend aplikasi (misal: http://localhost)
|
||||
# MINIO_ENDPOINT : Endpoint MinIO object storage
|
||||
# MINIO_BUCKET_NAME : Nama bucket default MinIO
|
||||
# MINIO_ACCESS_KEY : Access key MinIO atau MINIO_ROOT_USER
|
||||
# MINIO_SECRET_KEY : Secret key MinIO atau MINIO_ROOT_PASSWORD
|
||||
# MINIO_ROOT_USER : Username root MinIO (default: minioadmin)
|
||||
# MINIO_ROOT_PASSWORD : Password root MinIO (default: minioadmin)
|
||||
# GOOGLE_CLIENT_ID : Client ID OAuth Google (untuk login Google)
|
||||
# GOOGLE_CLIENT_SECRET : Client Secret OAuth Google
|
||||
|
||||
RUST_ENV=development
|
||||
RUST_LOG=debug
|
||||
PORT=4099
|
||||
|
||||
Generated
+128
-124
@@ -626,41 +626,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -1451,12 +1416,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -1480,7 +1439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-backend"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1508,14 +1467,14 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-cms"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-test",
|
||||
"chrono",
|
||||
@@ -1525,6 +1484,10 @@ dependencies = [
|
||||
"imphnen-utils",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"paginator-axum",
|
||||
"paginator-rs",
|
||||
"paginator-sea-orm",
|
||||
"paginator-utils",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"sea-orm",
|
||||
@@ -1536,12 +1499,13 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
"zod-rs",
|
||||
"zod-rs-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-dimentorin"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1556,6 +1520,10 @@ dependencies = [
|
||||
"imphnen-middleware",
|
||||
"imphnen-utils",
|
||||
"lazy_static",
|
||||
"paginator-axum",
|
||||
"paginator-rs",
|
||||
"paginator-sea-orm",
|
||||
"paginator-utils",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"sea-orm",
|
||||
@@ -1567,12 +1535,13 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
"zod-rs",
|
||||
"zod-rs-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-entities"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1590,9 +1559,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-gacha"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-test",
|
||||
"chrono",
|
||||
@@ -1602,6 +1572,10 @@ dependencies = [
|
||||
"imphnen-utils",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"paginator-axum",
|
||||
"paginator-rs",
|
||||
"paginator-sea-orm",
|
||||
"paginator-utils",
|
||||
"rand 0.9.2",
|
||||
"rand_distr",
|
||||
"regex",
|
||||
@@ -1614,12 +1588,13 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
"zod-rs",
|
||||
"zod-rs-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-gateway"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1642,12 +1617,11 @@ dependencies = [
|
||||
"tower-http",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-iam"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1665,6 +1639,10 @@ dependencies = [
|
||||
"mockall",
|
||||
"oauth2",
|
||||
"once_cell",
|
||||
"paginator-axum",
|
||||
"paginator-rs",
|
||||
"paginator-sea-orm",
|
||||
"paginator-utils",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"reqwest",
|
||||
@@ -1680,12 +1658,13 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
"zod-rs",
|
||||
"zod-rs-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-libs"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -1714,12 +1693,12 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"validator",
|
||||
"zod-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1728,7 +1707,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-middleware"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1753,12 +1732,11 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-utils"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1768,6 +1746,10 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"imphnen-entities",
|
||||
"imphnen-libs",
|
||||
"paginator-axum",
|
||||
"paginator-rs",
|
||||
"paginator-sea-orm",
|
||||
"paginator-utils",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"reqwest",
|
||||
@@ -1780,7 +1762,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"validator",
|
||||
"zod-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2331,6 +2313,57 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paginator-axum"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "599c5c103c2e05968133b94fe4e50eadd32122ec5de67d23cc4560b92d14ad89"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"http",
|
||||
"paginator-rs",
|
||||
"paginator-utils",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paginator-rs"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fdee525f1aa47af3330e699c14044c317454207668c80c9aeb8207d2c662a6c"
|
||||
dependencies = [
|
||||
"paginator-utils",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paginator-sea-orm"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c7d707483ea0b638e24da8e8f2b4fd077bdc88dbd5832b534eee1d8e66f6974"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"paginator-rs",
|
||||
"paginator-utils",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paginator-utils"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1e7ca0ba9d0ec274d2dcf4aa95420bd07d5eaed5c5c20258e26fd59aa73bad2"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -3623,12 +3656,6 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
@@ -3750,33 +3777,6 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "tests"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"imphnen-dimentorin",
|
||||
"imphnen-entities",
|
||||
"imphnen-iam",
|
||||
"imphnen-libs",
|
||||
"imphnen-utils",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.2",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"yoke",
|
||||
"yoke-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -4248,36 +4248,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"once_cell",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -4908,6 +4878,40 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
|
||||
|
||||
[[package]]
|
||||
name = "zod-rs"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b3d6798b23cf9be839aa5dc002b72ae21b52c0a112920b4397252eb152ce10b"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"zod-rs-macros",
|
||||
"zod-rs-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zod-rs-macros"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa0aeb5fc2bd71f115da362f9173505fad648bd8ec57a1ce583049a6b0acf46b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zod-rs-util"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86ea855d76c2e2839eb794cc70c4e772c575ff997c00b6daea503d42cae44adb"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
|
||||
+6
-3
@@ -1,7 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"tests",
|
||||
"imphnen-entities", # Most basic - core data structures
|
||||
"imphnen-macros", # Macros
|
||||
"imphnen-libs", # Depends on entities
|
||||
@@ -37,7 +36,12 @@ rand = { version = "0.9.2", features = ["std", "alloc"] }
|
||||
rand_distr = "0.5.1"
|
||||
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
|
||||
http-body-util = "0.1.3"
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
zod-rs = { version = "0.4", features = ["macros"] }
|
||||
zod-rs-util = "0.4"
|
||||
paginator-rs = "0.2"
|
||||
paginator-utils = "0.2"
|
||||
paginator-sea-orm = { version = "0.2", features = ["sqlx-postgres", "runtime-tokio"] }
|
||||
paginator-axum = "0.2"
|
||||
lazy_static = "1.5.0"
|
||||
regex = "1.11.1"
|
||||
axum-test = "17.3.0"
|
||||
@@ -71,7 +75,6 @@ tokio-tungstenite = "0.27"
|
||||
url = "2.5"
|
||||
futures-util = "0.3"
|
||||
http = "1.3"
|
||||
tests = { path = "./tests" }
|
||||
imphnen-iam = { path = "./imphnen-iam" }
|
||||
imphnen-cms = { path = "./imphnen-cms" }
|
||||
imphnen-libs = { path = "./imphnen-libs" }
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
:: Cek apakah file .env ada
|
||||
if not exist ".env" (
|
||||
echo File .env tidak ditemukan di direktori saat ini.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Memuat variabel dari .env...
|
||||
|
||||
:: Baca file .env baris per baris
|
||||
for /f "tokens=*" %%a in ('type ".env" ^| findstr /v "^$" ^| findstr /v "^#"') do (
|
||||
echo.%%a | findstr "=" >nul && (
|
||||
for /f "tokens=1,2 delims==" %%b in ("%%a") do (
|
||||
set "key=%%b"
|
||||
set "value=%%c"
|
||||
:: Trim whitespace
|
||||
call :trimValue key value
|
||||
echo Set variabel: %%b=%%c
|
||||
setx %%b %%c >nul
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Semua variabel telah dimuat.
|
||||
endlocal
|
||||
goto :eof
|
||||
|
||||
:: Fungsi trim (sederhana)
|
||||
:trimValue
|
||||
set "%1=%[%1]%"
|
||||
set "%2=%[%2]%"
|
||||
goto :eof
|
||||
@@ -1,30 +0,0 @@
|
||||
function Set-TempEnvFromDotEnv {
|
||||
param (
|
||||
[string]$envFilePath
|
||||
)
|
||||
|
||||
if (-Not (Test-Path $envFilePath)) {
|
||||
Write-Error "The .env file at path '$envFilePath' does not exist."
|
||||
return
|
||||
}
|
||||
|
||||
$envContent = Get-Content $envFilePath
|
||||
|
||||
foreach ($line in $envContent) {
|
||||
$trimmedLine = $line.Trim()
|
||||
|
||||
if (-Not [string]::IsNullOrWhiteSpace($trimmedLine) -and -Not $trimmedLine.StartsWith("#")) {
|
||||
$keyValue = $trimmedLine -split "=", 2
|
||||
if ($keyValue.Length -eq 2) {
|
||||
$key = $keyValue[0].Trim()
|
||||
$value = $keyValue[1].Trim()
|
||||
[System.Environment]::SetEnvironmentVariable($key, $value, [System.EnvironmentVariableTarget]::Process)
|
||||
Write-Host "Set temporary environment variable: $key=$value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "All environment variables from '$envFilePath' have been set temporarily."
|
||||
}
|
||||
|
||||
Set-TempEnvFromDotEnv -envFilePath ".env"
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set_temp_env_from_dotenv() {
|
||||
local env_file_path="$1"
|
||||
|
||||
if [[ ! -f "$env_file_path" ]]; then
|
||||
echo "Error: The .env file at path '$env_file_path' does not exist."
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
trimmed_line=$(echo "$line" | xargs)
|
||||
|
||||
if [[ -n "$trimmed_line" && ! "$trimmed_line" =~ ^# ]]; then
|
||||
key=$(echo "$trimmed_line" | cut -d '=' -f 1 | xargs)
|
||||
value=$(echo "$trimmed_line" | cut -d '=' -f 2- | xargs)
|
||||
export "$key=$value"
|
||||
echo "Set temporary environment variable: $key=$value"
|
||||
fi
|
||||
done < "$env_file_path"
|
||||
|
||||
echo "All environment variables from '$env_file_path' have been set temporarily."
|
||||
}
|
||||
|
||||
set_temp_env_from_dotenv ".env"
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script deploy semua binary hasil build Rust (untuk Linux)
|
||||
|
||||
set -e
|
||||
|
||||
REMOTE_USER="asephs"
|
||||
REMOTE_HOST="70.153.9.42"
|
||||
REMOTE_PATH="/home/asephs/imphnen-backend-service"
|
||||
|
||||
# Build release binary
|
||||
echo "🔧 Building Rust project..."
|
||||
cargo build --release
|
||||
|
||||
# Filter hanya file executable tanpa ekstensi .exe, .rlib, atau .d
|
||||
BINARIES=$(find target/release -maxdepth 1 -type f ! -name "*.exe" ! -name "*.rlib" ! -name "*.d")
|
||||
|
||||
if [ -z "$BINARIES" ]; then
|
||||
echo "❌ Tidak ada binary Linux (.exe/.rlib/.d diabaikan)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Upload semua binary yang valid ke server
|
||||
echo "🚀 Mengirim binary ke server..."
|
||||
rsync -avz --compress-level=9 --progress \
|
||||
$BINARIES \
|
||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
|
||||
|
||||
# Jalankan ulang service utama di VPS
|
||||
echo "♻️ Restart service utama di server..."
|
||||
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||
set -e
|
||||
cd /home/asephs/imphnen-backend-service
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
if [ -f ~/.bashrc ]; then
|
||||
source ~/.bashrc
|
||||
fi
|
||||
|
||||
# Pastikan semua binary bisa dieksekusi
|
||||
chmod +x target/release/*
|
||||
|
||||
# Restart service utama (misalnya api.d)
|
||||
if pm2 list | grep -q api; then
|
||||
pm2 restart api --update-env
|
||||
else
|
||||
pm2 start target/release/api.d --name api
|
||||
fi
|
||||
EOF
|
||||
|
||||
echo "✅ Deploy semua binary Linux selesai ke $REMOTE_HOST:$REMOTE_PATH"
|
||||
+32
-31
@@ -1,42 +1,43 @@
|
||||
{pkgs, ...}: let
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
baseImage = pkgs.ociTools.pullImage {
|
||||
imageName = "ubuntu";
|
||||
tag = "latest";
|
||||
};
|
||||
in
|
||||
pkgs.dockerTools.buildImage {
|
||||
name = "imphnen-cms-api";
|
||||
pkgs.dockerTools.buildImage {
|
||||
name = "imphnen-backend-service";
|
||||
|
||||
fromImage = baseImage;
|
||||
fromImage = baseImage;
|
||||
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "imphnen-cms-api";
|
||||
paths = [
|
||||
(pkgs.stdenv.mkDerivation {
|
||||
name = "imphnen-cms-api";
|
||||
src = ./src;
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "imphnen-backend-service";
|
||||
paths = [
|
||||
(pkgs.stdenv.mkDerivation {
|
||||
name = "imphnen-backend-service";
|
||||
src = ./src;
|
||||
|
||||
buildInputs = [
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.openssl
|
||||
pkgs.pkg-config
|
||||
];
|
||||
buildInputs = [
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.openssl
|
||||
pkgs.pkg-config
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
cargo build --release
|
||||
'';
|
||||
buildPhase = ''
|
||||
cargo build --release
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/najm-course-api $out/bin/
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/imphnen-backend-service $out/bin/
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
config = {
|
||||
Cmd = ["/bin/imphnen-cms-api"];
|
||||
WorkingDir = "/bin";
|
||||
};
|
||||
}
|
||||
config = {
|
||||
Cmd = [ "/bin/imphnen-backend-service" ];
|
||||
WorkingDir = "/bin";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,28 +5,37 @@
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
}: let
|
||||
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
||||
pkgsFor = system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
}:
|
||||
let
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
];
|
||||
pkgsFor =
|
||||
system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in {
|
||||
packages = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./default.nix {};
|
||||
});
|
||||
devShells = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./shell.nix {};
|
||||
});
|
||||
dockerImages = forAllSystems (system: {
|
||||
tryOutApi = (pkgsFor system).callPackage ./docker.nix {};
|
||||
});
|
||||
};
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./default.nix { };
|
||||
});
|
||||
devShells = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./shell.nix { };
|
||||
});
|
||||
dockerImages = forAllSystems (system: {
|
||||
tryOutApi = (pkgsFor system).callPackage ./docker.nix { };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imphnen-backend"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
@@ -63,7 +63,6 @@ serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
axum-test.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
+38
-31
@@ -1,31 +1,38 @@
|
||||
[package]
|
||||
name = "imphnen-cms"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
axum-test.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
VALID_URL_REGEX = "^https?://"
|
||||
[package]
|
||||
name = "imphnen-cms"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
zod-rs.workspace = true
|
||||
zod-rs-util.workspace = true
|
||||
axum-test.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
paginator-rs.workspace = true
|
||||
paginator-utils.workspace = true
|
||||
paginator-sea-orm.workspace = true
|
||||
paginator-axum.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
VALID_URL_REGEX = "^https?://"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use crate::events::domain::{EventEntity, EventRepository, EventService};
|
||||
|
||||
pub struct EventServiceImpl {
|
||||
repo: Arc<dyn EventRepository>,
|
||||
}
|
||||
|
||||
impl EventServiceImpl {
|
||||
pub fn new(repo: Arc<dyn EventRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventService for EventServiceImpl {
|
||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||
self.repo.find_all(params).await
|
||||
}
|
||||
|
||||
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||
self.repo.find_by_id(id).await
|
||||
}
|
||||
|
||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||
self.repo.create(entity).await
|
||||
}
|
||||
|
||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||
self.repo.update(entity).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.repo.delete(id).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod event_service;
|
||||
|
||||
pub use event_service::EventServiceImpl;
|
||||
@@ -0,0 +1,18 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EventEntity {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub location: Option<String>,
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod event;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
|
||||
pub use event::EventEntity;
|
||||
pub use repository::EventRepository;
|
||||
pub use service::EventService;
|
||||
@@ -0,0 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::event::EventEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventRepository: Send + Sync {
|
||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::event::EventEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventService: Send + Sync {
|
||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use crate::events::domain::event::EventEntity;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsCreateRequestDto {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub location: Option<String>,
|
||||
pub is_online: bool,
|
||||
}
|
||||
|
||||
impl ZodValidate for EventsCreateRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventsCreateRequestDto> for EventEntity {
|
||||
fn from(dto: EventsCreateRequestDto) -> Self {
|
||||
EventEntity {
|
||||
id: Uuid::new_v4(),
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
detail_link: dto.detail_link,
|
||||
price: dto.price,
|
||||
is_online: dto.is_online,
|
||||
is_deleted: false,
|
||||
location: dto.location,
|
||||
start_date: dto.start_date,
|
||||
end_date: dto.end_date,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsUpdateRequestDto {
|
||||
pub name: String,
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for EventsUpdateRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsListItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub location: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
impl From<EventEntity> for EventsListItemDto {
|
||||
fn from(e: EventEntity) -> Self {
|
||||
EventsListItemDto {
|
||||
id: e.id.to_string(),
|
||||
name: e.name,
|
||||
description: e.description,
|
||||
detail_link: e.detail_link,
|
||||
price: e.price,
|
||||
is_online: e.is_online,
|
||||
start_date: e.start_date.to_rfc3339(),
|
||||
end_date: e.end_date.to_rfc3339(),
|
||||
created_at: e.created_at.to_rfc3339(),
|
||||
location: e.location,
|
||||
is_deleted: e.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsDetailItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl From<EventEntity> for EventsDetailItemDto {
|
||||
fn from(e: EventEntity) -> Self {
|
||||
EventsDetailItemDto {
|
||||
id: e.id.to_string(),
|
||||
name: e.name,
|
||||
description: e.description,
|
||||
detail_link: e.detail_link,
|
||||
price: e.price,
|
||||
is_online: e.is_online,
|
||||
start_date: e.start_date.to_rfc3339(),
|
||||
end_date: e.end_date.to_rfc3339(),
|
||||
created_at: e.created_at.to_rfc3339(),
|
||||
updated_at: e.updated_at.to_rfc3339(),
|
||||
location: e.location,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{Extension, extract::Path, http::HeaderMap, response::{IntoResponse, Response}};
|
||||
use paginator_axum::PaginationQuery;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_utils::{ApiSuccess, ApiPaginated, ApiMessage};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::AppError;
|
||||
use super::dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsUpdateRequestDto};
|
||||
use crate::events::domain::EventService;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event list")
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_list(
|
||||
Extension(service): Extension<Arc<dyn EventService>>,
|
||||
PaginationQuery(params): PaginationQuery,
|
||||
) -> Response {
|
||||
match service.list(params).await {
|
||||
Ok(result) => {
|
||||
let mapped = PaginatorResponse {
|
||||
data: result.data.into_iter().map(EventsListItemDto::from).collect::<Vec<_>>(),
|
||||
meta: result.meta,
|
||||
};
|
||||
ApiPaginated(mapped).into_response()
|
||||
}
|
||||
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_by_id(
|
||||
Extension(service): Extension<Arc<dyn EventService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
let uuid = match Uuid::parse_str(&id) {
|
||||
Ok(u) => u,
|
||||
Err(e) => return ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, format!("Invalid UUID: {e}")).into_response(),
|
||||
};
|
||||
match service.get(uuid).await {
|
||||
Ok(event) => ApiSuccess(EventsDetailItemDto::from(event)).into_response(),
|
||||
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/events/create",
|
||||
request_body = EventsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[ADMIN] Create new event")
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn post_create_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn EventService>>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
let entity = payload.into();
|
||||
service.create(entity).await?;
|
||||
Ok(ApiMessage::created("Event created"))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/events/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
request_body = EventsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Update event")
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn patch_update_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn EventService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
let uuid = Uuid::parse_str(&id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||
let existing = service.get(uuid).await?;
|
||||
let entity = crate::events::domain::EventEntity {
|
||||
id: existing.id,
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
price: payload.price,
|
||||
is_online: payload.is_online,
|
||||
location: payload.location,
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
is_deleted: existing.is_deleted,
|
||||
created_at: existing.created_at,
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
service.update(entity).await?;
|
||||
Ok(ApiMessage::ok("Event updated"))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/events/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Soft delete event")
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn delete_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn EventService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
let uuid = Uuid::parse_str(&id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||
service.delete(uuid).await?;
|
||||
Ok(ApiMessage::ok("Event deleted"))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::{events_public_routes, events_protected_routes};
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{Router, routing::{delete, get, patch, post}, Extension};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use crate::events::application::EventServiceImpl;
|
||||
use crate::events::domain::EventService;
|
||||
use crate::events::infrastructure::persistence::PostgresEventRepository;
|
||||
use super::handlers::{
|
||||
delete_event, get_event_by_id, get_event_list, patch_update_event, post_create_event,
|
||||
};
|
||||
|
||||
fn build_service(db: DatabaseConnection) -> Arc<dyn EventService> {
|
||||
let repo = Arc::new(PostgresEventRepository::new(db));
|
||||
Arc::new(EventServiceImpl::new(repo))
|
||||
}
|
||||
|
||||
pub fn events_public_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/cms/landing/events", get(get_event_list))
|
||||
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn events_protected_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/cms/landing/events/create", post(post_create_event))
|
||||
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod postgres_event_repository;
|
||||
|
||||
pub use postgres_event_repository::PostgresEventRepository;
|
||||
@@ -0,0 +1,149 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, Order, QueryOrder, PaginatorTrait};
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_entities::seaorm::common::events::{
|
||||
Entity as EventsEntity, Column as EventsColumn,
|
||||
ActiveModel as EventsActiveModel, Model as EventsModel,
|
||||
};
|
||||
use crate::events::domain::{event::EventEntity, repository::EventRepository};
|
||||
|
||||
fn to_entity(model: EventsModel) -> EventEntity {
|
||||
EventEntity {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
description: model.description,
|
||||
detail_link: model.detail_link,
|
||||
price: model.price,
|
||||
is_online: model.is_online,
|
||||
is_deleted: model.is_deleted,
|
||||
location: model.location,
|
||||
start_date: model.start_date,
|
||||
end_date: model.end_date,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresEventRepository {
|
||||
db: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl PostgresEventRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventRepository for PostgresEventRepository {
|
||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let mut query = EventsEntity::find()
|
||||
.filter(EventsColumn::IsDeleted.eq(false));
|
||||
|
||||
if let Some(ref search) = params.search {
|
||||
query = query.filter(EventsColumn::Name.contains(&search.query));
|
||||
}
|
||||
|
||||
query = match params.sort_by.as_deref() {
|
||||
Some("name") => match params.sort_direction {
|
||||
Some(SortDirection::Desc) => query.order_by(EventsColumn::Name, Order::Desc),
|
||||
_ => query.order_by(EventsColumn::Name, Order::Asc),
|
||||
},
|
||||
_ => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by(EventsColumn::CreatedAt, Order::Asc),
|
||||
_ => query.order_by(EventsColumn::CreatedAt, Order::Desc),
|
||||
},
|
||||
};
|
||||
|
||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||
let total = paginator.num_items().await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let events = paginator.fetch_page((page - 1) as u64).await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let data = events.into_iter().map(to_entity).collect();
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||
let event = EventsEntity::find_by_id(id)
|
||||
.filter(EventsColumn::IsDeleted.eq(false))
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?;
|
||||
|
||||
Ok(to_entity(event))
|
||||
}
|
||||
|
||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||
let active_model = EventsActiveModel {
|
||||
id: ActiveValue::Set(entity.id),
|
||||
name: ActiveValue::Set(entity.name),
|
||||
description: ActiveValue::Set(entity.description),
|
||||
detail_link: ActiveValue::Set(entity.detail_link),
|
||||
price: ActiveValue::Set(entity.price),
|
||||
is_online: ActiveValue::Set(entity.is_online),
|
||||
is_deleted: ActiveValue::Set(false),
|
||||
location: ActiveValue::Set(entity.location),
|
||||
start_date: ActiveValue::Set(entity.start_date),
|
||||
end_date: ActiveValue::Set(entity.end_date),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
EventsEntity::insert(active_model)
|
||||
.exec(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(entity.id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||
.into();
|
||||
|
||||
active_model.name = ActiveValue::Set(entity.name);
|
||||
active_model.description = ActiveValue::Set(entity.description);
|
||||
active_model.detail_link = ActiveValue::Set(entity.detail_link);
|
||||
active_model.price = ActiveValue::Set(entity.price);
|
||||
active_model.is_online = ActiveValue::Set(entity.is_online);
|
||||
active_model.location = ActiveValue::Set(entity.location);
|
||||
active_model.start_date = ActiveValue::Set(entity.start_date);
|
||||
active_model.end_date = ActiveValue::Set(entity.end_date);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(self.db.as_ref()).await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||
.into();
|
||||
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
active_model.update(self.db.as_ref()).await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub use infrastructure::http::{events_public_routes, events_protected_routes};
|
||||
@@ -1,9 +1,5 @@
|
||||
pub mod v1;
|
||||
|
||||
pub use v1::landing;
|
||||
pub use v1::landing::events;
|
||||
pub use v1::landing::testimonials;
|
||||
pub use v1::landing::events::events_public_routes;
|
||||
pub use v1::landing::events::events_protected_routes;
|
||||
pub use v1::landing::testimonials::testimonials_public_routes;
|
||||
pub use v1::landing::testimonials::testimonials_protected_routes;
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
|
||||
pub use events::{events_public_routes, events_protected_routes};
|
||||
pub use testimonials::{testimonials_public_routes, testimonials_protected_routes};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod testimonial_service;
|
||||
|
||||
pub use testimonial_service::TestimonialServiceImpl;
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use crate::testimonials::domain::{TestimonialEntity, TestimonialRepository, TestimonialService};
|
||||
|
||||
pub struct TestimonialServiceImpl {
|
||||
repo: Arc<dyn TestimonialRepository>,
|
||||
}
|
||||
|
||||
impl TestimonialServiceImpl {
|
||||
pub fn new(repo: Arc<dyn TestimonialRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TestimonialService for TestimonialServiceImpl {
|
||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
||||
self.repo.find_all(params).await
|
||||
}
|
||||
|
||||
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
||||
self.repo.find_by_id(id).await
|
||||
}
|
||||
|
||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError> {
|
||||
self.repo.create(entity).await
|
||||
}
|
||||
|
||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
||||
self.repo.update(entity).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.repo.delete(id).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod testimonial;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
|
||||
pub use testimonial::TestimonialEntity;
|
||||
pub use repository::TestimonialRepository;
|
||||
pub use service::TestimonialService;
|
||||
@@ -0,0 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::testimonial::TestimonialEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait TestimonialRepository: Send + Sync {
|
||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError>;
|
||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::testimonial::TestimonialEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait TestimonialService: Send + Sync {
|
||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
||||
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError>;
|
||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestimonialEntity {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
use crate::testimonials::domain::testimonial::TestimonialEntity;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct TestimonialsCreateRequestDto {
|
||||
#[zod(min_length(1), max_length(100))]
|
||||
pub role: String,
|
||||
#[zod(min_length(1), max_length(1000))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for TestimonialsCreateRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct TestimonialsUpdateRequestDto {
|
||||
#[zod(min_length(1), max_length(100))]
|
||||
pub role: String,
|
||||
#[zod(min_length(1), max_length(1000))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for TestimonialsUpdateRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsListItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
impl From<TestimonialEntity> for TestimonialsListItemDto {
|
||||
fn from(e: TestimonialEntity) -> Self {
|
||||
TestimonialsListItemDto {
|
||||
id: e.id.to_string(),
|
||||
user_id: e.user_id.to_string(),
|
||||
user_fullname: e.user_fullname,
|
||||
role: e.role,
|
||||
content: e.content,
|
||||
created_at: e.created_at,
|
||||
is_deleted: e.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsDetailItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<TestimonialEntity> for TestimonialsDetailItemDto {
|
||||
fn from(e: TestimonialEntity) -> Self {
|
||||
TestimonialsDetailItemDto {
|
||||
id: e.id.to_string(),
|
||||
user_id: e.user_id.to_string(),
|
||||
user_fullname: e.user_fullname,
|
||||
role: e.role,
|
||||
content: e.content,
|
||||
created_at: e.created_at,
|
||||
updated_at: e.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{Extension, extract::Path, http::HeaderMap, http::StatusCode, response::{IntoResponse, Response}};
|
||||
use paginator_axum::PaginationQuery;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_utils::{ApiSuccess, ApiCreated, ApiPaginated, ApiMessage, extract_email};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_iam::require_auth;
|
||||
use imphnen_utils::AppError;
|
||||
use super::dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
};
|
||||
use crate::testimonials::domain::{TestimonialEntity, TestimonialService};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial list")
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_list(
|
||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||
PaginationQuery(params): PaginationQuery,
|
||||
) -> Response {
|
||||
match service.list(params).await {
|
||||
Ok(result) => {
|
||||
let mapped = PaginatorResponse {
|
||||
data: result.data.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(TestimonialsListItemDto::from)
|
||||
.collect::<Vec<_>>(),
|
||||
meta: result.meta,
|
||||
};
|
||||
ApiPaginated(mapped).into_response()
|
||||
}
|
||||
Err(e) => ApiMessage::new(StatusCode::BAD_REQUEST, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_by_id(
|
||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
let uuid = match Uuid::parse_str(&id) {
|
||||
Ok(u) => u,
|
||||
Err(e) => return ApiMessage::new(StatusCode::BAD_REQUEST, format!("Invalid UUID: {e}")).into_response(),
|
||||
};
|
||||
match service.get(uuid).await {
|
||||
Ok(t) if !t.is_deleted => {
|
||||
ApiSuccess(TestimonialsDetailItemDto::from(t)).into_response()
|
||||
}
|
||||
Ok(_) => ApiMessage::new(StatusCode::NOT_FOUND, "Testimonial not found").into_response(),
|
||||
Err(e) => ApiMessage::new(StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/testimonials/create",
|
||||
request_body = TestimonialsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[USER] Create new testimonial", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn post_create_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_auth!(headers.clone(), state, {
|
||||
let email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let user_info = state.user_lookup_service.get_user_by_email(&email, &state).await
|
||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||
let user = user_info.basic_info;
|
||||
let user_id = Uuid::parse_str(&user.id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {e}")))?;
|
||||
let entity = TestimonialEntity {
|
||||
id: Uuid::new_v4(),
|
||||
user_id,
|
||||
user_fullname: user.fullname.clone(),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
is_deleted: false,
|
||||
created_at: chrono::Utc::now().to_rfc3339(),
|
||||
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||
};
|
||||
let created = service.create(entity).await?;
|
||||
Ok(ApiCreated(TestimonialsDetailItemDto::from(created)))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/testimonials/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
request_body = TestimonialsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[USER] Update testimonial")
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn patch_update_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_auth!(headers, state, {
|
||||
let uuid = Uuid::parse_str(&id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||
let existing = service.get(uuid).await?;
|
||||
let entity = TestimonialEntity {
|
||||
id: existing.id,
|
||||
user_id: existing.user_id,
|
||||
user_fullname: existing.user_fullname,
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
is_deleted: existing.is_deleted,
|
||||
created_at: existing.created_at,
|
||||
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||
};
|
||||
service.update(entity).await?;
|
||||
Ok(ApiMessage::ok("Testimonial updated"))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/testimonials/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[USER] Soft delete testimonial")
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn delete_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_auth!(headers, state, {
|
||||
let uuid = Uuid::parse_str(&id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||
service.delete(uuid).await?;
|
||||
Ok(ApiMessage::ok("Testimonial deleted"))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::{testimonials_public_routes, testimonials_protected_routes};
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{Router, routing::{delete, get, patch, post}, Extension};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use crate::testimonials::application::TestimonialServiceImpl;
|
||||
use crate::testimonials::domain::TestimonialService;
|
||||
use crate::testimonials::infrastructure::persistence::PostgresTestimonialRepository;
|
||||
use super::handlers::{
|
||||
delete_testimonial, get_testimonial_by_id, get_testimonial_list,
|
||||
patch_update_testimonial, post_create_testimonial,
|
||||
};
|
||||
|
||||
fn build_service(db: DatabaseConnection) -> Arc<dyn TestimonialService> {
|
||||
let repo = Arc::new(PostgresTestimonialRepository::new(db));
|
||||
Arc::new(TestimonialServiceImpl::new(repo))
|
||||
}
|
||||
|
||||
pub fn testimonials_public_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
||||
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn testimonials_protected_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
||||
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
||||
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod postgres_testimonial_repository;
|
||||
|
||||
pub use postgres_testimonial_repository::PostgresTestimonialRepository;
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, QueryOrder, PaginatorTrait};
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_entities::seaorm::common::testimonials::{
|
||||
Entity as TestimonialsEntity, Column as TestimonialsColumn, ActiveModel as TestimonialsActiveModel,
|
||||
};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as UsersEntity;
|
||||
use crate::testimonials::domain::{testimonial::TestimonialEntity, repository::TestimonialRepository};
|
||||
|
||||
pub struct PostgresTestimonialRepository {
|
||||
db: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl PostgresTestimonialRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TestimonialRepository for PostgresTestimonialRepository {
|
||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let mut query = TestimonialsEntity::find()
|
||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||
.find_also_related(UsersEntity);
|
||||
|
||||
query = match params.sort_by.as_deref() {
|
||||
Some("updated_at") => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by_asc(TestimonialsColumn::UpdatedAt),
|
||||
_ => query.order_by_desc(TestimonialsColumn::UpdatedAt),
|
||||
},
|
||||
_ => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by_asc(TestimonialsColumn::CreatedAt),
|
||||
_ => query.order_by_desc(TestimonialsColumn::CreatedAt),
|
||||
},
|
||||
};
|
||||
|
||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||
let total = paginator.num_items().await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let rows = paginator.fetch_page((page - 1) as u64).await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let data: Vec<TestimonialEntity> = rows.into_iter()
|
||||
.filter_map(|(t, u)| {
|
||||
u.map(|user| TestimonialEntity {
|
||||
id: t.id,
|
||||
user_id: t.user_id,
|
||||
user_fullname: format!(
|
||||
"{} {}",
|
||||
user.first_name.as_deref().unwrap_or(""),
|
||||
user.last_name.as_deref().unwrap_or("")
|
||||
).trim().to_string(),
|
||||
role: t.role,
|
||||
content: t.content,
|
||||
is_deleted: t.is_deleted,
|
||||
created_at: t.created_at.to_rfc3339(),
|
||||
updated_at: t.updated_at.to_rfc3339(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
||||
let (testimonial, user) = TestimonialsEntity::find_by_id(id)
|
||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||
.find_also_related(UsersEntity)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?;
|
||||
|
||||
let user = user.ok_or_else(|| AppError::NotFoundError("User not found for testimonial".to_string()))?;
|
||||
|
||||
Ok(TestimonialEntity {
|
||||
id: testimonial.id,
|
||||
user_id: testimonial.user_id,
|
||||
user_fullname: format!(
|
||||
"{} {}",
|
||||
user.first_name.as_deref().unwrap_or(""),
|
||||
user.last_name.as_deref().unwrap_or("")
|
||||
).trim().to_string(),
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
is_deleted: testimonial.is_deleted,
|
||||
created_at: testimonial.created_at.to_rfc3339(),
|
||||
updated_at: testimonial.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError> {
|
||||
let active_model = TestimonialsActiveModel {
|
||||
id: ActiveValue::Set(entity.id),
|
||||
user_id: ActiveValue::Set(entity.user_id),
|
||||
role: ActiveValue::Set(entity.role.clone()),
|
||||
content: ActiveValue::Set(entity.content.clone()),
|
||||
is_deleted: ActiveValue::Set(false),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
let inserted = active_model.insert(self.db.as_ref()).await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(TestimonialEntity {
|
||||
id: inserted.id,
|
||||
user_id: inserted.user_id,
|
||||
user_fullname: entity.user_fullname,
|
||||
role: inserted.role,
|
||||
content: inserted.content,
|
||||
is_deleted: inserted.is_deleted,
|
||||
created_at: inserted.created_at.to_rfc3339(),
|
||||
updated_at: inserted.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
||||
let mut active_model: TestimonialsActiveModel = TestimonialsEntity::find_by_id(entity.id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||
.into();
|
||||
|
||||
active_model.role = ActiveValue::Set(entity.role);
|
||||
active_model.content = ActiveValue::Set(entity.content);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(self.db.as_ref()).await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let mut active_model: TestimonialsActiveModel = TestimonialsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||
.into();
|
||||
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
active_model.update(self.db.as_ref()).await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub use infrastructure::http::{testimonials_public_routes, testimonials_protected_routes};
|
||||
@@ -1,139 +0,0 @@
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_service::EventsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, http::HeaderMap, http::StatusCode};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::common_response;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::get_event_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
EventsService::get_event_by_id(&state, parsed_id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/create",
|
||||
request_body = EventsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[ADMIN] Create new event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn post_create_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::create_event(&state, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
request_body = EventsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Update event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn patch_update_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::update_event(&state, id, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Soft delete event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn delete_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
EventsService::delete_event(&state, parsed_id).await
|
||||
})
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::LazyLock;
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
// Custom URL validator that ensures valid HTTP/HTTPS URLs
|
||||
static VALID_URL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap());
|
||||
|
||||
pub fn validate_url(url: &str) -> Result<(), ValidationError> {
|
||||
if VALID_URL_REGEX.is_match(url) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("invalid_url"))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validator for future dates
|
||||
pub fn validate_future_date(end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||
let now = Utc::now();
|
||||
if end_date > &now {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("future_date"))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validator for event date ranges (for combined validation)
|
||||
pub fn validate_date_range(start_date: &DateTime<Utc>, end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||
if start_date <= end_date {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("date_range"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsCreateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Name must be between 1 and 100 characters"))]
|
||||
pub name: String,
|
||||
|
||||
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
|
||||
pub description: String,
|
||||
|
||||
#[validate(custom(
|
||||
function = "validate_url",
|
||||
message = "Detail link must be a valid HTTP/HTTPS URL"
|
||||
))]
|
||||
pub detail_link: String,
|
||||
|
||||
#[validate(range(min = 0.0, max = 1_000_000.0, message = "Price must be between 0 and 1,000,000"))]
|
||||
pub price: f64,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
#[validate(custom(
|
||||
function = "validate_future_date",
|
||||
message = "End date must be in the future"
|
||||
))]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(length(max = 200, message = "Location name cannot exceed 200 characters"))]
|
||||
pub location: Option<String>,
|
||||
pub is_online: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsUpdateRequestDto {
|
||||
#[validate(length(min = 1, message = "Name is required"))]
|
||||
pub name: String,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub description: String,
|
||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsListItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub location: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsDetailItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsQueryDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl EventsQueryDto {
|
||||
pub fn from(self) -> EventsListItemDto {
|
||||
EventsListItemDto {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
detail_link: self.detail_link,
|
||||
price: self.price,
|
||||
location: self.location,
|
||||
is_online: self.is_online,
|
||||
start_date: self.start_date,
|
||||
end_date: self.end_date,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, QueryOrder};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid; // Added Uuid import
|
||||
use imphnen_entities::seaorm::common::events::{Entity as EventsEntity, Column as EventsColumn, Model as EventsModel};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, AppStatePostgresExt};
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::Result;
|
||||
use crate::events::events_dto::EventsQueryDto;
|
||||
use crate::events::events_schema::EventsSchema;
|
||||
|
||||
pub struct EventsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> EventsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_event_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let page_size = 10u64;
|
||||
let _offset = (page - 1) * page_size;
|
||||
|
||||
let mut query = EventsEntity::find()
|
||||
.filter(EventsColumn::IsDeleted.eq(false)); // Add sorting
|
||||
if let Some(sort_by) = &meta.sort_by {
|
||||
match sort_by.as_str() {
|
||||
"created_at" => {
|
||||
if meta.order.as_deref() == Some("desc") {
|
||||
query = query.order_by_desc(EventsColumn::CreatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(EventsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
"name" => {
|
||||
if meta.order.as_deref() == Some("desc") {
|
||||
query = query.order_by_desc(EventsColumn::Name);
|
||||
} else {
|
||||
query = query.order_by_asc(EventsColumn::Name);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query = query.order_by_desc(EventsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = query.order_by_desc(EventsColumn::CreatedAt);
|
||||
}
|
||||
|
||||
let paginator = query.paginate(self.state.postgres_db(), page_size);
|
||||
let events: Vec<EventsModel> = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
let res: Vec<EventsQueryDto> = events.into_iter().map(|model| EventsQueryDto {
|
||||
id: model.id.to_string(),
|
||||
name: model.name,
|
||||
description: model.description,
|
||||
detail_link: model.detail_link,
|
||||
price: model.price,
|
||||
is_online: model.is_online,
|
||||
is_deleted: model.is_deleted,
|
||||
start_date: model.start_date.to_rfc3339(),
|
||||
end_date: model.end_date.to_rfc3339(),
|
||||
created_at: model.created_at.to_rfc3339(),
|
||||
updated_at: model.updated_at.to_rfc3339(),
|
||||
location: model.location,
|
||||
}).collect();
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_event_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = ResponseListSuccessDto {
|
||||
data: res,
|
||||
meta: None,
|
||||
};
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_event_by_id(&self, id: Uuid) -> Result<EventsQueryDto> {
|
||||
let now = Instant::now();
|
||||
|
||||
let event = EventsEntity::find_by_id(id)
|
||||
.filter(EventsColumn::IsDeleted.eq(false))
|
||||
.one(self.state.postgres_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Event not found"))?;
|
||||
|
||||
let result = EventsQueryDto {
|
||||
id: event.id.to_string(),
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
is_deleted: event.is_deleted,
|
||||
start_date: event.start_date.to_rfc3339(),
|
||||
end_date: event.end_date.to_rfc3339(),
|
||||
created_at: event.created_at.to_rfc3339(),
|
||||
updated_at: event.updated_at.to_rfc3339(),
|
||||
location: event.location,
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_event_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let active_model = imphnen_entities::seaorm::common::events::ActiveModel {
|
||||
id: ActiveValue::Set(Uuid::parse_str(&data.id)?),
|
||||
name: ActiveValue::Set(data.name),
|
||||
description: ActiveValue::Set(data.description),
|
||||
detail_link: ActiveValue::Set(data.detail_link),
|
||||
price: ActiveValue::Set(data.price),
|
||||
is_online: ActiveValue::Set(data.is_online),
|
||||
is_deleted: ActiveValue::Set(data.is_deleted),
|
||||
location: ActiveValue::Set(data.location),
|
||||
start_date: ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.start_date)?.with_timezone(&chrono::Utc)),
|
||||
end_date: ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.end_date)?.with_timezone(&chrono::Utc)),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
let _result = EventsEntity::insert(active_model).exec(self.state.postgres_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success create event".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let existing = self.query_event_by_id(Uuid::parse_str(&data.id)?).await?;
|
||||
if existing.is_deleted {
|
||||
return Err(AppError::BadRequestError("Event already deleted".to_string()));
|
||||
}
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::events::ActiveModel = EventsEntity::find_by_id(Uuid::parse_str(&data.id)?)
|
||||
.one(self.state.postgres_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Event not found"))?
|
||||
.into();
|
||||
|
||||
active_model.name = ActiveValue::Set(data.name);
|
||||
active_model.description = ActiveValue::Set(data.description);
|
||||
active_model.detail_link = ActiveValue::Set(data.detail_link);
|
||||
active_model.price = ActiveValue::Set(data.price);
|
||||
active_model.is_online = ActiveValue::Set(data.is_online);
|
||||
active_model.location = ActiveValue::Set(data.location);
|
||||
active_model.start_date = ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.start_date)?.with_timezone(&chrono::Utc));
|
||||
active_model.end_date = ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.end_date)?.with_timezone(&chrono::Utc));
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(self.state.postgres_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success update event".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_event(&self, id: Uuid) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let event = self.query_event_by_id(id).await?;
|
||||
if event.is_deleted {
|
||||
return Err(AppError::NotFoundError("Event not found".to_string()));
|
||||
}
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::events::ActiveModel = EventsEntity::find_by_id(id)
|
||||
.one(self.state.postgres_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Event not found"))?
|
||||
.into();
|
||||
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(self.state.postgres_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success delete event".into())
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::events_dto::{
|
||||
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsSchema {
|
||||
pub id: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub name: String,
|
||||
pub end_date: String,
|
||||
pub start_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for EventsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
detail_link: String::new(),
|
||||
price: 0.0,
|
||||
location: None,
|
||||
is_online: false,
|
||||
is_deleted: false,
|
||||
start_date: String::new(),
|
||||
end_date: String::new(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsSchema {
|
||||
pub fn from(dto: EventsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
detail_link: dto.detail_link,
|
||||
price: dto.price,
|
||||
location: dto.location,
|
||||
is_online: dto.is_online,
|
||||
is_deleted: false,
|
||||
start_date: dto.start_date,
|
||||
end_date: dto.end_date,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: EventsCreateRequestDto) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
is_deleted: false,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(payload: EventsUpdateRequestDto, id: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name: payload.name,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
updated_at: get_iso_date(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_repository::EventsRepository,
|
||||
events_schema::EventsSchema,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct EventsService;
|
||||
|
||||
impl EventsService {
|
||||
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<EventsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(EventsQueryDto::from)
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_event_by_id(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_by_id(id).await {
|
||||
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: EventsDetailItemDto {
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
start_date: event.start_date,
|
||||
end_date: event.end_date,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.updated_at,
|
||||
location: event.location,
|
||||
},
|
||||
}),
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_event(
|
||||
state: &AppState,
|
||||
payload: EventsCreateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::create(payload);
|
||||
match repo.query_create_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_event(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: EventsUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::update(payload, id);
|
||||
match repo.query_update_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_event(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_delete_event(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod events_controller;
|
||||
pub mod events_dto;
|
||||
pub mod events_repository;
|
||||
pub mod events_schema;
|
||||
pub mod events_service;
|
||||
|
||||
// Export only the necessary public items
|
||||
pub use events_dto::{
|
||||
EventsCreateRequestDto,
|
||||
EventsUpdateRequestDto,
|
||||
EventsListItemDto,
|
||||
EventsDetailItemDto,
|
||||
};
|
||||
pub use events_controller::{
|
||||
get_event_list,
|
||||
get_event_by_id,
|
||||
post_create_event,
|
||||
patch_update_event,
|
||||
delete_event,
|
||||
};
|
||||
|
||||
pub fn events_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/events", get(get_event_list))
|
||||
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||
}
|
||||
|
||||
pub fn events_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/events/create", post(post_create_event))
|
||||
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
|
||||
pub use events::events_public_routes;
|
||||
pub use events::events_protected_routes;
|
||||
pub use testimonials::testimonials_public_routes;
|
||||
pub use testimonials::testimonials_protected_routes;
|
||||
@@ -1,38 +0,0 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod testimonials_controller;
|
||||
pub mod testimonials_dto;
|
||||
pub mod testimonials_repository;
|
||||
pub mod testimonials_schema;
|
||||
pub mod testimonials_service;
|
||||
|
||||
// Export only the necessary public items
|
||||
pub use testimonials_dto::{
|
||||
TestimonialsCreateRequestDto,
|
||||
TestimonialsUpdateRequestDto,
|
||||
TestimonialsListItemDto,
|
||||
TestimonialsDetailItemDto,
|
||||
};
|
||||
pub use testimonials_controller::{
|
||||
get_testimonial_list,
|
||||
get_testimonial_by_id,
|
||||
post_create_testimonial,
|
||||
patch_update_testimonial,
|
||||
delete_testimonial,
|
||||
};
|
||||
|
||||
pub fn testimonials_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
||||
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
||||
}
|
||||
|
||||
pub fn testimonials_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
||||
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
||||
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_service::TestimonialsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, http::HeaderMap};
|
||||
use imphnen_iam::{UsersDetailQueryDto, require_auth};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::common_response;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::get_testimonial_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(axum::http::StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
TestimonialsService::get_testimonial_by_id(&state, parsed_id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/create",
|
||||
request_body = TestimonialsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[USER] Create new testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn post_create_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
request_body = TestimonialsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[USER] Update testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn patch_update_testimonial(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[USER] Soft delete testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn delete_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(axum::http::StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
TestimonialsService::delete_testimonial(&state, parsed_id, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
// Custom validator for content length and format
|
||||
pub fn validate_testimonial_content(content: &str) -> Result<(), ValidationError> {
|
||||
lazy_static! {
|
||||
static ref CONTENT_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9\s.,!?'-]+$").unwrap();
|
||||
}
|
||||
if CONTENT_REGEX.is_match(content) && content.len() <= 1000 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("invalid_content"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsCreateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 1000,
|
||||
message = "Content must be between 1 and 1000 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "validate_testimonial_content",
|
||||
message = "Content contains invalid characters or is too long"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsUpdateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 1000,
|
||||
message = "Content must be between 1 and 1000 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "validate_testimonial_content",
|
||||
message = "Content contains invalid characters or is too long"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsListItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsDetailItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsQueryDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl TestimonialsQueryDto {
|
||||
pub fn from(self) -> TestimonialsListItemDto {
|
||||
TestimonialsListItemDto {
|
||||
id: self.id,
|
||||
user_id: self.user_id,
|
||||
user_fullname: self.user_fullname,
|
||||
role: self.role,
|
||||
content: self.content,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, ActiveModelTrait, QueryOrder};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid; // Added Uuid import
|
||||
use imphnen_entities::seaorm::common::testimonials::{Entity as TestimonialsEntity, Column as TestimonialsColumn};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as UsersEntity;
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, AppStatePostgresExt};
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::Result;
|
||||
use crate::testimonials::testimonials_schema::TestimonialsSchema;
|
||||
use crate::testimonials::testimonials_dto::TestimonialsQueryDto;
|
||||
|
||||
pub struct TestimonialsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> TestimonialsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
fn get_db(&self) -> &DatabaseConnection {
|
||||
self.state.postgres_db()
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_testimonial_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(10);
|
||||
|
||||
let mut query = TestimonialsEntity::find()
|
||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||
.find_also_related(UsersEntity); // Apply sorting
|
||||
if let Some(sort_by) = &meta.sort_by {
|
||||
let order = meta.order.as_deref().unwrap_or("asc");
|
||||
match sort_by.as_str() {
|
||||
"created_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(TestimonialsColumn::CreatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(TestimonialsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
"updated_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(TestimonialsColumn::UpdatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(TestimonialsColumn::UpdatedAt);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query = query.order_by_desc(TestimonialsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = query.order_by_desc(TestimonialsColumn::CreatedAt);
|
||||
}
|
||||
|
||||
let paginator = query.paginate(db, per_page);
|
||||
let total_pages = paginator.num_pages().await?;
|
||||
let testimonials = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
let data: Vec<TestimonialsQueryDto> = testimonials
|
||||
.into_iter()
|
||||
.filter_map(|(testimonial, user)| {
|
||||
user.map(|u| TestimonialsQueryDto {
|
||||
id: testimonial.id.to_string(),
|
||||
user_id: testimonial.user_id.to_string(),
|
||||
user_fullname: format!("{} {}", u.first_name.as_deref().unwrap_or(""), u.last_name.as_deref().unwrap_or("")).trim().to_string(),
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
is_deleted: testimonial.is_deleted,
|
||||
created_at: testimonial.created_at.to_rfc3339(),
|
||||
updated_at: testimonial.updated_at.to_rfc3339(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_list' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let response = ResponseListSuccessDto {
|
||||
data,
|
||||
meta: Some(imphnen_entities::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total_pages),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_testimonial_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
) -> Result<TestimonialsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let (testimonial, user) = TestimonialsEntity::find_by_id(id)
|
||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||
.find_also_related(UsersEntity)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Testimonial not found"))?;
|
||||
|
||||
let user = user.ok_or_else(|| anyhow::anyhow!("User not found for testimonial"))?;
|
||||
|
||||
let result = TestimonialsQueryDto {
|
||||
id: testimonial.id.to_string(),
|
||||
user_id: testimonial.user_id.to_string(),
|
||||
user_fullname: format!("{} {}", user.first_name.as_deref().unwrap_or(""), user.last_name.as_deref().unwrap_or("")).trim().to_string(),
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
is_deleted: testimonial.is_deleted,
|
||||
created_at: testimonial.created_at.to_rfc3339(),
|
||||
updated_at: testimonial.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<TestimonialsSchema> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let active_model = imphnen_entities::seaorm::common::testimonials::ActiveModel {
|
||||
id: ActiveValue::Set(Uuid::parse_str(&data.id)?),
|
||||
user_id: ActiveValue::Set(Uuid::parse_str(&data.user_id)?),
|
||||
role: ActiveValue::Set(data.role.clone()),
|
||||
content: ActiveValue::Set(data.content.clone()),
|
||||
is_deleted: ActiveValue::Set(data.is_deleted),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
let inserted = active_model.insert(db).await?;
|
||||
let created_testimonial = TestimonialsSchema {
|
||||
id: inserted.id.to_string(),
|
||||
user_id: inserted.user_id.to_string(),
|
||||
role: inserted.role,
|
||||
content: inserted.content,
|
||||
is_deleted: inserted.is_deleted,
|
||||
created_at: inserted.created_at.to_rfc3339(),
|
||||
updated_at: inserted.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(created_testimonial)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let existing = self.query_testimonial_by_id(Uuid::parse_str(&data.id)?).await?;
|
||||
if existing.is_deleted {
|
||||
return Err(AppError::BadRequestError("Testimonial already deleted".to_string()));
|
||||
}
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::testimonials::ActiveModel = TestimonialsEntity::find_by_id(Uuid::parse_str(&data.id)?)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Testimonial not found"))?
|
||||
.into();
|
||||
|
||||
active_model.role = ActiveValue::Set(data.role.clone());
|
||||
active_model.content = ActiveValue::Set(data.content.clone());
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(db).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success update testimonial".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_testimonial(&self, id: Uuid) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let testimonial = self.query_testimonial_by_id(id).await?;
|
||||
if testimonial.is_deleted {
|
||||
return Err(AppError::NotFoundError("Testimonial not found".to_string()));
|
||||
}
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::testimonials::ActiveModel = TestimonialsEntity::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||
.into();
|
||||
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(db).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success delete testimonial".into())
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsSchema {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for TestimonialsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_id: Uuid::new_v4().to_string(),
|
||||
role: String::new(),
|
||||
content: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestimonialsSchema {
|
||||
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user_id: dto.user_id,
|
||||
role: dto.role,
|
||||
content: dto.content,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &str) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
id: String,
|
||||
user_id: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
updated_at: get_iso_date(),
|
||||
user_id: user_id.to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_repository::TestimonialsRepository,
|
||||
testimonials_schema::TestimonialsSchema,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, success_created_response, validate_request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct TestimonialsService;
|
||||
|
||||
impl TestimonialsService {
|
||||
pub async fn get_testimonial_list(
|
||||
state: &AppState,
|
||||
meta: MetaRequestDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<TestimonialsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(|e| e.from())
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_testimonial_by_id(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_by_id(id).await {
|
||||
Ok(testimonial) if !testimonial.is_deleted => {
|
||||
success_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: testimonial.id,
|
||||
user_id: testimonial.user_id,
|
||||
user_fullname: testimonial.user_fullname,
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
created_at: testimonial.created_at,
|
||||
updated_at: testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Testimonial not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_testimonial(
|
||||
state: &AppState,
|
||||
payload: TestimonialsCreateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
||||
match repo.query_create_testimonial(schema).await {
|
||||
Ok(created_testimonial) => {
|
||||
success_created_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: created_testimonial.id,
|
||||
user_id: created_testimonial.user_id,
|
||||
user_fullname: authenticated_user.fullname.clone(),
|
||||
role: created_testimonial.role,
|
||||
content: created_testimonial.content,
|
||||
created_at: created_testimonial.created_at,
|
||||
updated_at: created_testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_testimonial(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::update(payload, id, &authenticated_user.id);
|
||||
match repo.query_update_testimonial(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_testimonial(
|
||||
state: &AppState,
|
||||
id: Uuid,
|
||||
_authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_delete_testimonial(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
pub mod landing;
|
||||
|
||||
pub use landing::events;
|
||||
pub use landing::testimonials;
|
||||
pub use landing::events::events_public_routes;
|
||||
pub use landing::events::events_protected_routes;
|
||||
pub use landing::testimonials::testimonials_public_routes;
|
||||
pub use landing::testimonials::testimonials_protected_routes;
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imphnen-dimentorin"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -16,7 +16,8 @@ serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
zod-rs.workspace = true
|
||||
zod-rs-util.workspace = true
|
||||
axum-test.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
@@ -27,6 +28,10 @@ utoipa-swagger-ui.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
sea-orm.workspace = true
|
||||
paginator-rs.workspace = true
|
||||
paginator-utils.workspace = true
|
||||
paginator-sea-orm.workspace = true
|
||||
paginator-axum.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
pub mod v1;
|
||||
|
||||
// Explicitly export only what's needed from v1
|
||||
pub use v1::dimentorin_router;
|
||||
pub use v1::mentors::mentors_router;
|
||||
pub use v1::sessions::{
|
||||
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
AvailabilitySlotDto,
|
||||
};
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
pub use mentors::{mentors_public_routes, mentors_protected_routes};
|
||||
pub use sessions::{sessions_public_routes, sessions_protected_routes};
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_libs::{AppState, hash_password};
|
||||
use imphnen_entities::{RolesDetailQueryDto, users::UserProfileExtensionDto};
|
||||
use imphnen_iam::users::domain::{UserRepository, UserEntity};
|
||||
use imphnen_iam::roles::domain::RoleRepository;
|
||||
use tracing::error;
|
||||
use crate::mentors::domain::{MentorEntity, MentorRepository, MentorService};
|
||||
use crate::mentors::infrastructure::http::dto::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
};
|
||||
|
||||
pub struct MentorServiceImpl {
|
||||
repo: Arc<dyn MentorRepository>,
|
||||
state: Arc<AppState>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
role_repo: Arc<dyn RoleRepository>,
|
||||
}
|
||||
|
||||
impl MentorServiceImpl {
|
||||
pub fn new(
|
||||
repo: Arc<dyn MentorRepository>,
|
||||
state: Arc<AppState>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
role_repo: Arc<dyn RoleRepository>,
|
||||
) -> Self {
|
||||
Self { repo, state, user_repo, role_repo }
|
||||
}
|
||||
|
||||
fn build_detail_response(
|
||||
entity: &MentorEntity,
|
||||
user: Option<&imphnen_entities::UsersDetailQueryDto>,
|
||||
) -> MentorDetailResponseDto {
|
||||
MentorDetailResponseDto {
|
||||
id: entity.id.to_string(),
|
||||
user_id: entity.user_id.to_string(),
|
||||
fullname: user.map(|u| u.fullname.clone()),
|
||||
email: user.map(|u| u.email.clone()),
|
||||
legal_name: user.and_then(|u| u.legal_name.clone()),
|
||||
gender: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.gender.clone()),
|
||||
domicile: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.domicile.clone()),
|
||||
phone_for_verification: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.phone_for_verification.clone()),
|
||||
bio: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.bio.clone()),
|
||||
last_education: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.last_education.clone()),
|
||||
linkedin_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.linkedin_url.clone()),
|
||||
github_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.github_url.clone()),
|
||||
cv_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.cv_url.clone()),
|
||||
portfolio_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.portfolio_url.clone()),
|
||||
industries: entity.industries.clone(),
|
||||
expertise: entity.expertise.clone(),
|
||||
languages: entity.languages.clone(),
|
||||
current_company: entity.current_company.clone(),
|
||||
current_role: entity.current_role.clone(),
|
||||
years_of_experience: entity.years_of_experience,
|
||||
topics_of_interest: entity.topics_of_interest.clone(),
|
||||
preferred_mentee_level: entity.preferred_mentee_level.clone(),
|
||||
preferred_mentoring_formats: entity.preferred_mentoring_formats.clone(),
|
||||
availability_commitment: entity.availability_commitment.clone(),
|
||||
mentoring_rate: entity.mentoring_rate,
|
||||
status: entity.status.clone(),
|
||||
created_at: entity.created_at.to_rfc3339(),
|
||||
updated_at: entity.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MentorService for MentorServiceImpl {
|
||||
async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorListResponseDto>, AppError> {
|
||||
let result = self.repo.find_all(params).await?;
|
||||
|
||||
let mut items: Vec<MentorListResponseDto> = Vec::with_capacity(result.data.len());
|
||||
for entity in &result.data {
|
||||
let mut item = MentorListResponseDto {
|
||||
id: entity.id.to_string(),
|
||||
user_id: entity.user_id.to_string(),
|
||||
fullname: None,
|
||||
email: None,
|
||||
status: entity.status.clone(),
|
||||
created_at: entity.created_at.to_rfc3339(),
|
||||
updated_at: entity.updated_at.to_rfc3339(),
|
||||
};
|
||||
if let Ok(info) = self.state.user_lookup_service
|
||||
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||
.await
|
||||
{
|
||||
item.fullname = Some(info.basic_info.fullname);
|
||||
item.email = Some(info.basic_info.email);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
Ok(PaginatorResponse { data: items, meta: result.meta })
|
||||
}
|
||||
|
||||
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let entity = self.repo.find_by_id(id, false).await?;
|
||||
let user = self.state.user_lookup_service
|
||||
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&entity, user.as_ref()))
|
||||
}
|
||||
|
||||
async fn get_by_email(&self, email: &str) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let user_dto = self.state.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||
Ok(Self::build_detail_response(&entity, Some(&user_dto)))
|
||||
}
|
||||
|
||||
async fn register(
|
||||
&self,
|
||||
dto: MentorUserRegisterRequestDto,
|
||||
) -> Result<MentorRegisterResponseDto, AppError> {
|
||||
let user_email = dto.email.clone();
|
||||
|
||||
let user_id: Uuid = match self.user_repo.find_by_email(user_email.clone()).await {
|
||||
Ok(mut entity) => {
|
||||
let existing_user_id = Uuid::parse_str(&entity.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
if self.repo.find_by_user_id(existing_user_id, false).await.is_ok() {
|
||||
return Err(AppError::ConflictError(
|
||||
"Mentor profile already exists for this user".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mentor_role = self.role_repo
|
||||
.find_by_name("Mentor".to_string())
|
||||
.await
|
||||
.map_err(|_| AppError::BadRequestError("Mentor Role Not Found".to_string()))?;
|
||||
|
||||
entity.fullname = dto.fullname.clone();
|
||||
entity.is_active = false;
|
||||
entity.role = RolesDetailQueryDto {
|
||||
id: mentor_role.id.to_string(),
|
||||
name: mentor_role.name.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
entity.password = hash_password(&dto.password).map_err(|e| {
|
||||
error!("Failed to hash password for {}: {}", user_email, e);
|
||||
AppError::InternalServerError("Failed to hash password".to_string())
|
||||
})?;
|
||||
|
||||
let mut profile_ext = entity.profile_extension.clone().unwrap_or_default();
|
||||
profile_ext.phone_number = dto.phone_number.clone();
|
||||
profile_ext.phone_for_verification = dto.identity_and_verification.phone_for_verification.clone();
|
||||
profile_ext.gender = dto.identity_and_verification.gender.clone();
|
||||
profile_ext.domicile = dto.identity_and_verification.domicile.clone();
|
||||
profile_ext.bio = Some(dto.professional_profile.bio.clone());
|
||||
profile_ext.last_education = dto.professional_profile.last_education.clone();
|
||||
profile_ext.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
profile_ext.github_url = dto.professional_profile.github_url.clone();
|
||||
profile_ext.cv_url = dto.professional_profile.cv_url.clone();
|
||||
profile_ext.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
entity.profile_extension = Some(profile_ext);
|
||||
|
||||
let uid_str = entity.id.clone();
|
||||
self.user_repo.update(entity).await.map_err(|e| {
|
||||
error!("Failed to update user {} to mentor role: {}", user_email, e);
|
||||
AppError::InternalServerError(e.to_string())
|
||||
})?;
|
||||
|
||||
Uuid::parse_str(&uid_str)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
}
|
||||
Err(_) => {
|
||||
let mentor_role = self.role_repo
|
||||
.find_by_name("Mentor".to_string())
|
||||
.await
|
||||
.map_err(|_| AppError::BadRequestError("Mentor Role Not Found".to_string()))?;
|
||||
|
||||
let hashed_password = hash_password(&dto.password).map_err(|e| {
|
||||
error!("Failed to hash password for new user {}: {}", user_email, e);
|
||||
AppError::InternalServerError("Failed to hash password".to_string())
|
||||
})?;
|
||||
|
||||
let new_user_id = Uuid::new_v4();
|
||||
let profile_ext = UserProfileExtensionDto {
|
||||
phone_number: dto.phone_number.clone(),
|
||||
phone_for_verification: dto.identity_and_verification.phone_for_verification.clone(),
|
||||
gender: dto.identity_and_verification.gender.clone(),
|
||||
domicile: dto.identity_and_verification.domicile.clone(),
|
||||
bio: Some(dto.professional_profile.bio.clone()),
|
||||
last_education: dto.professional_profile.last_education.clone(),
|
||||
linkedin_url: dto.professional_profile.linkedin_url.clone(),
|
||||
github_url: dto.professional_profile.github_url.clone(),
|
||||
cv_url: dto.professional_profile.cv_url.clone(),
|
||||
portfolio_url: dto.professional_profile.portfolio_url.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let new_entity = UserEntity {
|
||||
id: new_user_id.to_string(),
|
||||
email: dto.email.clone(),
|
||||
fullname: dto.fullname.clone(),
|
||||
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||
password: hashed_password,
|
||||
is_active: false,
|
||||
role: RolesDetailQueryDto {
|
||||
id: mentor_role.id.to_string(),
|
||||
name: mentor_role.name.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
profile_extension: Some(profile_ext),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.user_repo.create(new_entity).await.map_err(|e| {
|
||||
error!("Failed to create new user {}: {}", user_email, e);
|
||||
AppError::InternalServerError(e.to_string())
|
||||
})?;
|
||||
|
||||
new_user_id
|
||||
}
|
||||
};
|
||||
|
||||
let new_entity = MentorEntity {
|
||||
id: Uuid::new_v4(),
|
||||
user_id,
|
||||
industries: dto.professional_profile.industries.clone(),
|
||||
expertise: dto.professional_profile.expertise.clone(),
|
||||
languages: dto.professional_profile.languages.clone(),
|
||||
current_company: dto.professional_profile.current_company.clone(),
|
||||
current_role: dto.professional_profile.current_role.clone(),
|
||||
years_of_experience: dto.professional_profile.years_of_experience,
|
||||
topics_of_interest: dto.mentoring_logistics.topics_of_interest.clone(),
|
||||
preferred_mentee_level: dto.mentoring_logistics.preferred_mentee_level.clone(),
|
||||
preferred_mentoring_formats: dto.mentoring_logistics.preferred_mentoring_formats.clone(),
|
||||
availability_commitment: dto.mentoring_logistics.availability_commitment.clone(),
|
||||
mentoring_rate: dto.mentoring_logistics.mentoring_rate_amount as f64,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let mentor_id = self.repo.create(new_entity.clone()).await.map_err(|e| {
|
||||
error!("Failed to create mentor profile for {}: {}", user_email, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(MentorRegisterResponseDto {
|
||||
id: mentor_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
email: Some(user_email),
|
||||
status: "pending".to_string(),
|
||||
created_at: new_entity.created_at.to_rfc3339(),
|
||||
updated_at: new_entity.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||
|
||||
if let Some(val) = dto.industries { entity.industries = val; }
|
||||
if let Some(val) = dto.expertise { entity.expertise = val; }
|
||||
if let Some(val) = dto.languages { entity.languages = val; }
|
||||
if let Some(val) = dto.current_company { entity.current_company = val; }
|
||||
if let Some(val) = dto.current_role { entity.current_role = val; }
|
||||
if let Some(val) = dto.years_of_experience { entity.years_of_experience = val; }
|
||||
if let Some(val) = dto.topics_of_interest { entity.topics_of_interest = val; }
|
||||
if let Some(val) = dto.preferred_mentee_level { entity.preferred_mentee_level = val; }
|
||||
if let Some(val) = dto.preferred_mentoring_formats { entity.preferred_mentoring_formats = val; }
|
||||
if let Some(val) = dto.availability_commitment { entity.availability_commitment = val; }
|
||||
if let Some(val) = dto.mentoring_rate_amount { entity.mentoring_rate = val as f64; }
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(id, false).await?;
|
||||
let user = self.state.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&updated, user.as_ref()))
|
||||
}
|
||||
|
||||
async fn update_me(
|
||||
&self,
|
||||
email: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let user_dto = self.state.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let mut entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||
|
||||
if let Some(val) = dto.industries { entity.industries = val; }
|
||||
if let Some(val) = dto.expertise { entity.expertise = val; }
|
||||
if let Some(val) = dto.languages { entity.languages = val; }
|
||||
if let Some(val) = dto.current_company { entity.current_company = val; }
|
||||
if let Some(val) = dto.current_role { entity.current_role = val; }
|
||||
if let Some(val) = dto.years_of_experience { entity.years_of_experience = val; }
|
||||
if let Some(val) = dto.topics_of_interest { entity.topics_of_interest = val; }
|
||||
if let Some(val) = dto.preferred_mentee_level { entity.preferred_mentee_level = val; }
|
||||
if let Some(val) = dto.preferred_mentoring_formats { entity.preferred_mentoring_formats = val; }
|
||||
if let Some(val) = dto.availability_commitment { entity.availability_commitment = val; }
|
||||
if let Some(val) = dto.mentoring_rate_amount { entity.mentoring_rate = val as f64; }
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
let entity_id = entity.id;
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(entity_id, false).await?;
|
||||
let refreshed_user = self.state.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&updated, refreshed_user.as_ref()))
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.repo.soft_delete(id).await
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||
entity.status = dto.status;
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(id, false).await?;
|
||||
let user = self.state.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&updated, user.as_ref()))
|
||||
}
|
||||
|
||||
async fn get_status(&self, email: &str) -> Result<String, AppError> {
|
||||
let user_dto = self.state.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| {
|
||||
AppError::NotFoundError("No mentor application found for current user".to_string())
|
||||
})?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let entity = self.repo.find_by_user_id(user_id, false).await.map_err(|_| {
|
||||
AppError::NotFoundError("No mentor application found for current user".to_string())
|
||||
})?;
|
||||
|
||||
Ok(entity.status)
|
||||
}
|
||||
|
||||
async fn get_entity_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
self.repo.find_by_id(id, include_deleted).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod mentor_service;
|
||||
|
||||
pub use mentor_service::MentorServiceImpl;
|
||||
@@ -0,0 +1,22 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MentorEntity {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod mentor;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
|
||||
pub use mentor::MentorEntity;
|
||||
pub use repository::MentorRepository;
|
||||
pub use service::MentorService;
|
||||
@@ -0,0 +1,33 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::mentor::MentorEntity;
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait MentorRepository: Send + Sync {
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorEntity>, AppError>;
|
||||
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
|
||||
async fn find_by_user_id(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
|
||||
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError>;
|
||||
|
||||
async fn update(&self, entity: MentorEntity) -> Result<(), AppError>;
|
||||
|
||||
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::mentor::MentorEntity;
|
||||
use crate::mentors::infrastructure::http::dto::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait MentorService: Send + Sync {
|
||||
async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorListResponseDto>, AppError>;
|
||||
|
||||
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetailResponseDto, AppError>;
|
||||
|
||||
async fn get_by_email(&self, email: &str) -> Result<MentorDetailResponseDto, AppError>;
|
||||
|
||||
async fn register(
|
||||
&self,
|
||||
dto: MentorUserRegisterRequestDto,
|
||||
) -> Result<MentorRegisterResponseDto, AppError>;
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError>;
|
||||
|
||||
async fn update_me(
|
||||
&self,
|
||||
email: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError>;
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
|
||||
async fn verify(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError>;
|
||||
|
||||
async fn get_status(&self, email: &str) -> Result<String, AppError>;
|
||||
|
||||
async fn get_entity_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
// ============================================================
|
||||
// Response DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorListResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorDetailResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorRegisterResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Request DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorUserRegisterRequestDto {
|
||||
#[zod(email, min_length(1))]
|
||||
pub email: String,
|
||||
#[zod(min_length(8), regex(pattern = "^[A-Za-z\\d@$!%*?&]{8,}$"))]
|
||||
pub password: String,
|
||||
#[zod(min_length(2))]
|
||||
pub fullname: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorUserRegisterRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct IdentityAndVerification {
|
||||
#[zod(min_length(3))]
|
||||
pub legal_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[zod(url)]
|
||||
pub identity_document_url: String,
|
||||
#[zod(min_length(10), max_length(15))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for IdentityAndVerification {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct ProfessionalProfile {
|
||||
#[zod(min_length(50))]
|
||||
pub bio: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub current_company: String,
|
||||
#[zod(min_length(1))]
|
||||
pub current_role: String,
|
||||
#[zod(min(2.0), int)]
|
||||
pub years_of_experience: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for ProfessionalProfile {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentoringLogistics {
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
#[zod(min_length(5))]
|
||||
pub availability_commitment: String,
|
||||
#[zod(min(1.0))]
|
||||
pub mentoring_rate_amount: u64,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentoringLogistics {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorUpdateRequestDto {
|
||||
#[zod(min_length(3))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub legal_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[zod(min_length(10), max_length(15))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
#[zod(min_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub industries: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expertise: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub languages: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_company: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_role: Option<String>,
|
||||
#[zod(min(2.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub years_of_experience: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub topics_of_interest: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentee_level: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentoring_formats: Option<Vec<String>>,
|
||||
#[zod(min_length(5))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub availability_commitment: Option<String>,
|
||||
#[zod(min(1.0))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentoring_rate_amount: Option<u64>,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorUpdateRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorVerifyRequestDto {
|
||||
#[zod(min_length(1))]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorVerifyRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema, Default)]
|
||||
pub struct MentoringRate {
|
||||
#[zod(min(1.0))]
|
||||
pub amount: u64,
|
||||
#[zod(min_length(1))]
|
||||
pub currency: String,
|
||||
#[zod(min_length(1))]
|
||||
pub per_duration: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentoringRate {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorRegisterFromTokenRequestDto {
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorRegisterFromTokenRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use paginator_axum::PaginationQuery;
|
||||
use uuid::Uuid;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_utils::{ApiSuccess, ApiPaginated, ApiMessage, extract_email};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::AppError;
|
||||
use crate::mentors::domain::MentorService;
|
||||
use super::dto::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/create",
|
||||
request_body = MentorUserRegisterRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||
(status = 400, description = "[PUBLIC] Bad request - validation error"),
|
||||
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
|
||||
(status = 500, description = "[PUBLIC] Internal server error")
|
||||
),
|
||||
tag = "Mentors"
|
||||
)]
|
||||
pub async fn post_register_mentor(
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
match service.register(dto).await {
|
||||
Ok(resp) => ApiSuccess(resp).into_response(),
|
||||
Err(e) => ApiMessage::new(e.status_code(), e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors",
|
||||
params(
|
||||
("page" = Option<u64>, Query, description = "Page number"),
|
||||
("per_page" = Option<u64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search query"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_list(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
PaginationQuery(params): PaginationQuery,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers, state, [PermissionsEnum::ReadListMentors], {
|
||||
let result = service.list(params).await?;
|
||||
Ok(ApiPaginated(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_by_id(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::ReadDetailMentors], {
|
||||
let dto = service.get_by_id(mentor_uuid).await?;
|
||||
Ok(ApiSuccess(dto))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_update_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::UpdateMentors], {
|
||||
let result = service.update(mentor_uuid, dto).await?;
|
||||
Ok(ApiSuccess(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/mentors/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn delete_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::DeleteMentors], {
|
||||
service.delete(mentor_uuid).await?;
|
||||
Ok(ApiMessage::ok("Mentor deleted successfully"))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/verify/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorVerifyRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_verify_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::VerifyMentors], {
|
||||
let result = service.verify(mentor_uuid, dto).await?;
|
||||
Ok(ApiSuccess(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers.clone(), state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||
let email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let dto = service.get_by_email(&email).await
|
||||
.map_err(|_| AppError::ForbiddenError("Mentor profile not found for current user".to_string()))?;
|
||||
Ok(ApiSuccess(dto))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/me/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[MENTOR] Bad request - validation error"),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 404, description = "[MENTOR] Mentor profile not found"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_update_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers.clone(), state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||
let email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.update_me(&email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
|
||||
),
|
||||
tag = "Mentors - Admin"
|
||||
)]
|
||||
pub async fn put_update_mentor_no_id() -> impl IntoResponse {
|
||||
ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, "Mentor ID is required for update")
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me/status",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor application status", body = String),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] No mentor application found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_status(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers.clone(), state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||
let email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let status = service.get_status(&email).await
|
||||
.map_err(|_| AppError::ForbiddenError("No mentor application found for current user".to_string()))?;
|
||||
Ok(ApiMessage::ok(&status))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::{mentors_protected_routes, mentors_public_routes};
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
routing::{delete, get, post, put},
|
||||
Extension, Router,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_iam::users::infrastructure::persistence::PostgresUserRepository;
|
||||
use imphnen_iam::roles::infrastructure::persistence::PostgresRoleRepository;
|
||||
use crate::mentors::application::MentorServiceImpl;
|
||||
use crate::mentors::domain::MentorService;
|
||||
use crate::mentors::infrastructure::persistence::PostgresMentorRepository;
|
||||
use super::handlers::{
|
||||
delete_mentor, get_mentor_by_id, get_mentor_list, get_mentor_me, get_mentor_status,
|
||||
post_register_mentor, put_update_mentor, put_update_mentor_me, put_update_mentor_no_id,
|
||||
put_verify_mentor,
|
||||
};
|
||||
|
||||
fn build_service(db: DatabaseConnection, state: Arc<AppState>) -> Arc<dyn MentorService> {
|
||||
let user_repo = Arc::new(PostgresUserRepository::new(db.clone()));
|
||||
let role_repo = Arc::new(PostgresRoleRepository::new(db.clone()));
|
||||
let repo = Arc::new(PostgresMentorRepository::new(db));
|
||||
Arc::new(MentorServiceImpl::new(repo, state, user_repo, role_repo))
|
||||
}
|
||||
|
||||
pub fn mentors_public_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||
let service = build_service(db, state);
|
||||
Router::new()
|
||||
.route("/mentors/create", post(post_register_mentor))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn mentors_protected_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||
let svc = build_service(db, Arc::clone(&state));
|
||||
Router::new()
|
||||
.route("/mentors", get(get_mentor_list))
|
||||
.route("/mentors/me", get(get_mentor_me))
|
||||
.route("/mentors/me/update", put(put_update_mentor_me))
|
||||
.route("/mentors/me/status", get(get_mentor_status))
|
||||
.route("/mentors/detail/{id}", get(get_mentor_by_id))
|
||||
.route("/mentors/update/{id}", put(put_update_mentor))
|
||||
.route("/mentors/update", put(put_update_mentor_no_id))
|
||||
.route("/mentors/delete/{id}", delete(delete_mentor))
|
||||
.route("/mentors/verify/{id}", put(put_verify_mentor))
|
||||
.layer(Extension(svc))
|
||||
.layer(Extension((*state).clone()))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod postgres_mentor_repository;
|
||||
|
||||
pub use postgres_mentor_repository::PostgresMentorRepository;
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, Order, QueryOrder, PaginatorTrait};
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_entities::seaorm::auth::mentors::{
|
||||
Entity as MentorsEntity,
|
||||
Column as MentorColumn,
|
||||
ActiveModel as MentorActiveModel,
|
||||
Model as MentorModel,
|
||||
};
|
||||
use crate::mentors::domain::{mentor::MentorEntity, repository::MentorRepository};
|
||||
|
||||
fn model_to_entity(model: MentorModel) -> MentorEntity {
|
||||
MentorEntity {
|
||||
id: model.id,
|
||||
user_id: model.user_id,
|
||||
industries: serde_json::from_value(
|
||||
model.industries.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
expertise: serde_json::from_value(
|
||||
model.expertise.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
languages: serde_json::from_value(
|
||||
model.languages.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
current_company: model.current_company.unwrap_or_default(),
|
||||
current_role: model.current_role.unwrap_or_default(),
|
||||
years_of_experience: model.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(
|
||||
model.topics_of_interest.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(
|
||||
&model.preferred_mentee_level.unwrap_or_default(),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(
|
||||
model
|
||||
.preferred_mentoring_formats
|
||||
.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
availability_commitment: model.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: model.mentoring_rate.unwrap_or(0.0),
|
||||
status: model.status.unwrap_or_default(),
|
||||
is_deleted: model.is_deleted,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresMentorRepository {
|
||||
db: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl PostgresMentorRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MentorRepository for PostgresMentorRepository {
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let mut query = MentorsEntity::find()
|
||||
.filter(MentorColumn::IsDeleted.eq(false));
|
||||
|
||||
query = match params.sort_by.as_deref() {
|
||||
Some("updated_at") => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by(MentorColumn::UpdatedAt, Order::Asc),
|
||||
_ => query.order_by(MentorColumn::UpdatedAt, Order::Desc),
|
||||
},
|
||||
_ => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by(MentorColumn::CreatedAt, Order::Asc),
|
||||
_ => query.order_by(MentorColumn::CreatedAt, Order::Desc),
|
||||
},
|
||||
};
|
||||
|
||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let mentors = paginator
|
||||
.fetch_page((page - 1) as u64)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let data = mentors.into_iter().map(model_to_entity).collect();
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
let mut query = MentorsEntity::find_by_id(id);
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let model = query
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
|
||||
async fn find_by_user_id(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
let mut query = MentorsEntity::find()
|
||||
.filter(MentorColumn::UserId.eq(user_id));
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let model = query
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
|
||||
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError> {
|
||||
let active_model = MentorActiveModel {
|
||||
user_id: ActiveValue::Set(entity.user_id),
|
||||
industries: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.industries)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
expertise: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.expertise)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
languages: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.languages)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
current_company: ActiveValue::Set(Some(entity.current_company)),
|
||||
current_role: ActiveValue::Set(Some(entity.current_role)),
|
||||
years_of_experience: ActiveValue::Set(Some(entity.years_of_experience)),
|
||||
topics_of_interest: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.topics_of_interest)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
preferred_mentee_level: ActiveValue::Set(Some(
|
||||
serde_json::to_string(&entity.preferred_mentee_level)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
preferred_mentoring_formats: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
availability_commitment: ActiveValue::Set(Some(entity.availability_commitment)),
|
||||
mentoring_rate: ActiveValue::Set(Some(entity.mentoring_rate)),
|
||||
status: ActiveValue::Set(Some(entity.status)),
|
||||
is_deleted: ActiveValue::Set(false),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = MentorsEntity::insert(active_model)
|
||||
.exec(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(result.last_insert_id)
|
||||
}
|
||||
|
||||
async fn update(&self, entity: MentorEntity) -> Result<(), AppError> {
|
||||
let mut active_model: MentorActiveModel = MentorsEntity::find_by_id(entity.id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?
|
||||
.into();
|
||||
|
||||
if !entity.industries.is_empty() {
|
||||
active_model.industries = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.industries)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.expertise.is_empty() {
|
||||
active_model.expertise = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.expertise)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.languages.is_empty() {
|
||||
active_model.languages = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.languages)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.current_company.is_empty() {
|
||||
active_model.current_company = ActiveValue::Set(Some(entity.current_company));
|
||||
}
|
||||
if !entity.current_role.is_empty() {
|
||||
active_model.current_role = ActiveValue::Set(Some(entity.current_role));
|
||||
}
|
||||
active_model.years_of_experience = ActiveValue::Set(Some(entity.years_of_experience));
|
||||
if !entity.topics_of_interest.is_empty() {
|
||||
active_model.topics_of_interest = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.topics_of_interest)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.preferred_mentee_level.is_empty() {
|
||||
active_model.preferred_mentee_level = ActiveValue::Set(Some(
|
||||
serde_json::to_string(&entity.preferred_mentee_level)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.preferred_mentoring_formats.is_empty() {
|
||||
active_model.preferred_mentoring_formats = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.availability_commitment.is_empty() {
|
||||
active_model.availability_commitment =
|
||||
ActiveValue::Set(Some(entity.availability_commitment));
|
||||
}
|
||||
active_model.mentoring_rate = ActiveValue::Set(Some(entity.mentoring_rate));
|
||||
if !entity.status.is_empty() {
|
||||
active_model.status = ActiveValue::Set(Some(entity.status));
|
||||
}
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let model = MentorsEntity::find_by_id(id)
|
||||
.filter(MentorColumn::IsDeleted.eq(false))
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
|
||||
let mut active_model: MentorActiveModel = model.into();
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub use infrastructure::http::routes::{mentors_protected_routes, mentors_public_routes};
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod session_service;
|
||||
|
||||
pub use session_service::SessionServiceImpl;
|
||||
@@ -0,0 +1,310 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use crate::sessions::domain::{SessionEntity, SessionRepository, SessionService};
|
||||
use crate::sessions::infrastructure::http::dto::{
|
||||
AvailabilitySlotDto, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionDetailDto, SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
|
||||
pub struct SessionServiceImpl {
|
||||
repo: Arc<dyn SessionRepository>,
|
||||
}
|
||||
|
||||
impl SessionServiceImpl {
|
||||
pub fn new(repo: Arc<dyn SessionRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SessionService for SessionServiceImpl {
|
||||
async fn book_session(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
dto: BookSessionRequestDto,
|
||||
) -> Result<BookSessionResponseDto, AppError> {
|
||||
let scheduled_at = DateTime::parse_from_rfc3339(&dto.scheduled_at)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid scheduled_at format: {}", e)))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let mentee_uuid = Uuid::parse_str(&user_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let entity = SessionEntity {
|
||||
id: Uuid::new_v4(),
|
||||
mentor_id: mentor_uuid,
|
||||
mentee_id: mentee_uuid,
|
||||
topic: dto.topic,
|
||||
description: dto.description,
|
||||
scheduled_at,
|
||||
duration_minutes: dto.duration_minutes.unwrap_or(60),
|
||||
meeting_link: None,
|
||||
session_type: dto.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
let created = self.repo.create(entity).await?;
|
||||
|
||||
Ok(BookSessionResponseDto {
|
||||
id: created.id.to_string(),
|
||||
mentor_id: created.mentor_id.to_string(),
|
||||
mentee_id: created.mentee_id.to_string(),
|
||||
topic: created.topic,
|
||||
description: created.description,
|
||||
scheduled_at: created.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: created.duration_minutes,
|
||||
session_type: created.session_type,
|
||||
status: created.status,
|
||||
created_at: created.created_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let count = self
|
||||
.repo
|
||||
.count_by_mentor(mentor_uuid, status_filter.clone())
|
||||
.await?;
|
||||
|
||||
let sessions = self
|
||||
.repo
|
||||
.find_by_mentor_id(mentor_uuid, status_filter)
|
||||
.await?;
|
||||
|
||||
let items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id.to_string(),
|
||||
mentor_id: s.mentor_id.to_string(),
|
||||
mentee_id: s.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
mentee_email: None,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SessionListResponseDto {
|
||||
sessions: items,
|
||||
total: count,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError> {
|
||||
let user_uuid = Uuid::parse_str(&user_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let count = self
|
||||
.repo
|
||||
.count_by_mentee(user_uuid, status_filter.clone())
|
||||
.await?;
|
||||
|
||||
let sessions = self
|
||||
.repo
|
||||
.find_by_mentee_id(user_uuid, status_filter)
|
||||
.await?;
|
||||
|
||||
let items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id.to_string(),
|
||||
mentor_id: s.mentor_id.to_string(),
|
||||
mentee_id: s.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
mentee_email: None,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SessionListResponseDto {
|
||||
sessions: items,
|
||||
total: count,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_mentor_availability(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
) -> Result<MentorAvailabilityDto, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let booked_dates = self.repo.find_booked_dates(mentor_uuid).await?;
|
||||
|
||||
let mut slots = Vec::new();
|
||||
let today = Utc::now().date_naive();
|
||||
|
||||
for i in 0..7 {
|
||||
let date = today + Duration::days(i);
|
||||
let date_str = date.format("%Y-%m-%d").to_string();
|
||||
|
||||
for hour in 9..17 {
|
||||
let time_str = format!("{:02}:00", hour);
|
||||
let datetime_prefix = format!("{}T{}", date_str, time_str);
|
||||
|
||||
let is_booked = booked_dates
|
||||
.iter()
|
||||
.any(|d| d.starts_with(&datetime_prefix[..13]));
|
||||
|
||||
slots.push(AvailabilitySlotDto {
|
||||
date: date_str.clone(),
|
||||
time: time_str,
|
||||
available: !is_booked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MentorAvailabilityDto {
|
||||
mentor_id,
|
||||
availability_commitment: "Available weekdays 9 AM - 5 PM".to_string(),
|
||||
preferred_formats: vec!["video_call".to_string(), "phone_call".to_string()],
|
||||
slots,
|
||||
booked_dates,
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_session_status(
|
||||
&self,
|
||||
session_id: String,
|
||||
_user_id: String,
|
||||
dto: UpdateSessionStatusRequestDto,
|
||||
) -> Result<UpdateSessionStatusResponseDto, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||
|
||||
let mut session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
session.status = dto.status;
|
||||
if let Some(link) = dto.meeting_link {
|
||||
session.meeting_link = Some(link);
|
||||
}
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
let updated = self.repo.update(session_uuid, session).await?;
|
||||
|
||||
Ok(UpdateSessionStatusResponseDto {
|
||||
id: updated.id.to_string(),
|
||||
status: updated.status,
|
||||
meeting_link: updated.meeting_link,
|
||||
updated_at: updated.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn submit_feedback(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: SessionFeedbackRequestDto,
|
||||
) -> Result<SessionFeedbackResponseDto, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||
|
||||
let mut session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
if session.mentee_id.to_string() != user_id {
|
||||
return Err(AppError::ForbiddenError(
|
||||
"Only the mentee can submit feedback".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if session.status != "completed" {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Feedback can only be submitted for completed sessions".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
session.feedback = Some(dto.feedback.clone());
|
||||
session.rating = Some(dto.rating);
|
||||
session.feedback_submitted_at = Some(Utc::now());
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
let updated = self.repo.update(session_uuid, session).await?;
|
||||
let submitted_at = updated
|
||||
.feedback_submitted_at
|
||||
.unwrap_or_else(Utc::now)
|
||||
.to_rfc3339();
|
||||
|
||||
Ok(SessionFeedbackResponseDto {
|
||||
id: updated.id.to_string(),
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
submitted_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_session_detail(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<SessionDetailDto, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||
|
||||
let session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
Ok(SessionDetailDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentor_fullname: None,
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
topic: session.topic,
|
||||
description: session.description,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
meeting_link: session.meeting_link,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
feedback: session.feedback,
|
||||
rating: session.rating,
|
||||
feedback_submitted_at: session.feedback_submitted_at.map(|dt| dt.to_rfc3339()),
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
updated_at: session.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
pub mod session;
|
||||
|
||||
pub use repository::SessionRepository;
|
||||
pub use service::SessionService;
|
||||
pub use session::SessionEntity;
|
||||
@@ -0,0 +1,48 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::session::SessionEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SessionRepository: Send + Sync {
|
||||
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError>;
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError>;
|
||||
|
||||
async fn find_by_mentor_id(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError>;
|
||||
|
||||
async fn find_by_mentee_id(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError>;
|
||||
|
||||
async fn find_booked_dates(&self, mentor_id: Uuid) -> Result<Vec<String>, AppError>;
|
||||
|
||||
async fn update(&self, id: Uuid, entity: SessionEntity) -> Result<SessionEntity, AppError>;
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
|
||||
async fn count_by_mentor(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError>;
|
||||
|
||||
async fn count_by_mentee(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError>;
|
||||
|
||||
async fn find_all_paginated(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<SessionEntity>, AppError>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use crate::sessions::infrastructure::http::dto::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionDetailDto, SessionFeedbackRequestDto, SessionFeedbackResponseDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SessionService: Send + Sync {
|
||||
async fn book_session(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
dto: BookSessionRequestDto,
|
||||
) -> Result<BookSessionResponseDto, AppError>;
|
||||
|
||||
async fn get_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError>;
|
||||
|
||||
async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError>;
|
||||
|
||||
async fn get_mentor_availability(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
) -> Result<MentorAvailabilityDto, AppError>;
|
||||
|
||||
async fn update_session_status(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: UpdateSessionStatusRequestDto,
|
||||
) -> Result<UpdateSessionStatusResponseDto, AppError>;
|
||||
|
||||
async fn submit_feedback(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: SessionFeedbackRequestDto,
|
||||
) -> Result<SessionFeedbackResponseDto, AppError>;
|
||||
|
||||
async fn get_session_detail(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<SessionDetailDto, AppError>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionEntity {
|
||||
pub id: Uuid,
|
||||
pub mentor_id: Uuid,
|
||||
pub mentee_id: Uuid,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: DateTime<Utc>,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
// ============================================================
|
||||
// Request DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[zod(min_length(3), max_length(200))]
|
||||
pub topic: String,
|
||||
#[zod(max_length(1000))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub scheduled_at: String,
|
||||
#[zod(min(15.0), max(240.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
#[zod(max_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for BookSessionRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[zod(min_length(1), max_length(50))]
|
||||
pub status: String,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for UpdateSessionStatusRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[zod(min_length(10), max_length(2000))]
|
||||
pub feedback: String,
|
||||
#[zod(min(1.0), max(5.0), int)]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for SessionFeedbackRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Response DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BookSessionResponseDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListItemDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListResponseDto {
|
||||
pub sessions: Vec<SessionListItemDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetailDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String,
|
||||
pub time: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorAvailabilityDto {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlotDto>,
|
||||
pub booked_dates: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateSessionStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use imphnen_libs::ValidatedJson;
|
||||
use imphnen_utils::{ApiSuccess, extract_email};
|
||||
use imphnen_utils::AppError;
|
||||
use crate::sessions::domain::SessionService;
|
||||
use super::dto::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
|
||||
UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SessionStatusFilter {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/{id}/sessions/create",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
request_body = BookSessionRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_book_session(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<BookSessionRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.book_session(mentor_id, user_email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/sessions",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
("status" = Option<String>, Query, description = "Filter by status"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let _user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.get_mentor_sessions(mentor_id, filter.status).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/availability",
|
||||
tag = "sessions",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_availability(
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let resp = service.get_mentor_availability(mentor_id).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/sessions/update/{id}/status",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = UpdateSessionStatusRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_session_status(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(session_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<UpdateSessionStatusRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.update_session_status(session_id, user_email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sessions/{id}/feedback/create",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = SessionFeedbackRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
|
||||
(status = 400, description = "Invalid request or session not completed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_submit_feedback(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(session_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<SessionFeedbackRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.submit_feedback(session_id, user_email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/users/me/sessions",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("status" = Option<String>, Query, description = "Filter by status"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_my_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.get_user_sessions(user_email, filter.status).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::{sessions_protected_routes, sessions_public_routes};
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
routing::{get, post, put},
|
||||
Extension, Router,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use imphnen_libs::AppState;
|
||||
use crate::sessions::application::SessionServiceImpl;
|
||||
use crate::sessions::domain::SessionService;
|
||||
use crate::sessions::infrastructure::persistence::PostgresSessionRepository;
|
||||
use super::handlers::{
|
||||
get_mentor_availability, get_mentor_sessions, get_my_sessions, post_book_session,
|
||||
post_submit_feedback, put_update_session_status,
|
||||
};
|
||||
|
||||
fn build_service(db: DatabaseConnection) -> Arc<dyn SessionService> {
|
||||
let repo = Arc::new(PostgresSessionRepository::new(db));
|
||||
Arc::new(SessionServiceImpl::new(repo))
|
||||
}
|
||||
|
||||
pub fn sessions_public_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/availability", get(get_mentor_availability))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn sessions_protected_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/sessions/create", post(post_book_session))
|
||||
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
|
||||
.route("/sessions/update/{id}/status", put(put_update_session_status))
|
||||
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
|
||||
.route("/users/me/sessions", get(get_my_sessions))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension((*state).clone()))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod postgres_session_repository;
|
||||
|
||||
pub use postgres_session_repository::PostgresSessionRepository;
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, IntoActiveModel, Order, QueryOrder, PaginatorTrait};
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_entities::seaorm::auth::sessions::{
|
||||
Entity as SessionsEntity,
|
||||
Column as SessionColumn,
|
||||
ActiveModel as SessionActiveModel,
|
||||
Model as SessionModel,
|
||||
};
|
||||
use crate::sessions::domain::{session::SessionEntity, repository::SessionRepository};
|
||||
|
||||
fn model_to_entity(model: SessionModel) -> SessionEntity {
|
||||
SessionEntity {
|
||||
id: model.id,
|
||||
mentor_id: model.mentor_id,
|
||||
mentee_id: model.mentee_id,
|
||||
topic: model.topic,
|
||||
description: model.description,
|
||||
scheduled_at: model.scheduled_at,
|
||||
duration_minutes: model.duration_minutes,
|
||||
meeting_link: model.meeting_link,
|
||||
session_type: model.session_type,
|
||||
status: model.status,
|
||||
feedback: model.feedback,
|
||||
rating: model.rating,
|
||||
feedback_submitted_at: model.feedback_submitted_at,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresSessionRepository {
|
||||
db: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl PostgresSessionRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SessionRepository for PostgresSessionRepository {
|
||||
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError> {
|
||||
let active_model = SessionActiveModel {
|
||||
id: ActiveValue::Set(entity.id),
|
||||
mentor_id: ActiveValue::Set(entity.mentor_id),
|
||||
mentee_id: ActiveValue::Set(entity.mentee_id),
|
||||
topic: ActiveValue::Set(entity.topic.clone()),
|
||||
description: ActiveValue::Set(entity.description.clone()),
|
||||
scheduled_at: ActiveValue::Set(entity.scheduled_at),
|
||||
duration_minutes: ActiveValue::Set(entity.duration_minutes),
|
||||
meeting_link: ActiveValue::Set(entity.meeting_link.clone()),
|
||||
session_type: ActiveValue::Set(entity.session_type.clone()),
|
||||
status: ActiveValue::Set(entity.status.clone()),
|
||||
feedback: ActiveValue::Set(entity.feedback.clone()),
|
||||
rating: ActiveValue::Set(entity.rating),
|
||||
feedback_submitted_at: ActiveValue::Set(entity.feedback_submitted_at),
|
||||
created_at: ActiveValue::Set(entity.created_at),
|
||||
updated_at: ActiveValue::Set(entity.updated_at),
|
||||
};
|
||||
|
||||
let model: SessionModel = active_model
|
||||
.insert(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(model.map(model_to_entity))
|
||||
}
|
||||
|
||||
async fn find_by_mentor_id(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let models = query
|
||||
.all(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(models.into_iter().map(model_to_entity).collect())
|
||||
}
|
||||
|
||||
async fn find_by_mentee_id(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MenteeId.eq(mentee_id))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let models = query
|
||||
.all(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(models.into_iter().map(model_to_entity).collect())
|
||||
}
|
||||
|
||||
async fn find_booked_dates(&self, mentor_id: Uuid) -> Result<Vec<String>, AppError> {
|
||||
let sessions = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||
.filter(SessionColumn::Status.is_in(["pending", "confirmed"]))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Asc)
|
||||
.all(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(sessions
|
||||
.into_iter()
|
||||
.map(|s| s.scheduled_at.to_rfc3339())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn update(&self, id: Uuid, entity: SessionEntity) -> Result<SessionEntity, AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
let mut active_model = model.into_active_model();
|
||||
|
||||
active_model.topic = ActiveValue::Set(entity.topic);
|
||||
active_model.description = ActiveValue::Set(entity.description);
|
||||
active_model.scheduled_at = ActiveValue::Set(entity.scheduled_at);
|
||||
active_model.duration_minutes = ActiveValue::Set(entity.duration_minutes);
|
||||
active_model.meeting_link = ActiveValue::Set(entity.meeting_link);
|
||||
active_model.session_type = ActiveValue::Set(entity.session_type);
|
||||
active_model.status = ActiveValue::Set(entity.status);
|
||||
active_model.feedback = ActiveValue::Set(entity.feedback);
|
||||
active_model.rating = ActiveValue::Set(entity.rating);
|
||||
active_model.feedback_submitted_at = ActiveValue::Set(entity.feedback_submitted_at);
|
||||
active_model.updated_at = ActiveValue::Set(entity.updated_at);
|
||||
|
||||
let updated: SessionModel = active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(model_to_entity(updated))
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
model
|
||||
.into_active_model()
|
||||
.delete(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn count_by_mentor(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id));
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let count = query
|
||||
.count(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
async fn count_by_mentee(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MenteeId.eq(mentee_id));
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let count = query
|
||||
.count(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
async fn find_all_paginated(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<SessionEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let query = match params.sort_direction {
|
||||
Some(SortDirection::Asc) => SessionsEntity::find()
|
||||
.order_by(SessionColumn::CreatedAt, Order::Asc),
|
||||
_ => SessionsEntity::find()
|
||||
.order_by(SessionColumn::CreatedAt, Order::Desc),
|
||||
};
|
||||
|
||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let sessions = paginator
|
||||
.fetch_page((page - 1) as u64)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let data = sessions.into_iter().map(model_to_entity).collect();
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub use infrastructure::http::routes::{sessions_protected_routes, sessions_public_routes};
|
||||
@@ -1,333 +0,0 @@
|
||||
use super::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsService,
|
||||
};
|
||||
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
||||
use ::axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
};
|
||||
use imphnen_entities::MetaRequestDto;
|
||||
use uuid::Uuid;
|
||||
use axum::http::StatusCode;
|
||||
use imphnen_utils::common_response;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::extract_email;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/create",
|
||||
request_body = MentorUserRegisterRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||
(status = 400, description = "[PUBLIC] Bad request - validation error"),
|
||||
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
|
||||
(status = 500, description = "[PUBLIC] Internal server error")
|
||||
),
|
||||
tag = "Mentors"
|
||||
)]
|
||||
pub async fn post_register_mentor(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
MentorsService::register_mentor(&app_state, dto).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors",
|
||||
params(
|
||||
("page" = Option<u64>, Query, description = "Page number"),
|
||||
("per_page" = Option<u64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search query"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_list(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
|
||||
MentorsService::get_mentor_list(&app_state, meta).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_by_id(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
|
||||
MentorsService::get_mentor_by_id(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
|
||||
MentorsService::update_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/mentors/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
|
||||
MentorsService::delete_mentor(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/verify/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorVerifyRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn put_verify_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
|
||||
MentorsService::verify_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_me(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/me/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[MENTOR] Bad request - validation error"),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 404, description = "[MENTOR] Mentor profile not found"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||
})
|
||||
}
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
|
||||
),
|
||||
tag = "Mentors - Admin"
|
||||
)]
|
||||
pub async fn put_update_mentor_no_id() -> Response {
|
||||
imphnen_utils::common_response(
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
"Mentor ID is required for update",
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me/status",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor application status", body = String),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] No mentor application found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_status(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_status(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
@@ -1,438 +0,0 @@
|
||||
use crate::v1::mentors::MentorSchema;
|
||||
use crate::v1::sessions::sessions_schema::Thing;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorListResponseDto {
|
||||
pub id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorDetailWithUserDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
// Personal data is now in UsersSchema, access via user_id
|
||||
// Removed: fullname, email, legal_name, identity_document_url,
|
||||
// phone_for_verification, bio, linkedin_url, github_url, cv_url
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorDetailResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
// Personal data fields from UsersSchema
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
// Professional data from MentorSchema
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorRegisterResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorUpdateRequestDto {
|
||||
#[validate(length(
|
||||
min = 3,
|
||||
message = "Legal name must be at least 3 characters"
|
||||
))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub legal_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[validate(length(
|
||||
min = 10,
|
||||
max = 15,
|
||||
message = "Phone must be 10-15 characters"
|
||||
))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
#[validate(length(min = 50, message = "Bio must be at least 50 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[validate(url(message = "Invalid LinkedIn URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[validate(url(message = "Invalid GitHub URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[validate(url(message = "Invalid CV URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[validate(url(message = "Invalid portfolio URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 industry required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub industries: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 expertise required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expertise: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 language required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub languages: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "Current company required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_company: Option<String>,
|
||||
#[validate(length(min = 1, message = "Current role required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_role: Option<String>,
|
||||
#[validate(range(min = 2, message = "At least 2 years of experience required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub years_of_experience: Option<i32>,
|
||||
#[validate(length(min = 1, message = "At least 1 topic required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub topics_of_interest: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentee level required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentee_level: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentoring format required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentoring_formats: Option<Vec<String>>,
|
||||
#[validate(length(
|
||||
min = 5,
|
||||
message = "Availability commitment must be at least 5 characters"
|
||||
))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub availability_commitment: Option<String>,
|
||||
#[validate(range(min = 1, message = "Amount must be at least 1"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentoring_rate_amount: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorUserRegisterRequestDto {
|
||||
#[validate(
|
||||
length(min = 1, message = "Email cannot be empty"),
|
||||
email(message = "Email not valid")
|
||||
)]
|
||||
pub email: String,
|
||||
#[validate(length(
|
||||
min = 8,
|
||||
message = "Password must have at least 8 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "imphnen_iam::v1::auth::auth_dto::validate_password_complexity",
|
||||
message = "Password must include uppercase, lowercase, number, and special character"
|
||||
))]
|
||||
pub password: String,
|
||||
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||
pub fullname: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
#[validate(nested)]
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
#[validate(nested)]
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
#[validate(nested)]
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorRegisterFromTokenRequestDto {
|
||||
#[validate(nested)]
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
#[validate(nested)]
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
#[validate(nested)]
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct IdentityAndVerification {
|
||||
#[validate(length(
|
||||
min = 3,
|
||||
message = "Legal name must be at least 3 characters"
|
||||
))]
|
||||
pub legal_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[validate(url(message = "Invalid identity document URL"))]
|
||||
pub identity_document_url: String,
|
||||
#[validate(length(
|
||||
min = 10,
|
||||
max = 15,
|
||||
message = "Phone must be 10-15 characters"
|
||||
))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct ProfessionalProfile {
|
||||
#[validate(length(min = 50, message = "Bio must be at least 50 characters"))]
|
||||
pub bio: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[validate(url(message = "Invalid LinkedIn URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[validate(url(message = "Invalid GitHub URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[validate(url(message = "Invalid CV URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 industry required"))]
|
||||
pub industries: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 expertise required"))]
|
||||
pub expertise: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 language required"))]
|
||||
pub languages: Vec<String>,
|
||||
#[validate(length(min = 1, message = "Current company required"))]
|
||||
pub current_company: String,
|
||||
#[validate(length(min = 1, message = "Current role required"))]
|
||||
pub current_role: String,
|
||||
#[validate(range(min = 2, message = "At least 2 years of experience required"))]
|
||||
pub years_of_experience: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentoringLogistics {
|
||||
#[validate(length(min = 1, message = "At least 1 topic required"))]
|
||||
pub topics_of_interest: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentee level required"))]
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentoring format required"))]
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
#[validate(length(
|
||||
min = 5,
|
||||
message = "Availability commitment must be at least 5 characters"
|
||||
))]
|
||||
pub availability_commitment: String,
|
||||
#[validate(range(min = 1, message = "Amount must be at least 1"))]
|
||||
pub mentoring_rate_amount: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate, Default)]
|
||||
pub struct MentoringRate {
|
||||
#[validate(range(min = 1, message = "Amount must be at least 1"))]
|
||||
pub amount: u64,
|
||||
#[validate(length(min = 1, message = "Currency is required"))]
|
||||
pub currency: String,
|
||||
#[validate(length(min = 1, message = "Per duration is required"))]
|
||||
pub per_duration: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorInsertDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Option<Thing>,
|
||||
// Personal data removed - now stored in UsersSchema
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<MentorSchema> for MentorInsertDto {
|
||||
fn from(schema: MentorSchema) -> Self {
|
||||
MentorInsertDto {
|
||||
id: schema.id,
|
||||
user_id: schema.user_id,
|
||||
// Personal data removed from MentorSchema
|
||||
industries: schema.industries,
|
||||
expertise: schema.expertise,
|
||||
languages: schema.languages,
|
||||
current_company: schema.current_company,
|
||||
current_role: schema.current_role,
|
||||
years_of_experience: schema.years_of_experience,
|
||||
topics_of_interest: schema.topics_of_interest,
|
||||
preferred_mentee_level: schema.preferred_mentee_level,
|
||||
preferred_mentoring_formats: schema.preferred_mentoring_formats,
|
||||
availability_commitment: schema.availability_commitment,
|
||||
mentoring_rate: schema.mentoring_rate,
|
||||
status: schema.status,
|
||||
is_deleted: schema.is_deleted,
|
||||
created_at: schema.created_at,
|
||||
updated_at: schema.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorVerifyRequestDto {
|
||||
#[validate(length(min = 1, message = "Status is required"))]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorDetailQueryDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
// Personal data has been moved to UsersSchema
|
||||
// Use user_id to get: fullname, email, legal_name, gender, domicile,
|
||||
// identity_document_url, phone_for_verification, bio, last_education,
|
||||
// linkedin_url, github_url, cv_url, portfolio_url
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.clone(),
|
||||
fullname: None, // now in user table, must be populated from service layer
|
||||
email: None, // now in user table, must be populated from service layer
|
||||
status: dto.status,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.clone(),
|
||||
user_id: dto.user_id.clone(),
|
||||
// Personal data fields are populated in service layer from UsersSchema
|
||||
fullname: None, // populated from user table in service layer
|
||||
email: None, // populated from user table in service layer
|
||||
legal_name: None, // populated from user table in service layer
|
||||
gender: None, // populated from user table in service layer
|
||||
domicile: None, // populated from user table in service layer
|
||||
phone_for_verification: None, // populated from user table in service layer
|
||||
bio: None, // populated from user table in service layer
|
||||
last_education: None, // populated from user table in service layer
|
||||
linkedin_url: None, // populated from user table in service layer
|
||||
github_url: None, // populated from user table in service layer
|
||||
cv_url: None, // populated from user table in service layer
|
||||
portfolio_url: None, // populated from user table in service layer
|
||||
// Professional data from mentor
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
current_company: dto.current_company,
|
||||
current_role: dto.current_role,
|
||||
years_of_experience: dto.years_of_experience,
|
||||
topics_of_interest: dto.topics_of_interest,
|
||||
preferred_mentee_level: dto.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto.preferred_mentoring_formats,
|
||||
availability_commitment: dto.availability_commitment,
|
||||
mentoring_rate: dto.mentoring_rate,
|
||||
status: dto.status,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<MentorSchema> for MentorRegisterResponseDto {
|
||||
fn from(schema: MentorSchema) -> Self {
|
||||
Self {
|
||||
id: schema.id.to_string(),
|
||||
user_id: schema.user_id.unwrap_or_default(),
|
||||
email: None, // schema.email - now in user table
|
||||
status: schema.status,
|
||||
created_at: schema.created_at,
|
||||
updated_at: schema.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MentorDetailWithUserDto> for MentorDetailQueryDto {
|
||||
fn from(dto: MentorDetailWithUserDto) -> Self {
|
||||
MentorDetailQueryDto {
|
||||
id: dto.id,
|
||||
user_id: dto.user_id,
|
||||
// Personal data removed from MentorDetailWithUserDto - now in UsersSchema
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
current_company: dto.current_company,
|
||||
current_role: dto.current_role,
|
||||
years_of_experience: dto.years_of_experience,
|
||||
topics_of_interest: dto.topics_of_interest,
|
||||
preferred_mentee_level: dto.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto.preferred_mentoring_formats,
|
||||
availability_commitment: dto.availability_commitment,
|
||||
mentoring_rate: dto.mentoring_rate,
|
||||
status: dto.status,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
use crate::v1::mentors::mentors_schema::MentorSchema;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_libs::AppStatePostgresExt;
|
||||
use imphnen_entities::seaorm::auth::mentors::{Entity as Mentors, ActiveModel as MentorActiveModel, Column as MentorColumn};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as Users;
|
||||
use anyhow::{Result, bail};
|
||||
use sea_orm::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use uuid::Uuid;
|
||||
use sea_orm::ActiveModelTrait as ActiveModelTraitSpecific;
|
||||
use anyhow::anyhow;
|
||||
use imphnen_entities::{MetaRequestDto, ResponseListSuccessDto};
|
||||
use imphnen_utils::Result as UtilsResult;
|
||||
use crate::v1::mentors::mentors_dto::MentorDetailQueryDto;
|
||||
use std::time::Instant;
|
||||
use tracing::{instrument, info};
|
||||
use serde_json;
|
||||
use chrono::Utc;
|
||||
|
||||
pub struct MentorsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> MentorsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
fn get_db(&self) -> &DatabaseConnection {
|
||||
self.state.postgres_db()
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mentor_active_model = MentorActiveModel {
|
||||
user_id: ActiveValue::Set(Uuid::parse_str(&data.user_id.unwrap_or_default())?),
|
||||
industries: ActiveValue::Set(Some(data.industries.into())),
|
||||
expertise: ActiveValue::Set(Some(data.expertise.into())),
|
||||
languages: ActiveValue::Set(Some(data.languages.into())),
|
||||
current_company: ActiveValue::Set(Some(data.current_company)),
|
||||
current_role: ActiveValue::Set(Some(data.current_role)),
|
||||
years_of_experience: ActiveValue::Set(Some(data.years_of_experience)),
|
||||
topics_of_interest: ActiveValue::Set(Some(data.topics_of_interest.into())),
|
||||
preferred_mentee_level: ActiveValue::Set(Some(serde_json::to_string(&data.preferred_mentee_level).unwrap())),
|
||||
preferred_mentoring_formats: ActiveValue::Set(Some(data.preferred_mentoring_formats.into())),
|
||||
availability_commitment: ActiveValue::Set(Some(data.availability_commitment)),
|
||||
mentoring_rate: ActiveValue::Set(Some(data.mentoring_rate)),
|
||||
status: ActiveValue::Set(Some(data.status)),
|
||||
created_at: ActiveValue::Set(Utc::now()),
|
||||
updated_at: ActiveValue::Set(Utc::now()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
info!("Executing PostgreSQL create in query_create_mentor");
|
||||
let result = <MentorActiveModel as sea_orm::ActiveModelTrait>::insert(mentor_active_model, self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_create_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result.id.to_string())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mentor_id = Uuid::parse_str(&data.id)?;
|
||||
let existing_mentor = Mentors::find_by_id(mentor_id)
|
||||
.one(self.get_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
let mut mentor_active_model: MentorActiveModel = existing_mentor.into();
|
||||
|
||||
if !data.industries.is_empty() { mentor_active_model.industries = ActiveValue::Set(Some(serde_json::to_value(data.industries).unwrap())); }
|
||||
if !data.expertise.is_empty() { mentor_active_model.expertise = ActiveValue::Set(Some(serde_json::to_value(data.expertise).unwrap())); }
|
||||
if !data.languages.is_empty() { mentor_active_model.languages = ActiveValue::Set(Some(serde_json::to_value(data.languages).unwrap())); }
|
||||
if !data.current_company.is_empty() { mentor_active_model.current_company = ActiveValue::Set(Some(data.current_company)); }
|
||||
if !data.current_role.is_empty() { mentor_active_model.current_role = ActiveValue::Set(Some(data.current_role)); }
|
||||
mentor_active_model.years_of_experience = ActiveValue::Set(Some(data.years_of_experience));
|
||||
if !data.topics_of_interest.is_empty() { mentor_active_model.topics_of_interest = ActiveValue::Set(Some(serde_json::to_value(data.topics_of_interest).unwrap())); }
|
||||
if !data.preferred_mentee_level.is_empty() { mentor_active_model.preferred_mentee_level = ActiveValue::Set(Some(serde_json::to_string(&data.preferred_mentee_level).unwrap())); }
|
||||
if !data.preferred_mentoring_formats.is_empty() { mentor_active_model.preferred_mentoring_formats = ActiveValue::Set(Some(serde_json::to_value(data.preferred_mentoring_formats).unwrap())); }
|
||||
if !data.availability_commitment.is_empty() { mentor_active_model.availability_commitment = ActiveValue::Set(Some(data.availability_commitment)); }
|
||||
mentor_active_model.mentoring_rate = ActiveValue::Set(Some(data.mentoring_rate));
|
||||
if !data.status.is_empty() { mentor_active_model.status = ActiveValue::Set(Some(data.status)); }
|
||||
mentor_active_model.updated_at = ActiveValue::Set(Utc::now());
|
||||
|
||||
info!("Executing PostgreSQL update in query_update_mentor");
|
||||
let result = mentor_active_model.update(self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_update_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(format!("Success update mentor: {}", result.id))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_mentor(&self, id: &str) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mentor = Mentors::find_by_id(Uuid::parse_str(id)?)
|
||||
.one(self.get_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
if mentor.is_deleted {
|
||||
bail!("Mentor is already soft deleted");
|
||||
}
|
||||
|
||||
let mut mentor_active_model: MentorActiveModel = mentor.into();
|
||||
mentor_active_model.is_deleted = ActiveValue::Set(true);
|
||||
mentor_active_model.updated_at = ActiveValue::Set(Utc::now());
|
||||
|
||||
info!("Executing PostgreSQL soft delete in query_delete_mentor");
|
||||
let result = mentor_active_model.update(self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_delete_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(format!("Success soft delete mentor: {}", result.id))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, email, include_deleted), err)]
|
||||
pub async fn query_mentor_by_email(
|
||||
&self,
|
||||
email: String,
|
||||
include_deleted: bool,
|
||||
) -> UtilsResult<MentorDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let mut query = Mentors::find()
|
||||
.find_also_related(Users);
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let (mentor, user) = query
|
||||
.filter(imphnen_entities::seaorm::auth::users::Column::Email.eq(email))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Mentor not found"))?;
|
||||
|
||||
let _user = user.ok_or_else(|| anyhow::anyhow!("User not found for mentor"))?;
|
||||
|
||||
let result = MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_by_email' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_mentor_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> UtilsResult<ResponseListSuccessDto<Vec<MentorDetailQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(10);
|
||||
|
||||
let mut query = Mentors::find()
|
||||
.filter(MentorColumn::IsDeleted.eq(false))
|
||||
.find_also_related(Users);
|
||||
|
||||
// Apply sorting
|
||||
if let Some(sort_by) = &meta.sort_by {
|
||||
let order = meta.order.as_deref().unwrap_or("asc");
|
||||
match sort_by.as_str() {
|
||||
"created_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(MentorColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
"updated_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(MentorColumn::UpdatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(MentorColumn::UpdatedAt);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
}
|
||||
|
||||
let paginator = query.paginate(db, per_page);
|
||||
let total_pages = paginator.num_pages().await?;
|
||||
let mentors = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
let data: Vec<MentorDetailQueryDto> = mentors
|
||||
.into_iter()
|
||||
.filter_map(|(mentor, user)| {
|
||||
user.map(|_u| MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_list' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let response = ResponseListSuccessDto {
|
||||
data,
|
||||
meta: Some(imphnen_entities::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total_pages),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, include_deleted), err)]
|
||||
pub async fn query_mentor_by_id(
|
||||
&self,
|
||||
id: &str,
|
||||
include_deleted: bool,
|
||||
) -> UtilsResult<MentorDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let mentor_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let mut query = Mentors::find_by_id(mentor_id)
|
||||
.find_also_related(Users);
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let (mentor, user) = query
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Mentor not found"))?;
|
||||
|
||||
let _user = user.ok_or_else(|| anyhow::anyhow!("User not found for mentor"))?;
|
||||
|
||||
let result = MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
use super::{
|
||||
MentorDetailQueryDto, MentorUpdateRequestDto,
|
||||
MentoringLogistics, ProfessionalProfile,
|
||||
};
|
||||
use crate::v1::sessions::sessions_schema::Thing;
|
||||
use imphnen_entities::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorSchema {
|
||||
pub id: Thing,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<Thing>,
|
||||
// Personal data has been moved to UsersSchema - use user_id to reference
|
||||
// phone_for_verification, bio, last_education, linkedin_url, github_url,
|
||||
// cv_url, portfolio_url
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for MentorSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
ResourceEnum::Mentors.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user_id: Some(make_thing(
|
||||
ResourceEnum::Users.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
)),
|
||||
industries: Vec::new(),
|
||||
expertise: Vec::new(),
|
||||
languages: Vec::new(),
|
||||
current_company: String::new(),
|
||||
current_role: String::new(),
|
||||
years_of_experience: 0,
|
||||
topics_of_interest: Vec::new(),
|
||||
preferred_mentee_level: Vec::new(),
|
||||
preferred_mentoring_formats: Vec::new(),
|
||||
availability_commitment: String::new(),
|
||||
mentoring_rate: 0.0,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MentorSchema {
|
||||
pub fn create(
|
||||
professional_profile: ProfessionalProfile,
|
||||
mentoring_logistics: MentoringLogistics,
|
||||
user_id_raw: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Mentors.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)),
|
||||
// Personal data now stored in UsersSchema, not here
|
||||
industries: professional_profile.industries,
|
||||
expertise: professional_profile.expertise,
|
||||
languages: professional_profile.languages,
|
||||
current_company: professional_profile.current_company,
|
||||
current_role: professional_profile.current_role,
|
||||
years_of_experience: professional_profile.years_of_experience,
|
||||
topics_of_interest: mentoring_logistics.topics_of_interest,
|
||||
preferred_mentee_level: mentoring_logistics.preferred_mentee_level,
|
||||
preferred_mentoring_formats: mentoring_logistics.preferred_mentoring_formats,
|
||||
availability_commitment: mentoring_logistics.availability_commitment,
|
||||
mentoring_rate: mentoring_logistics.mentoring_rate_amount as f64,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user_id: Some(dto.user_id),
|
||||
// Personal data now comes from UsersSchema via user_id
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
current_company: dto.current_company,
|
||||
current_role: dto.current_role,
|
||||
years_of_experience: dto.years_of_experience,
|
||||
topics_of_interest: dto.topics_of_interest,
|
||||
preferred_mentee_level: dto.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto.preferred_mentoring_formats,
|
||||
availability_commitment: dto.availability_commitment,
|
||||
mentoring_rate: dto.mentoring_rate,
|
||||
status: dto.status,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self {
|
||||
// phone_for_verification, bio, last_education, linkedin_url, github_url,
|
||||
// cv_url, portfolio_url) are now updated in UsersSchema, not here
|
||||
|
||||
// Only update professional fields that are still in MentorSchema
|
||||
if let Some(val) = dto.industries {
|
||||
self.industries = val;
|
||||
}
|
||||
if let Some(val) = dto.expertise {
|
||||
self.expertise = val;
|
||||
}
|
||||
if let Some(val) = dto.languages {
|
||||
self.languages = val;
|
||||
}
|
||||
if let Some(val) = dto.current_company {
|
||||
self.current_company = val;
|
||||
}
|
||||
if let Some(val) = dto.current_role {
|
||||
self.current_role = val;
|
||||
}
|
||||
if let Some(val) = dto.years_of_experience {
|
||||
self.years_of_experience = val;
|
||||
}
|
||||
if let Some(val) = dto.topics_of_interest {
|
||||
self.topics_of_interest = val;
|
||||
}
|
||||
if let Some(val) = dto.preferred_mentee_level {
|
||||
self.preferred_mentee_level = val;
|
||||
}
|
||||
if let Some(val) = dto.preferred_mentoring_formats {
|
||||
self.preferred_mentoring_formats = val;
|
||||
}
|
||||
if let Some(val) = dto.availability_commitment {
|
||||
self.availability_commitment = val;
|
||||
}
|
||||
if let Some(val) = dto.mentoring_rate_amount {
|
||||
self.mentoring_rate = val as f64;
|
||||
}
|
||||
|
||||
self.updated_at = get_iso_date();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn update_status(mut self, status: String) -> Self {
|
||||
self.status = status;
|
||||
self.updated_at = get_iso_date();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
use crate::v1::mentors::{
|
||||
MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorRegisterResponseDto, MentorSchema, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsRepository,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
use imphnen_entities::{
|
||||
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_iam::{
|
||||
v1::auth::AuthRepository,
|
||||
RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||
};
|
||||
use imphnen_libs::argon::hash_password;
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validator::validate_request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use tracing::error;
|
||||
|
||||
pub struct MentorsService;
|
||||
|
||||
impl MentorsService {
|
||||
pub async fn register_mentor(
|
||||
state: &AppState,
|
||||
dto: MentorUserRegisterRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let role_repo = RolesRepository::new(state);
|
||||
let _auth_repo = AuthRepository::new(state);
|
||||
|
||||
let user_email = &dto.email;
|
||||
let mut _user_to_update: Option<UsersSchema> = None;
|
||||
|
||||
let existing_user_result =
|
||||
user_repo.query_user_by_email(user_email.clone()).await;
|
||||
|
||||
let user_id;
|
||||
let final_user_email = user_email.clone();
|
||||
|
||||
if let Ok(user_detail_query_dto) = existing_user_result {
|
||||
if mentor_repo
|
||||
.query_mentor_by_email(user_email.clone(), false)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return common_response(
|
||||
StatusCode::CONFLICT,
|
||||
"Mentor profile already exists for this user",
|
||||
);
|
||||
}
|
||||
|
||||
let mut user_schema = UsersSchema::from(user_detail_query_dto);
|
||||
|
||||
user_schema.fullname = Some(dto.fullname.clone());
|
||||
// Update profile_extension fields
|
||||
let mut profile_ext = user_schema.profile_extension.clone().unwrap_or_default();
|
||||
profile_ext.phone_number = dto.phone_number.clone();
|
||||
profile_ext.phone_for_verification = dto.identity_and_verification.phone_for_verification.clone();
|
||||
profile_ext.gender = dto.identity_and_verification.gender.clone();
|
||||
profile_ext.domicile = dto.identity_and_verification.domicile.clone();
|
||||
profile_ext.bio = Some(dto.professional_profile.bio.clone());
|
||||
profile_ext.last_education = dto.professional_profile.last_education.clone();
|
||||
profile_ext.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
profile_ext.github_url = dto.professional_profile.github_url.clone();
|
||||
profile_ext.cv_url = dto.professional_profile.cv_url.clone();
|
||||
profile_ext.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
user_schema.profile_extension = Some(profile_ext);
|
||||
user_schema.updated_at = imphnen_utils::get_iso_date();
|
||||
|
||||
let hashed_password = match hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to hash password during update for {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
);
|
||||
}
|
||||
};
|
||||
user_schema.password = Some(hashed_password);
|
||||
|
||||
let mentor_role = match role_repo
|
||||
.query_role_by_name(RolesEnum::Mentor.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(role) => role,
|
||||
Err(_e) => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
|
||||
}
|
||||
};
|
||||
user_schema.mentor_id = Some(imphnen_utils::make_thing_from_enum("Roles", &mentor_role.id));
|
||||
user_schema.is_active = false;
|
||||
|
||||
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
|
||||
error!(
|
||||
"Failed to update existing user {} to mentor role: {}",
|
||||
final_user_email, _err
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
user_id = user_schema.id.clone();
|
||||
} else {
|
||||
let mentor_role = match role_repo
|
||||
.query_role_by_name(RolesEnum::Mentor.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(role) => role,
|
||||
Err(_e) => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
|
||||
}
|
||||
};
|
||||
|
||||
let hashed_password = match hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to hash password for new user {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_user_schema = UsersSchema {
|
||||
id: imphnen_utils::make_thing_from_enum(
|
||||
"Users",
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
email: Some(dto.email),
|
||||
fullname: Some(dto.fullname),
|
||||
password: Some(hashed_password),
|
||||
// Set phone number in profile extension instead
|
||||
// Store personal data from identity_and_verification in user
|
||||
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||
// Use profile_extension for these fields
|
||||
// Store personal data from professional_profile in user
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
mentor_id: Some(imphnen_utils::make_thing_from_enum(
|
||||
"Roles",
|
||||
&mentor_role.id,
|
||||
)),
|
||||
is_active: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// populate profile_extension
|
||||
let mut profile_ext = new_user_schema.profile_extension.clone().unwrap_or_default();
|
||||
profile_ext.phone_number = dto.phone_number.clone();
|
||||
profile_ext.phone_for_verification = dto.identity_and_verification.phone_for_verification.clone();
|
||||
profile_ext.gender = dto.identity_and_verification.gender.clone();
|
||||
profile_ext.domicile = dto.identity_and_verification.domicile.clone();
|
||||
profile_ext.bio = Some(dto.professional_profile.bio.clone());
|
||||
profile_ext.last_education = dto.professional_profile.last_education.clone();
|
||||
profile_ext.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
profile_ext.github_url = dto.professional_profile.github_url.clone();
|
||||
profile_ext.cv_url = dto.professional_profile.cv_url.clone();
|
||||
profile_ext.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
new_user_schema.profile_extension = Some(profile_ext);
|
||||
user_id = new_user_schema.id.clone();
|
||||
|
||||
match user_repo.query_create_user(new_user_schema).await {
|
||||
Ok(_) => {}
|
||||
Err(_err) => {
|
||||
error!("Failed to create new user {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip OTP for now - implement later if needed
|
||||
|
||||
let mentor_schema = MentorSchema::create(
|
||||
dto.professional_profile,
|
||||
dto.mentoring_logistics,
|
||||
user_id.clone(),
|
||||
);
|
||||
|
||||
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
||||
Ok(mentor_profile_id) => {
|
||||
let user_after_mentor_creation_dto = user_repo
|
||||
.query_user_by_email(final_user_email.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut user_after_mentor_creation_schema =
|
||||
UsersSchema::from(user_after_mentor_creation_dto);
|
||||
user_after_mentor_creation_schema = user_after_mentor_creation_schema
|
||||
.update_mentor_id(Some(mentor_profile_id));
|
||||
if let Err(_e) = user_repo
|
||||
.query_update_user(user_after_mentor_creation_schema)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to update user's mentor_id for {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_e.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let response_dto = MentorRegisterResponseDto::from(mentor_schema);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to create mentor profile for {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let user_repo = UsersRepository::new(state);
|
||||
|
||||
match repo.query_mentor_list(meta).await {
|
||||
Ok(result) => {
|
||||
let mut mentor_list_data: Vec<MentorListResponseDto> = Vec::new();
|
||||
|
||||
for mentor_with_user in result.data {
|
||||
let mentor_dto = mentor_with_user;
|
||||
let mut list_item = MentorListResponseDto::from(mentor_dto.clone());
|
||||
|
||||
// Get user data to populate personal fields
|
||||
if let Ok(user) = user_repo.query_user_by_id(&mentor_dto.user_id).await {
|
||||
list_item.fullname = Some(user.fullname);
|
||||
list_item.email = Some(user.email);
|
||||
}
|
||||
|
||||
mentor_list_data.push(list_item);
|
||||
}
|
||||
|
||||
success_list_response(ResponseListSuccessDto {
|
||||
data: mentor_list_data,
|
||||
meta: result.meta,
|
||||
})
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let thing_id = imphnen_utils::make_thing_from_enum("Mentors", id); match mentor_repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => {
|
||||
// Get user data separately
|
||||
let user_result = user_repo.query_user_by_id(&mentor.user_id).await;
|
||||
match user_result {
|
||||
Ok(user) => {
|
||||
// Combine mentor and user data
|
||||
let dto = MentorDetailResponseDto {
|
||||
id: mentor.id.clone(),
|
||||
user_id: mentor.user_id.clone(),
|
||||
// Personal data from user
|
||||
fullname: Some(user.fullname),
|
||||
email: Some(user.email),
|
||||
legal_name: user.legal_name,
|
||||
gender: user.profile_extension.as_ref().and_then(|ext| ext.gender.clone()),
|
||||
domicile: user.profile_extension.as_ref().and_then(|ext| ext.domicile.clone()),
|
||||
phone_for_verification: user.profile_extension.as_ref().and_then(|ext| ext.phone_for_verification.clone()),
|
||||
bio: user.profile_extension.as_ref().and_then(|ext| ext.bio.clone()),
|
||||
last_education: user.profile_extension.as_ref().and_then(|ext| ext.last_education.clone()),
|
||||
linkedin_url: user.profile_extension.as_ref().and_then(|ext| ext.linkedin_url.clone()),
|
||||
github_url: user.profile_extension.as_ref().and_then(|ext| ext.github_url.clone()),
|
||||
cv_url: user.profile_extension.as_ref().and_then(|ext| ext.cv_url.clone()),
|
||||
portfolio_url: user.profile_extension.as_ref().and_then(|ext| ext.portfolio_url.clone()),
|
||||
// Professional data from mentor
|
||||
industries: mentor.industries,
|
||||
expertise: mentor.expertise,
|
||||
languages: mentor.languages,
|
||||
current_company: mentor.current_company,
|
||||
current_role: mentor.current_role,
|
||||
years_of_experience: mentor.years_of_experience,
|
||||
topics_of_interest: mentor.topics_of_interest,
|
||||
preferred_mentee_level: mentor.preferred_mentee_level,
|
||||
preferred_mentoring_formats: mentor.preferred_mentoring_formats,
|
||||
availability_commitment: mentor.availability_commitment,
|
||||
mentoring_rate: mentor.mentoring_rate,
|
||||
status: mentor.status,
|
||||
created_at: mentor.created_at,
|
||||
updated_at: mentor.updated_at,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(_e) => {
|
||||
error!("Failed to get user data for mentor {}: {}", id, _e);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get mentor user data")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_mentor(
|
||||
state: &AppState,
|
||||
id: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = MentorsRepository::new(state);
|
||||
let mentor_uuid = Uuid::parse_str(id).map_err(|_| {
|
||||
common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format"
|
||||
)
|
||||
}).unwrap();
|
||||
let existing_mentor = match repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(id, false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_mentor(state: &AppState, id: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_delete_mentor(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_me(state: &AppState, email: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => {
|
||||
let dto = MentorDetailResponseDto::from(mentor);
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(_e) => common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Mentor profile not found for current user",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_mentor_me(
|
||||
state: &AppState,
|
||||
email: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = MentorsRepository::new(state);
|
||||
let existing_mentor =
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::FORBIDDEN, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor = repo
|
||||
.query_mentor_by_email(email.to_string(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_status(state: &AppState, email: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => common_response(StatusCode::OK, &mentor.status),
|
||||
Err(_e) => common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"No mentor application found for current user",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_mentor(
|
||||
state: &AppState,
|
||||
id: &str,
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let mentor_uuid = Uuid::parse_str(id).map_err(|_| {
|
||||
common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format"
|
||||
)
|
||||
}).unwrap();
|
||||
let existing_mentor = match repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update_status(dto.status);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user