Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b51a56dee | ||
|
|
cb98440b65 | ||
|
|
3c19d262f3 | ||
|
|
bdffe85544 | ||
|
|
9ed619e3fd | ||
|
|
00fd7e1907 | ||
|
|
b70126734e | ||
|
|
6af9461079 | ||
|
|
57b3662951 | ||
|
|
c0c08f2821 | ||
|
|
8402415649 | ||
|
|
01e20baa45 | ||
|
|
10dc869eaf | ||
|
|
199d0c885e | ||
|
|
89ef48a57e | ||
|
|
da1826ebba | ||
|
|
874986f1d6 | ||
|
|
bbff555a06 | ||
|
|
d3eb687a58 | ||
|
|
c5516d4006 | ||
|
|
5f51dd281d | ||
|
|
f6232832af | ||
|
|
277cf0770d | ||
|
|
650728a385 | ||
|
|
3cc3e2a7d8 | ||
|
|
39fc2f86ad | ||
|
|
7bd0ef2274 | ||
|
|
b40a430c49 | ||
|
|
44b1e09551 | ||
|
|
1b9251b2dd | ||
|
|
34b6cdabfb | ||
|
|
a25e896fa2 | ||
|
|
adf3ef8e28 | ||
|
|
ad46687f07 | ||
|
|
0ba04abee9 | ||
|
|
1e25a5b496 | ||
|
|
d7178c792d | ||
|
|
d2754de9ef | ||
|
|
cda950ed7e | ||
|
|
c6a95c1231 | ||
|
|
86116b8157 | ||
|
|
ac06224ed1 | ||
|
|
83e765f358 | ||
|
|
558e0ce568 | ||
|
|
148cf3a5cd | ||
|
|
ee9d23e01b |
+32
-7
@@ -1,4 +1,30 @@
|
|||||||
|
# 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_ENV=development
|
||||||
|
RUST_LOG=debug
|
||||||
PORT=4099
|
PORT=4099
|
||||||
SURREALDB_URL=ws://localhost:8000/rpc
|
SURREALDB_URL=ws://localhost:8000/rpc
|
||||||
SURREALDB_USERNAME=root
|
SURREALDB_USERNAME=root
|
||||||
@@ -15,10 +41,9 @@ REDISDB_URL=localhost
|
|||||||
FE_URL=http://localhost
|
FE_URL=http://localhost
|
||||||
MINIO_ENDPOINT=http://localhost:9000
|
MINIO_ENDPOINT=http://localhost:9000
|
||||||
MINIO_BUCKET_NAME=default_bucket
|
MINIO_BUCKET_NAME=default_bucket
|
||||||
MINIO_ACCESS_KEY=your-minio-access-key
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
MINIO_SECRET_KEY=your-minio-secret-key
|
MINIO_SECRET_KEY=minioadmin
|
||||||
MAIL_USER=your-email@example.com
|
MINIO_SECURE=false
|
||||||
MAIL_PASSWORD=your-smtp-password
|
|
||||||
MAIL_HOST=smtp.gmail.com
|
GOOGLE_CLIENT_ID="your_google_client_id"
|
||||||
MAIL_PORT=465
|
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
||||||
MAIL_SECURE=true
|
|
||||||
Generated
+514
-218
File diff suppressed because it is too large
Load Diff
+38
-27
@@ -16,47 +16,58 @@ members = [
|
|||||||
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
oauth2 = "5.0.0"
|
||||||
|
reqwest = { version = "0.12.23", features = ["json"] }
|
||||||
|
serde_json = "1.0.142"
|
||||||
axum = { version = "0.8.4", features = ["multipart"] }
|
axum = { version = "0.8.4", features = ["multipart"] }
|
||||||
log = "0.4.25"
|
log = "0.4.27"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.138"
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
tokio = { version = "1.45.0", features = ["full"] }
|
|
||||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||||
jsonwebtoken = "9.3.1"
|
jsonwebtoken = "9.3.1"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] }
|
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||||
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.14"
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.99"
|
||||||
rand = { version = "0.9.1", features = ["std", "alloc"] }
|
rand = { version = "0.9.2", features = ["std", "alloc"] }
|
||||||
rand_distr = "0.5.1"
|
rand_distr = "0.5.1"
|
||||||
tower-http = { version = "0.6.4", features = ["cors", "trace"] }
|
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
|
||||||
http-body-util = "0.1.1"
|
http-body-util = "0.1.3"
|
||||||
|
|
||||||
validator = { version = "0.20.0", features = ["derive"] }
|
validator = { version = "0.20.0", features = ["derive"] }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.5.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
axum-test = "17.2.0"
|
axum-test = "17.3.0"
|
||||||
fancy-regex = "0.15.0"
|
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||||
|
fancy-regex = "0.16.1"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
tower = "0.5.2"
|
tower = "0.5.2"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
dotenvy = "0.15.7"
|
tracing = "0.1.41"
|
||||||
tracing = "0.1.40"
|
uuid = { version = "1.18.0", features = ["v4", "fast-rng", "serde"] }
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
strum = { version = "0.27.2", features = ["derive"] }
|
||||||
once_cell = "1.21.3"
|
strum_macros = "0.27.2"
|
||||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] }
|
base64 = "0.22.1"
|
||||||
strum = { version = "0.27.1", features = ["derive"] }
|
sha2 = "0.10.9"
|
||||||
strum_macros = "0.27.1"
|
hmac = "0.12"
|
||||||
|
hex = "0.4"
|
||||||
|
urlencoding = "2.1"
|
||||||
hyper = "1.6.0"
|
hyper = "1.6.0"
|
||||||
hyper-util = "0.1.0"
|
hyper-util = "0.1.16"
|
||||||
async-trait = "0.1.75"
|
minio = "0.3.0"
|
||||||
tokio-tungstenite = "0.23"
|
|
||||||
|
|
||||||
|
tokio-test = "0.4.4"
|
||||||
|
mockall = "0.13.1"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
|
tokio-tungstenite = "0.27"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
http = "1.3"
|
||||||
tests = { path = "./tests" }
|
tests = { path = "./tests" }
|
||||||
imphnen-iam = { path = "./imphnen-iam" }
|
imphnen-iam = { path = "./imphnen-iam" }
|
||||||
imphnen-cms = { path = "./imphnen-cms" }
|
imphnen-cms = { path = "./imphnen-cms" }
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script deploy ke VPS
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REMOTE_USER="asephs"
|
||||||
|
REMOTE_HOST="70.153.9.42"
|
||||||
|
REMOTE_PATH="/home/asephs/imphnen-backend-service"
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
taskset -c 0,1 cargo build --release -j 2
|
||||||
|
|
||||||
|
# Rsync hasil build dan file yang diperlukan
|
||||||
|
rsync -avz --delete \
|
||||||
|
target/release/ \
|
||||||
|
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
|
||||||
|
|
||||||
|
# Sync file konfigurasi dan source code (jika perlu)
|
||||||
|
rsync -avz --delete \
|
||||||
|
imphnen-backend/ \
|
||||||
|
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/imphnen-backend/
|
||||||
|
|
||||||
|
rsync -avz --delete \
|
||||||
|
docker-compose.yml Dockerfile \
|
||||||
|
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/
|
||||||
|
|
||||||
|
# Tambahkan file lain jika diperlukan
|
||||||
|
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||||
|
cd /home/asephs/imphnen-backend-service
|
||||||
|
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
|
||||||
|
export PATH=$PATH:/root/.local/share/pnpm
|
||||||
|
export PATH=$PATH:/home/asephs/.nvm/versions/node/v22.17.1/bin
|
||||||
|
export PATH=$PATH:/home/asephs/.bun/bin/bun
|
||||||
|
|
||||||
|
if [ -f ~/.bashrc ]; then
|
||||||
|
source ~/.bashrc
|
||||||
|
fi
|
||||||
|
pm2 restart 4 --update-env
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Deploy selesai ke $REMOTE_HOST:$REMOTE_PATH"
|
||||||
@@ -14,3 +14,19 @@ services:
|
|||||||
command: start --log trace --user root --pass root
|
command: start --log trace --user root --pass root
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: minio
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minio_data:
|
||||||
|
|||||||
@@ -3,6 +3,46 @@ name = "imphnen-backend"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "api"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "clear_db_test"
|
||||||
|
path = "src/bin/clear_db_test.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seeder"
|
||||||
|
path = "src/bin/seeder.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_events"
|
||||||
|
path = "src/bin/seed_events.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_gacha_rolls"
|
||||||
|
path = "src/bin/seed_gacha_rolls.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_mentor_user"
|
||||||
|
path = "src/bin/seed_mentor_user.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_permissions"
|
||||||
|
path = "src/bin/seed_permissions.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_roles"
|
||||||
|
path = "src/bin/seed_roles.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_roles_permissions"
|
||||||
|
path = "src/bin/seed_roles_permissions.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_users"
|
||||||
|
path = "src/bin/seed_users.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
imphnen-libs.workspace = true
|
imphnen-libs.workspace = true
|
||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use axum::Router;
|
||||||
use imphnen_gateway::gateway_service;
|
use imphnen_gateway::gateway_service;
|
||||||
use imphnen_libs::axum_init;
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
@@ -5,7 +6,10 @@ use imphnen_libs::axum_init;
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
let app = gateway_service(surrealdb_ws, surrealdb_mem).await;
|
||||||
|
let mut router = Router::new();
|
||||||
|
router = router.nest("/api/v1/auth", imphnen_iam::v1::auth::auth_router());
|
||||||
|
app.merge(router)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
|
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
|
||||||
|
|
||||||
let (ws_stream, _) = connect_async(url).await?;
|
let (ws_stream, _) = connect_async(url.as_str()).await?;
|
||||||
let (mut write, mut read) = ws_stream.split();
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
||||||
// Authenticate (signin)
|
// Authenticate (signin)
|
||||||
@@ -43,7 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
"id": 1
|
"id": 1
|
||||||
}).to_string();
|
}).to_string();
|
||||||
println!("DEBUG: Sending signin query: {}", signin_query);
|
println!("DEBUG: Sending signin query: {}", signin_query);
|
||||||
write.send(Message::Text(signin_query)).await?;
|
write.send(Message::Text(signin_query.into())).await?;
|
||||||
|
|
||||||
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
|
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
|
||||||
let signin_response_msg = signin_response?;
|
let signin_response_msg = signin_response?;
|
||||||
@@ -60,7 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
"id": 2
|
"id": 2
|
||||||
}).to_string();
|
}).to_string();
|
||||||
println!("DEBUG: Sending use query: {}", use_query);
|
println!("DEBUG: Sending use query: {}", use_query);
|
||||||
write.send(Message::Text(use_query)).await?;
|
write.send(Message::Text(use_query.into())).await?;
|
||||||
|
|
||||||
let use_response = read.next().await.ok_or("Failed to read use response")?;
|
let use_response = read.next().await.ok_or("Failed to read use response")?;
|
||||||
let use_response_msg = use_response?;
|
let use_response_msg = use_response?;
|
||||||
@@ -82,7 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}).to_string();
|
}).to_string();
|
||||||
|
|
||||||
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
|
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
|
||||||
write.send(Message::Text(query_json)).await?;
|
write.send(Message::Text(query_json.into())).await?;
|
||||||
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
|
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}).to_string();
|
}).to_string();
|
||||||
|
|
||||||
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
|
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
|
||||||
write.send(Message::Text(delete_all_json)).await?;
|
write.send(Message::Text(delete_all_json.into())).await?;
|
||||||
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
|
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
|
||||||
|
|
||||||
match delete_response_result {
|
match delete_response_result {
|
||||||
@@ -137,7 +137,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
"params": [select_query],
|
"params": [select_query],
|
||||||
"id": i + 1000
|
"id": i + 1000
|
||||||
}).to_string();
|
}).to_string();
|
||||||
write.send(Message::Text(select_json)).await?;
|
write.send(Message::Text(select_json.into())).await?;
|
||||||
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
|
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
|
||||||
match select_response_result {
|
match select_response_result {
|
||||||
Ok(select_msg) => {
|
Ok(select_msg) => {
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||||
use imphnen_libs::enviroment::load_env;
|
use imphnen_utils::{get_iso_date};
|
||||||
use imphnen_utils::{get_iso_date, Env};
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
load_env();
|
let env = &imphnen_libs::enviroment::ENV;
|
||||||
let env = Env::new();
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
password: &env.surrealdb_password,
|
password: &env.surrealdb_password,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
db.use_ns(env.surrealdb_namespace)
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
.use_db(env.surrealdb_dbname)
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let events = vec![
|
let events = vec![
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
use imphnen_libs::enviroment::load_env;
|
use imphnen_utils::{get_iso_date};
|
||||||
use imphnen_utils::{get_iso_date, Env};
|
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::opt::auth::Root;
|
use surrealdb::opt::auth::Root;
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
load_env();
|
let env = &imphnen_libs::enviroment::ENV;
|
||||||
let env = Env::new();
|
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
@@ -16,8 +13,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
password: &env.surrealdb_password,
|
password: &env.surrealdb_password,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
db.use_ns(env.surrealdb_namespace)
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
.use_db(env.surrealdb_dbname)
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_gacha_items', $id)")
|
db.query("DELETE type::thing('app_gacha_items', $id)")
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
use imphnen_libs::enviroment::load_env;
|
use imphnen_utils::{get_iso_date, hash_password};
|
||||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::opt::auth::Root;
|
use surrealdb::opt::auth::Root;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
load_env();
|
let env = &imphnen_libs::enviroment::ENV;
|
||||||
let env = Env::new();
|
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
@@ -15,8 +13,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
password: &env.surrealdb_password,
|
password: &env.surrealdb_password,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
db.use_ns(env.surrealdb_namespace)
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
.use_db(env.surrealdb_dbname)
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_mentors', $id)")
|
db.query("DELETE type::thing('app_mentors', $id)")
|
||||||
@@ -28,7 +26,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, created_at = $created_at, updated_at = $updated_at")
|
db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, legal_name = $legal_name, domicile = $domicile, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, last_education = $last_education, linkedin_url = $linkedin_url, github_url = $github_url, cv_url = $cv_url, portfolio_url = $portfolio_url, created_at = $created_at, updated_at = $updated_at")
|
||||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||||
.bind(("fullname", "Mentor User"))
|
.bind(("fullname", "Mentor User"))
|
||||||
.bind(("email", "mentor@example.com"))
|
.bind(("email", "mentor@example.com"))
|
||||||
@@ -41,20 +39,23 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
.bind(("gender", "male"))
|
.bind(("gender", "male"))
|
||||||
.bind(("birthdate", "1990-05-15"))
|
.bind(("birthdate", "1990-05-15"))
|
||||||
.bind(("role", Thing::from(("app_roles", "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a"))))
|
.bind(("role", Thing::from(("app_roles", "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a"))))
|
||||||
|
.bind(("legal_name", "Mentor User"))
|
||||||
|
.bind(("domicile", "Jakarta, Indonesia"))
|
||||||
|
// .bind(("identity_document_url", "https://example.com/ktp.jpg"))
|
||||||
|
.bind(("phone_for_verification", "081234567890"))
|
||||||
|
.bind(("bio", "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant."))
|
||||||
|
.bind(("last_education", "S1 Teknik Informatika"))
|
||||||
|
.bind(("linkedin_url", "https://linkedin.com/in/mentor"))
|
||||||
|
.bind(("github_url", "https://github.com/mentor"))
|
||||||
|
.bind(("cv_url", Option::<String>::None))
|
||||||
|
.bind(("portfolio_url", Option::<String>::None))
|
||||||
.bind(("created_at", get_iso_date()))
|
.bind(("created_at", get_iso_date()))
|
||||||
.bind(("updated_at", get_iso_date()))
|
.bind(("updated_at", get_iso_date()))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
db.query("CREATE type::thing('app_mentors', $id) SET user_id = $user_id, legal_name = $legal_name, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, linkedin_url = $linkedin_url, github_url = $github_url, cv_url = $cv_url, industries = $industries, expertise = $expertise, languages = $languages, current_company = $current_company, current_role = $current_role, years_of_experience = $years_of_experience, topics_of_interest = $topics_of_interest, preferred_mentee_level = $preferred_mentee_level, preferred_mentoring_formats = $preferred_mentoring_formats, availability_commitment = $availability_commitment, mentoring_rate = $mentoring_rate, status = $status, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at, email = $email")
|
db.query("CREATE type::thing('app_mentors', $id) SET user_id = $user_id, industries = $industries, expertise = $expertise, languages = $languages, current_company = $current_company, current_role = $current_role, years_of_experience = $years_of_experience, topics_of_interest = $topics_of_interest, preferred_mentee_level = $preferred_mentee_level, preferred_mentoring_formats = $preferred_mentoring_formats, availability_commitment = $availability_commitment, mentoring_rate = $mentoring_rate, status = $status, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||||
.bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))))
|
.bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))))
|
||||||
.bind(("legal_name", "Mentor User"))
|
|
||||||
.bind(("identity_document_url", "https://example.com/ktp.jpg"))
|
|
||||||
.bind(("phone_for_verification", "081234567890"))
|
|
||||||
.bind(("bio", "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant."))
|
|
||||||
.bind(("linkedin_url", "https://linkedin.com/in/mentor"))
|
|
||||||
.bind(("github_url", "https://github.com/mentor"))
|
|
||||||
.bind(("cv_url", Option::<String>::None))
|
|
||||||
.bind(("industries", vec!["Software", "Education"]))
|
.bind(("industries", vec!["Software", "Education"]))
|
||||||
.bind(("expertise", vec!["Rust", "Microservices"]))
|
.bind(("expertise", vec!["Rust", "Microservices"]))
|
||||||
.bind(("languages", vec!["Indonesian", "English"]))
|
.bind(("languages", vec!["Indonesian", "English"]))
|
||||||
@@ -74,7 +75,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
.bind(("is_deleted", false))
|
.bind(("is_deleted", false))
|
||||||
.bind(("created_at", get_iso_date()))
|
.bind(("created_at", get_iso_date()))
|
||||||
.bind(("updated_at", get_iso_date()))
|
.bind(("updated_at", get_iso_date()))
|
||||||
.bind(("email", "mentor@example.com"))
|
|
||||||
.await?;
|
.await?;
|
||||||
println!("Mentor created successfully!");
|
println!("Mentor created successfully!");
|
||||||
println!("Updating user with mentor_id...");
|
println!("Updating user with mentor_id...");
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use imphnen_iam::PermissionsEnum;
|
use imphnen_iam::PermissionsEnum;
|
||||||
use imphnen_libs::enviroment::load_env;
|
use imphnen_utils::{get_iso_date};
|
||||||
use imphnen_utils::{get_iso_date, Env};
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
@@ -8,17 +7,15 @@ use surrealdb::opt::auth::Root;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
load_env();
|
let env = &imphnen_libs::enviroment::ENV;
|
||||||
|
|
||||||
let env = Env::new();
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
password: &env.surrealdb_password,
|
password: &env.surrealdb_password,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
db.use_ns(env.surrealdb_namespace)
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
.use_db(env.surrealdb_dbname)
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for permission in [
|
for permission in [
|
||||||
@@ -54,6 +51,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
PermissionsEnum::ReadOwnMentorProfile,
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
PermissionsEnum::UpdateOwnMentorProfile,
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
PermissionsEnum::ReadOwnMentorStatus,
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
|
PermissionsEnum::UpdateMentors,
|
||||||
PermissionsEnum::VerifyMentors,
|
PermissionsEnum::VerifyMentors,
|
||||||
PermissionsEnum::DeleteMentors,
|
PermissionsEnum::DeleteMentors,
|
||||||
] {
|
] {
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
use imphnen_libs::enviroment::load_env;
|
use imphnen_utils::{get_iso_date};
|
||||||
use imphnen_utils::{get_iso_date, Env};
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
use surrealdb::opt::auth::Root;
|
use surrealdb::opt::auth::Root;
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
load_env();
|
let env = &imphnen_libs::enviroment::ENV;
|
||||||
let env = Env::new();
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
password: &env.surrealdb_password,
|
password: &env.surrealdb_password,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
db.use_ns(env.surrealdb_namespace)
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
.use_db(env.surrealdb_dbname)
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let roles = vec![
|
let roles = vec![
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum};
|
use imphnen_iam::{get_iso_date, make_thing, PermissionsEnum};
|
||||||
use imphnen_libs::enviroment::load_env;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
use surrealdb::opt::auth::Root;
|
use surrealdb::opt::auth::Root;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
load_env();
|
let env = &imphnen_libs::enviroment::ENV;
|
||||||
let env = Env::new();
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
password: &env.surrealdb_password,
|
password: &env.surrealdb_password,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
db.use_ns(env.surrealdb_namespace)
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
.use_db(env.surrealdb_dbname)
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
db.query("DEFINE INDEX user_email_index ON TABLE users COLUMNS email UNIQUE;")
|
||||||
|
|
||||||
|
.await?;
|
||||||
|
db.query("DEFINE INDEX role_name_idx ON TABLE roles COLUMNS name UNIQUE;")
|
||||||
|
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
|
||||||
|
|
||||||
let roles_permissions = vec![
|
let roles_permissions = vec![
|
||||||
(
|
(
|
||||||
@@ -59,6 +65,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
(
|
(
|
||||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
vec![
|
vec![
|
||||||
|
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
|
||||||
PermissionsEnum::ReadOwnMentorProfile,
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
PermissionsEnum::UpdateOwnMentorProfile,
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
PermissionsEnum::ReadOwnMentorStatus,
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
@@ -76,6 +83,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
vec![
|
vec![
|
||||||
PermissionsEnum::ReadListGachaItems,
|
PermissionsEnum::ReadListGachaItems,
|
||||||
PermissionsEnum::ReadDetailGachaItems,
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::ReadListUsers,
|
||||||
|
PermissionsEnum::ReadDetailUsers,
|
||||||
PermissionsEnum::CreateGachaClaims,
|
PermissionsEnum::CreateGachaClaims,
|
||||||
PermissionsEnum::ReadDetailGachaClaims,
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
PermissionsEnum::ReadDetailGachaRolls,
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
use imphnen_iam::UsersSchema;
|
use imphnen_iam::UsersSchema;
|
||||||
use imphnen_libs::enviroment::load_env;
|
use imphnen_utils::{get_iso_date, hash_password};
|
||||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
load_env();
|
let env = &imphnen_libs::enviroment::ENV;
|
||||||
let env = Env::new();
|
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
@@ -15,8 +13,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
password: &env.surrealdb_password,
|
password: &env.surrealdb_password,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
db.use_ns(env.surrealdb_namespace)
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
.use_db(env.surrealdb_dbname)
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let users = vec![
|
let users = vec![
|
||||||
@@ -48,15 +46,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let user = UsersSchema {
|
let user = UsersSchema {
|
||||||
id: Thing::from(("app_users", id)),
|
id: Thing::from(("app_users", id)),
|
||||||
fullname: fullname.into(),
|
fullname: fullname.into(),
|
||||||
|
legal_name: Some(format!("{} Legal Name", fullname)),
|
||||||
email: email.into(),
|
email: email.into(),
|
||||||
password: hash_password("password").unwrap(),
|
password: hash_password("password").unwrap(),
|
||||||
avatar: None,
|
avatar: Some("https://example.com/avatar.jpg".into()),
|
||||||
phone_number: "081234567890".into(),
|
phone_number: "081234567890".into(),
|
||||||
|
phone_for_verification: Some("081234567890".into()),
|
||||||
is_active: true,
|
is_active: true,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
mentor_id: None,
|
mentor_id: None,
|
||||||
gender: None,
|
gender: Some("male".into()),
|
||||||
birthdate: None,
|
birthdate: Some("1990-05-15".into()),
|
||||||
|
domicile: Some("Jakarta, Indonesia".into()),
|
||||||
|
// identity_document_url: None, // Sudah tidak dipakai, bisa dihapus dari schema jika tidak diperlukan
|
||||||
|
bio: Some(format!("{} adalah user dengan data pribadi lengkap untuk testing.", fullname)),
|
||||||
|
last_education: Some("S1 Teknik Informatika".into()),
|
||||||
|
linkedin_url: Some("https://linkedin.com/in/user".into()),
|
||||||
|
github_url: Some("https://github.com/user".into()),
|
||||||
|
cv_url: Some("https://example.com/cv.pdf".into()),
|
||||||
|
portfolio_url: Some("https://example.com/portfolio".into()),
|
||||||
|
website_url: Some("https://example.com/website".into()),
|
||||||
|
twitter_url: Some("https://twitter.com/user".into()),
|
||||||
|
location: Some("Jakarta, Indonesia".into()),
|
||||||
|
skills: Some(vec!["JavaScript".into(), "React".into(), "Node.js".into()]),
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: Some("Senior Developer".into()),
|
||||||
role: Thing::from(("app_roles", role_id)),
|
role: Thing::from(("app_roles", role_id)),
|
||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
updated_at: get_iso_date(),
|
updated_at: get_iso_date(),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
|
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date, make_thing};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub struct EventsRepository<'a> {
|
pub struct EventsRepository<'a> {
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
@@ -25,6 +26,7 @@ impl<'a> EventsRepository<'a> {
|
|||||||
.with_pagination(meta.page, Some(10))
|
.with_pagination(meta.page, Some(10))
|
||||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||||
.build();
|
.build();
|
||||||
|
info!(query = %query, "Executing SurrealDB query");
|
||||||
let res: Vec<EventsQueryDto> =
|
let res: Vec<EventsQueryDto> =
|
||||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -44,10 +46,20 @@ impl<'a> EventsRepository<'a> {
|
|||||||
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
|
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
// Attempt to parse the ID. If it's a full Thing (e.g., "events:some_id"), extract the ID part.
|
||||||
|
// Otherwise, assume it's already the raw ID.
|
||||||
|
let parsed_id = if id.contains(":") {
|
||||||
|
let thing = make_thing(ResourceEnum::Events.to_string().as_str(), &id);
|
||||||
|
get_id(&thing)?.1.to_string()
|
||||||
|
} else {
|
||||||
|
id.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
|
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||||
.with_id(&id)
|
.with_id(&parsed_id)
|
||||||
.with_select_fields(vec!["*"]);
|
.with_select_fields(vec!["*"]);
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Option<EventsQueryDto> =
|
let result: Option<EventsQueryDto> =
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -72,6 +84,8 @@ impl<'a> EventsRepository<'a> {
|
|||||||
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events.to_string());
|
||||||
|
info!(query = %query_str, "Executing SurrealDB query");
|
||||||
let record: Option<EventsSchema> = db
|
let record: Option<EventsSchema> = db
|
||||||
.create(ResourceEnum::Events.to_string())
|
.create(ResourceEnum::Events.to_string())
|
||||||
.content(data)
|
.content(data)
|
||||||
@@ -106,6 +120,8 @@ impl<'a> EventsRepository<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let record_key = get_id(&merged.id)?;
|
let record_key = get_id(&merged.id)?;
|
||||||
|
let query_str = format!("UPDATE {:?} MERGE ...", record_key);
|
||||||
|
info!(query = %query_str, "Executing SurrealDB query");
|
||||||
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
|
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
@@ -130,6 +146,8 @@ impl<'a> EventsRepository<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let record_key = get_id(&event.id)?;
|
let record_key = get_id(&event.id)?;
|
||||||
|
let query_str = format!("UPDATE {:?} MERGE {{ is_deleted: true }}", record_key);
|
||||||
|
info!(query = %query_str, "Executing SurrealDB query");
|
||||||
let record: Option<EventsSchema> = db
|
let record: Option<EventsSchema> = db
|
||||||
.update(record_key)
|
.update(record_key)
|
||||||
.merge(serde_json::json!({ "is_deleted": true }))
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
|||||||
use serde_json;
|
use serde_json;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub struct TestimonialsRepository<'a> {
|
pub struct TestimonialsRepository<'a> {
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
@@ -28,6 +29,7 @@ impl<'a> TestimonialsRepository<'a> {
|
|||||||
.with_pagination(meta.page, Some(10))
|
.with_pagination(meta.page, Some(10))
|
||||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||||
.build();
|
.build();
|
||||||
|
info!(query = %query, "Executing SurrealDB query");
|
||||||
let res: Vec<TestimonialsQueryDto> =
|
let res: Vec<TestimonialsQueryDto> =
|
||||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -55,6 +57,7 @@ impl<'a> TestimonialsRepository<'a> {
|
|||||||
.with_condition("is_deleted = false")
|
.with_condition("is_deleted = false")
|
||||||
.with_select_fields(vec!["*", "user.* as user"]);
|
.with_select_fields(vec!["*", "user.* as user"]);
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Option<TestimonialsQueryDto> =
|
let result: Option<TestimonialsQueryDto> =
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -79,9 +82,14 @@ impl<'a> TestimonialsRepository<'a> {
|
|||||||
pub async fn query_create_testimonial(
|
pub async fn query_create_testimonial(
|
||||||
&self,
|
&self,
|
||||||
data: TestimonialsSchema,
|
data: TestimonialsSchema,
|
||||||
) -> Result<String> {
|
) -> Result<TestimonialsSchema> { // Change return type from String to TestimonialsSchema
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
info!(
|
||||||
|
resource = %ResourceEnum::Testimonials.to_string(),
|
||||||
|
payload = ?data,
|
||||||
|
"Executing SurrealDB create"
|
||||||
|
);
|
||||||
let record: Option<TestimonialsSchema> = db
|
let record: Option<TestimonialsSchema> = db
|
||||||
.create(ResourceEnum::Testimonials.to_string())
|
.create(ResourceEnum::Testimonials.to_string())
|
||||||
.content(data)
|
.content(data)
|
||||||
@@ -94,7 +102,7 @@ impl<'a> TestimonialsRepository<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(_) => Ok("Success create testimonial".into()),
|
Some(created_testimonial) => Ok(created_testimonial), // Return the created testimonial
|
||||||
None => bail!("Failed to create testimonial"),
|
None => bail!("Failed to create testimonial"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +128,11 @@ impl<'a> TestimonialsRepository<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let record_key = get_id(&merged.id)?;
|
let record_key = get_id(&merged.id)?;
|
||||||
|
info!(
|
||||||
|
record_key = ?record_key,
|
||||||
|
payload = ?merged,
|
||||||
|
"Executing SurrealDB update"
|
||||||
|
);
|
||||||
let record: Option<TestimonialsSchema> =
|
let record: Option<TestimonialsSchema> =
|
||||||
db.update(record_key).merge(merged).await?;
|
db.update(record_key).merge(merged).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -145,6 +158,10 @@ impl<'a> TestimonialsRepository<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let record_key = get_id(&testimonial.id)?;
|
let record_key = get_id(&testimonial.id)?;
|
||||||
|
info!(
|
||||||
|
record_key = ?record_key,
|
||||||
|
"Executing SurrealDB soft delete"
|
||||||
|
);
|
||||||
let record: Option<TestimonialsSchema> = db
|
let record: Option<TestimonialsSchema> = db
|
||||||
.update(record_key)
|
.update(record_key)
|
||||||
.merge(serde_json::json!({ "is_deleted": true }))
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use imphnen_libs::{
|
|||||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
};
|
};
|
||||||
use imphnen_utils::{
|
use imphnen_utils::{
|
||||||
common_response, success_list_response, success_response, validate_request,
|
common_response, success_list_response, success_response, success_created_response, validate_request,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TestimonialsService;
|
pub struct TestimonialsService;
|
||||||
@@ -46,8 +46,8 @@ impl TestimonialsService {
|
|||||||
Ok(testimonial) if !testimonial.is_deleted => {
|
Ok(testimonial) if !testimonial.is_deleted => {
|
||||||
success_response(ResponseSuccessDto {
|
success_response(ResponseSuccessDto {
|
||||||
data: TestimonialsDetailItemDto {
|
data: TestimonialsDetailItemDto {
|
||||||
id: testimonial.id.id.to_raw(),
|
id: testimonial.id.to_raw(),
|
||||||
user_id: testimonial.user.id.id.to_raw(),
|
user_id: testimonial.user.id.to_raw(),
|
||||||
user_fullname: testimonial.user.fullname,
|
user_fullname: testimonial.user.fullname,
|
||||||
role: testimonial.role,
|
role: testimonial.role,
|
||||||
content: testimonial.content,
|
content: testimonial.content,
|
||||||
@@ -72,7 +72,19 @@ impl TestimonialsService {
|
|||||||
let repo = TestimonialsRepository::new(state);
|
let repo = TestimonialsRepository::new(state);
|
||||||
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
||||||
match repo.query_create_testimonial(schema).await {
|
match repo.query_create_testimonial(schema).await {
|
||||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
Ok(created_testimonial) => {
|
||||||
|
success_created_response(ResponseSuccessDto {
|
||||||
|
data: TestimonialsDetailItemDto {
|
||||||
|
id: created_testimonial.id.to_raw(),
|
||||||
|
user_id: created_testimonial.user.id.to_raw(),
|
||||||
|
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()),
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ pub async fn get_mentor_list(
|
|||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::ReadListMentors],
|
vec![PermissionsEnum::ReadListMentors],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => MentorsService::get_mentor_list(&app_state, meta).await,
|
Ok((_user, app_state)) => MentorsService::get_mentor_list(&app_state, meta).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,13 +90,13 @@ pub async fn get_mentor_by_id(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::ReadDetailMentors],
|
vec![PermissionsEnum::ReadDetailMentors],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => MentorsService::get_mentor_by_id(&app_state, &id).await,
|
Ok((_user, app_state)) => MentorsService::get_mentor_by_id(&app_state, &id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,13 +126,13 @@ pub async fn put_update_mentor(
|
|||||||
Json(dto): Json<MentorUpdateRequestDto>,
|
Json(dto): Json<MentorUpdateRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::UpdateMentors],
|
vec![PermissionsEnum::UpdateMentors],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => MentorsService::update_mentor(&app_state, &id, dto).await,
|
Ok((_user, app_state)) => MentorsService::update_mentor(&app_state, &id, dto).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,13 +159,13 @@ pub async fn delete_mentor(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::DeleteMentors],
|
vec![PermissionsEnum::DeleteMentors],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => MentorsService::delete_mentor(&app_state, &id).await,
|
Ok((_user, app_state)) => MentorsService::delete_mentor(&app_state, &id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,13 +195,13 @@ pub async fn put_verify_mentor(
|
|||||||
Json(dto): Json<MentorVerifyRequestDto>,
|
Json(dto): Json<MentorVerifyRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::VerifyMentors],
|
vec![PermissionsEnum::VerifyMentors],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => MentorsService::verify_mentor(&app_state, &id, dto).await,
|
Ok((_user, app_state)) => MentorsService::verify_mentor(&app_state, &id, dto).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,17 +221,17 @@ pub async fn put_verify_mentor(
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub async fn get_mentor_me(
|
pub async fn get_mentor_me(
|
||||||
Extension(app_state): Extension<AppState>,
|
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
Extension(app_state): Extension<AppState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers.clone(),
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::ReadOwnMentorProfile],
|
vec![PermissionsEnum::ReadOwnMentorProfile],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok((_user, app_state)) => {
|
||||||
let email = match extract_email(&headers) {
|
let email = match extract_email(&headers) {
|
||||||
Some(email) => email,
|
Some(email) => email,
|
||||||
None => {
|
None => {
|
||||||
@@ -268,18 +268,18 @@ pub async fn get_mentor_me(
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub async fn put_update_mentor_me(
|
pub async fn put_update_mentor_me(
|
||||||
Extension(app_state): Extension<AppState>,
|
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
Extension(app_state): Extension<AppState>,
|
||||||
Json(dto): Json<MentorUpdateRequestDto>,
|
Json(dto): Json<MentorUpdateRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers.clone(),
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::UpdateOwnMentorProfile],
|
vec![PermissionsEnum::UpdateOwnMentorProfile],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok((_user, app_state)) => {
|
||||||
let email = match extract_email(&headers) {
|
let email = match extract_email(&headers) {
|
||||||
Some(email) => email,
|
Some(email) => email,
|
||||||
None => {
|
None => {
|
||||||
@@ -325,17 +325,17 @@ pub async fn put_update_mentor_no_id() -> Response {
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub async fn get_mentor_status(
|
pub async fn get_mentor_status(
|
||||||
Extension(app_state): Extension<AppState>,
|
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
Extension(app_state): Extension<AppState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers.clone(),
|
||||||
app_state.clone(),
|
Extension(app_state),
|
||||||
vec![PermissionsEnum::ReadOwnMentorStatus],
|
vec![PermissionsEnum::ReadOwnMentorStatus],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok((_user, app_state)) => {
|
||||||
let email = match extract_email(&headers) {
|
let email = match extract_email(&headers) {
|
||||||
Some(email) => email,
|
Some(email) => email,
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
@@ -18,15 +18,9 @@ pub struct MentorListResponseDto {
|
|||||||
pub struct MentorDetailWithUserDto {
|
pub struct MentorDetailWithUserDto {
|
||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub user_id: Thing,
|
pub user_id: Thing,
|
||||||
pub fullname: Option<String>,
|
// Personal data is now in UsersSchema, access via user_id
|
||||||
pub email: Option<String>,
|
// Removed: fullname, email, legal_name, identity_document_url,
|
||||||
pub legal_name: String,
|
// phone_for_verification, bio, linkedin_url, github_url, cv_url
|
||||||
pub identity_document_url: String,
|
|
||||||
pub phone_for_verification: String,
|
|
||||||
pub bio: String,
|
|
||||||
pub linkedin_url: Option<String>,
|
|
||||||
pub github_url: Option<String>,
|
|
||||||
pub cv_url: Option<String>,
|
|
||||||
pub industries: Vec<String>,
|
pub industries: Vec<String>,
|
||||||
pub expertise: Vec<String>,
|
pub expertise: Vec<String>,
|
||||||
pub languages: Vec<String>,
|
pub languages: Vec<String>,
|
||||||
@@ -47,15 +41,20 @@ pub struct MentorDetailWithUserDto {
|
|||||||
pub struct MentorDetailResponseDto {
|
pub struct MentorDetailResponseDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
|
// Personal data fields from UsersSchema
|
||||||
pub fullname: Option<String>,
|
pub fullname: Option<String>,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub legal_name: String,
|
pub legal_name: Option<String>,
|
||||||
pub identity_document_url: String,
|
pub gender: Option<String>,
|
||||||
pub phone_for_verification: String,
|
pub domicile: Option<String>,
|
||||||
pub bio: String,
|
pub phone_for_verification: Option<String>,
|
||||||
|
pub bio: Option<String>,
|
||||||
|
pub last_education: Option<String>,
|
||||||
pub linkedin_url: Option<String>,
|
pub linkedin_url: Option<String>,
|
||||||
pub github_url: Option<String>,
|
pub github_url: Option<String>,
|
||||||
pub cv_url: Option<String>,
|
pub cv_url: Option<String>,
|
||||||
|
pub portfolio_url: Option<String>,
|
||||||
|
// Professional data from MentorSchema
|
||||||
pub industries: Vec<String>,
|
pub industries: Vec<String>,
|
||||||
pub expertise: Vec<String>,
|
pub expertise: Vec<String>,
|
||||||
pub languages: Vec<String>,
|
pub languages: Vec<String>,
|
||||||
@@ -94,9 +93,6 @@ pub struct MentorUpdateRequestDto {
|
|||||||
pub gender: Option<String>,
|
pub gender: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub domicile: Option<String>,
|
pub domicile: Option<String>,
|
||||||
#[validate(url(message = "Invalid identity document URL"))]
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub identity_document_url: Option<String>,
|
|
||||||
#[validate(length(
|
#[validate(length(
|
||||||
min = 10,
|
min = 10,
|
||||||
max = 15,
|
max = 15,
|
||||||
@@ -118,6 +114,7 @@ pub struct MentorUpdateRequestDto {
|
|||||||
#[validate(url(message = "Invalid CV URL"))]
|
#[validate(url(message = "Invalid CV URL"))]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub cv_url: Option<String>,
|
pub cv_url: Option<String>,
|
||||||
|
#[validate(url(message = "Invalid portfolio URL"))]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub portfolio_url: Option<String>,
|
pub portfolio_url: Option<String>,
|
||||||
#[validate(length(min = 1, message = "At least 1 industry required"))]
|
#[validate(length(min = 1, message = "At least 1 industry required"))]
|
||||||
@@ -279,18 +276,7 @@ pub struct MentoringRate {
|
|||||||
pub struct MentorInsertDto {
|
pub struct MentorInsertDto {
|
||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub user_id: Option<Thing>,
|
pub user_id: Option<Thing>,
|
||||||
pub email: Option<String>,
|
// Personal data removed - now stored in UsersSchema
|
||||||
pub legal_name: String,
|
|
||||||
pub gender: Option<String>,
|
|
||||||
pub domicile: Option<String>,
|
|
||||||
pub identity_document_url: String,
|
|
||||||
pub phone_for_verification: String,
|
|
||||||
pub bio: 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 industries: Vec<String>,
|
||||||
pub expertise: Vec<String>,
|
pub expertise: Vec<String>,
|
||||||
pub languages: Vec<String>,
|
pub languages: Vec<String>,
|
||||||
@@ -313,18 +299,7 @@ impl From<MentorSchema> for MentorInsertDto {
|
|||||||
MentorInsertDto {
|
MentorInsertDto {
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
user_id: schema.user_id,
|
user_id: schema.user_id,
|
||||||
email: schema.email,
|
// Personal data removed from MentorSchema
|
||||||
legal_name: schema.legal_name,
|
|
||||||
gender: schema.gender,
|
|
||||||
domicile: schema.domicile,
|
|
||||||
identity_document_url: schema.identity_document_url,
|
|
||||||
phone_for_verification: schema.phone_for_verification,
|
|
||||||
bio: schema.bio,
|
|
||||||
last_education: schema.last_education,
|
|
||||||
linkedin_url: schema.linkedin_url,
|
|
||||||
github_url: schema.github_url,
|
|
||||||
cv_url: schema.cv_url,
|
|
||||||
portfolio_url: schema.portfolio_url,
|
|
||||||
industries: schema.industries,
|
industries: schema.industries,
|
||||||
expertise: schema.expertise,
|
expertise: schema.expertise,
|
||||||
languages: schema.languages,
|
languages: schema.languages,
|
||||||
@@ -354,19 +329,10 @@ pub struct MentorVerifyRequestDto {
|
|||||||
pub struct MentorDetailQueryDto {
|
pub struct MentorDetailQueryDto {
|
||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub user_id: Thing,
|
pub user_id: Thing,
|
||||||
pub fullname: Option<String>,
|
// Personal data has been moved to UsersSchema
|
||||||
pub email: Option<String>,
|
// Use user_id to get: fullname, email, legal_name, gender, domicile,
|
||||||
pub legal_name: String,
|
// identity_document_url, phone_for_verification, bio, last_education,
|
||||||
pub gender: Option<String>,
|
// linkedin_url, github_url, cv_url, portfolio_url
|
||||||
pub domicile: Option<String>,
|
|
||||||
pub identity_document_url: String,
|
|
||||||
pub phone_for_verification: String,
|
|
||||||
pub bio: 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 industries: Vec<String>,
|
||||||
pub expertise: Vec<String>,
|
pub expertise: Vec<String>,
|
||||||
pub languages: Vec<String>,
|
pub languages: Vec<String>,
|
||||||
@@ -388,8 +354,8 @@ impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
|||||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: extract_id(&dto.id),
|
id: extract_id(&dto.id),
|
||||||
fullname: dto.fullname,
|
fullname: None, // now in user table, must be populated from service layer
|
||||||
email: dto.email,
|
email: None, // now in user table, must be populated from service layer
|
||||||
status: dto.status,
|
status: dto.status,
|
||||||
created_at: dto.created_at,
|
created_at: dto.created_at,
|
||||||
updated_at: dto.updated_at,
|
updated_at: dto.updated_at,
|
||||||
@@ -402,15 +368,20 @@ impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
|
|||||||
Self {
|
Self {
|
||||||
id: extract_id(&dto.id),
|
id: extract_id(&dto.id),
|
||||||
user_id: extract_id(&dto.user_id),
|
user_id: extract_id(&dto.user_id),
|
||||||
fullname: dto.fullname,
|
// Personal data fields are populated in service layer from UsersSchema
|
||||||
email: dto.email,
|
fullname: None, // populated from user table in service layer
|
||||||
legal_name: dto.legal_name,
|
email: None, // populated from user table in service layer
|
||||||
identity_document_url: dto.identity_document_url,
|
legal_name: None, // populated from user table in service layer
|
||||||
phone_for_verification: dto.phone_for_verification,
|
gender: None, // populated from user table in service layer
|
||||||
bio: dto.bio,
|
domicile: None, // populated from user table in service layer
|
||||||
linkedin_url: dto.linkedin_url,
|
phone_for_verification: None, // populated from user table in service layer
|
||||||
github_url: dto.github_url,
|
bio: None, // populated from user table in service layer
|
||||||
cv_url: dto.cv_url,
|
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,
|
industries: dto.industries,
|
||||||
expertise: dto.expertise,
|
expertise: dto.expertise,
|
||||||
languages: dto.languages,
|
languages: dto.languages,
|
||||||
@@ -433,7 +404,7 @@ impl From<MentorSchema> for MentorRegisterResponseDto {
|
|||||||
Self {
|
Self {
|
||||||
id: schema.id.to_string(),
|
id: schema.id.to_string(),
|
||||||
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
|
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
|
||||||
email: schema.email,
|
email: None, // schema.email - now in user table
|
||||||
status: schema.status,
|
status: schema.status,
|
||||||
created_at: schema.created_at,
|
created_at: schema.created_at,
|
||||||
updated_at: schema.updated_at,
|
updated_at: schema.updated_at,
|
||||||
@@ -446,19 +417,7 @@ impl From<MentorDetailWithUserDto> for MentorDetailQueryDto {
|
|||||||
MentorDetailQueryDto {
|
MentorDetailQueryDto {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
user_id: dto.user_id,
|
user_id: dto.user_id,
|
||||||
fullname: dto.fullname,
|
// Personal data removed from MentorDetailWithUserDto - now in UsersSchema
|
||||||
email: dto.email,
|
|
||||||
legal_name: dto.legal_name,
|
|
||||||
gender: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
|
||||||
domicile: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
|
||||||
identity_document_url: dto.identity_document_url,
|
|
||||||
phone_for_verification: dto.phone_for_verification,
|
|
||||||
bio: dto.bio,
|
|
||||||
last_education: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
|
||||||
linkedin_url: dto.linkedin_url,
|
|
||||||
github_url: dto.github_url,
|
|
||||||
cv_url: dto.cv_url,
|
|
||||||
portfolio_url: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
|
||||||
industries: dto.industries,
|
industries: dto.industries,
|
||||||
expertise: dto.expertise,
|
expertise: dto.expertise,
|
||||||
languages: dto.languages,
|
languages: dto.languages,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
|
|||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub struct MentorsRepository<'a> {
|
pub struct MentorsRepository<'a> {
|
||||||
pub state: &'a AppState,
|
pub state: &'a AppState,
|
||||||
@@ -27,19 +28,11 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let mentors_table = ResourceEnum::Mentors.to_string();
|
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||||
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
|
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
|
||||||
.search_field("legal_name")
|
.search_field("user_id.legal_name") // Search in user data instead
|
||||||
.select_fields(vec![
|
.select_fields(vec![
|
||||||
"id",
|
"id",
|
||||||
"user_id",
|
"user_id",
|
||||||
"user_id.fullname as fullname",
|
// Personal data comes from user relation, not mentor table
|
||||||
"email",
|
|
||||||
"legal_name",
|
|
||||||
"identity_document_url",
|
|
||||||
"phone_for_verification",
|
|
||||||
"bio",
|
|
||||||
"linkedin_url",
|
|
||||||
"github_url",
|
|
||||||
"cv_url",
|
|
||||||
"industries",
|
"industries",
|
||||||
"expertise",
|
"expertise",
|
||||||
"languages",
|
"languages",
|
||||||
@@ -79,19 +72,11 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
||||||
.with_where("email", Some(email.clone()))
|
.with_where("user_id.email", Some(email.clone())) // Search in user table
|
||||||
.with_select_fields(vec![
|
.with_select_fields(vec![
|
||||||
"id",
|
"id",
|
||||||
"user_id",
|
"user_id",
|
||||||
"user_id.fullname as fullname",
|
// Personal data comes from user relation, not mentor table
|
||||||
"email",
|
|
||||||
"legal_name",
|
|
||||||
"identity_document_url",
|
|
||||||
"phone_for_verification",
|
|
||||||
"bio",
|
|
||||||
"linkedin_url",
|
|
||||||
"github_url",
|
|
||||||
"cv_url",
|
|
||||||
"industries",
|
"industries",
|
||||||
"expertise",
|
"expertise",
|
||||||
"languages",
|
"languages",
|
||||||
@@ -114,6 +99,7 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_email");
|
||||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -141,15 +127,7 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
.with_select_fields(vec![
|
.with_select_fields(vec![
|
||||||
"id",
|
"id",
|
||||||
"user_id",
|
"user_id",
|
||||||
"user_id.fullname as fullname",
|
// Personal data comes from user relation, not mentor table
|
||||||
"email",
|
|
||||||
"legal_name",
|
|
||||||
"identity_document_url",
|
|
||||||
"phone_for_verification",
|
|
||||||
"bio",
|
|
||||||
"linkedin_url",
|
|
||||||
"github_url",
|
|
||||||
"cv_url",
|
|
||||||
"industries",
|
"industries",
|
||||||
"expertise",
|
"expertise",
|
||||||
"languages",
|
"languages",
|
||||||
@@ -172,6 +150,7 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
|
||||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -194,8 +173,10 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let dto: MentorInsertDto = data.into();
|
let dto: MentorInsertDto = data.into();
|
||||||
|
let resource = ResourceEnum::Mentors.to_string();
|
||||||
|
info!(query = %resource, "Executing SurrealDB create in query_create_mentor");
|
||||||
let record: Option<MentorSchema> = db
|
let record: Option<MentorSchema> = db
|
||||||
.create(ResourceEnum::Mentors.to_string())
|
.create(resource)
|
||||||
.content(dto.clone())
|
.content(dto.clone())
|
||||||
.await?;
|
.await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -237,6 +218,7 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
|
|
||||||
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||||
|
|
||||||
|
info!(query = ?record_key, "Executing SurrealDB update in query_update_mentor");
|
||||||
let record: Option<MentorSchema> =
|
let record: Option<MentorSchema> =
|
||||||
db.update(record_key).merge(merged_data_json).await?;
|
db.update(record_key).merge(merged_data_json).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -282,6 +264,7 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||||
|
|
||||||
|
info!(query = ?record_key, "Executing SurrealDB soft delete in query_delete_mentor");
|
||||||
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
|
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::{
|
use super::{
|
||||||
IdentityAndVerification, MentorDetailQueryDto, MentorUpdateRequestDto,
|
MentorDetailQueryDto, MentorUpdateRequestDto,
|
||||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||||
};
|
};
|
||||||
use imphnen_libs::ResourceEnum;
|
use imphnen_libs::ResourceEnum;
|
||||||
@@ -12,18 +12,9 @@ pub struct MentorSchema {
|
|||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub user_id: Option<Thing>,
|
pub user_id: Option<Thing>,
|
||||||
pub email: Option<String>,
|
// Personal data has been moved to UsersSchema - use user_id to reference
|
||||||
pub legal_name: String,
|
// phone_for_verification, bio, last_education, linkedin_url, github_url,
|
||||||
pub gender: Option<String>,
|
// cv_url, portfolio_url
|
||||||
pub domicile: Option<String>,
|
|
||||||
pub identity_document_url: String,
|
|
||||||
pub phone_for_verification: String,
|
|
||||||
pub bio: 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 industries: Vec<String>,
|
||||||
pub expertise: Vec<String>,
|
pub expertise: Vec<String>,
|
||||||
pub languages: Vec<String>,
|
pub languages: Vec<String>,
|
||||||
@@ -52,18 +43,6 @@ impl Default for MentorSchema {
|
|||||||
ResourceEnum::Users.to_string().as_str(),
|
ResourceEnum::Users.to_string().as_str(),
|
||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
)),
|
)),
|
||||||
email: None,
|
|
||||||
legal_name: String::new(),
|
|
||||||
gender: None,
|
|
||||||
domicile: None,
|
|
||||||
identity_document_url: String::new(),
|
|
||||||
phone_for_verification: String::new(),
|
|
||||||
bio: String::new(),
|
|
||||||
last_education: None,
|
|
||||||
linkedin_url: None,
|
|
||||||
github_url: None,
|
|
||||||
cv_url: None,
|
|
||||||
portfolio_url: None,
|
|
||||||
industries: Vec::new(),
|
industries: Vec::new(),
|
||||||
expertise: Vec::new(),
|
expertise: Vec::new(),
|
||||||
languages: Vec::new(),
|
languages: Vec::new(),
|
||||||
@@ -89,11 +68,9 @@ impl Default for MentorSchema {
|
|||||||
|
|
||||||
impl MentorSchema {
|
impl MentorSchema {
|
||||||
pub fn create(
|
pub fn create(
|
||||||
identity_and_verification: IdentityAndVerification,
|
|
||||||
professional_profile: ProfessionalProfile,
|
professional_profile: ProfessionalProfile,
|
||||||
mentoring_logistics: MentoringLogistics,
|
mentoring_logistics: MentoringLogistics,
|
||||||
user_id_raw: String,
|
user_id_raw: String,
|
||||||
email_str: String,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: make_thing(
|
id: make_thing(
|
||||||
@@ -101,18 +78,7 @@ impl MentorSchema {
|
|||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)),
|
user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)),
|
||||||
email: Some(email_str),
|
// Personal data now stored in UsersSchema, not here
|
||||||
legal_name: identity_and_verification.legal_name,
|
|
||||||
gender: identity_and_verification.gender,
|
|
||||||
domicile: identity_and_verification.domicile,
|
|
||||||
identity_document_url: identity_and_verification.identity_document_url,
|
|
||||||
phone_for_verification: identity_and_verification.phone_for_verification,
|
|
||||||
bio: professional_profile.bio,
|
|
||||||
last_education: professional_profile.last_education,
|
|
||||||
linkedin_url: professional_profile.linkedin_url,
|
|
||||||
github_url: professional_profile.github_url,
|
|
||||||
cv_url: professional_profile.cv_url,
|
|
||||||
portfolio_url: professional_profile.portfolio_url,
|
|
||||||
industries: professional_profile.industries,
|
industries: professional_profile.industries,
|
||||||
expertise: professional_profile.expertise,
|
expertise: professional_profile.expertise,
|
||||||
languages: professional_profile.languages,
|
languages: professional_profile.languages,
|
||||||
@@ -139,18 +105,7 @@ impl MentorSchema {
|
|||||||
Self {
|
Self {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
user_id: Some(dto.user_id),
|
user_id: Some(dto.user_id),
|
||||||
email: dto.email,
|
// Personal data now comes from UsersSchema via user_id
|
||||||
legal_name: dto.legal_name,
|
|
||||||
gender: dto.gender,
|
|
||||||
domicile: dto.domicile,
|
|
||||||
identity_document_url: dto.identity_document_url,
|
|
||||||
phone_for_verification: dto.phone_for_verification,
|
|
||||||
bio: dto.bio,
|
|
||||||
last_education: dto.last_education,
|
|
||||||
linkedin_url: dto.linkedin_url,
|
|
||||||
github_url: dto.github_url,
|
|
||||||
cv_url: dto.cv_url,
|
|
||||||
portfolio_url: dto.portfolio_url,
|
|
||||||
industries: dto.industries,
|
industries: dto.industries,
|
||||||
expertise: dto.expertise,
|
expertise: dto.expertise,
|
||||||
languages: dto.languages,
|
languages: dto.languages,
|
||||||
@@ -170,40 +125,10 @@ impl MentorSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self {
|
pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self {
|
||||||
// Update fields only if they are Some(value), otherwise preserve current value
|
// phone_for_verification, bio, last_education, linkedin_url, github_url,
|
||||||
if let Some(val) = dto.legal_name {
|
// cv_url, portfolio_url) are now updated in UsersSchema, not here
|
||||||
self.legal_name = val;
|
|
||||||
}
|
// Only update professional fields that are still in MentorSchema
|
||||||
if let Some(val) = dto.gender {
|
|
||||||
self.gender = Some(val);
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.domicile {
|
|
||||||
self.domicile = Some(val);
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.identity_document_url {
|
|
||||||
self.identity_document_url = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.phone_for_verification {
|
|
||||||
self.phone_for_verification = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.bio {
|
|
||||||
self.bio = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.last_education {
|
|
||||||
self.last_education = Some(val);
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.linkedin_url {
|
|
||||||
self.linkedin_url = Some(val);
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.github_url {
|
|
||||||
self.github_url = Some(val);
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.cv_url {
|
|
||||||
self.cv_url = Some(val);
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.portfolio_url {
|
|
||||||
self.portfolio_url = Some(val);
|
|
||||||
}
|
|
||||||
if let Some(val) = dto.industries {
|
if let Some(val) = dto.industries {
|
||||||
self.industries = val;
|
self.industries = val;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,18 @@ impl MentorsService {
|
|||||||
|
|
||||||
user_schema.fullname = dto.fullname.clone();
|
user_schema.fullname = dto.fullname.clone();
|
||||||
user_schema.phone_number = dto.phone_number.clone();
|
user_schema.phone_number = dto.phone_number.clone();
|
||||||
|
// Update personal data from identity_and_verification
|
||||||
|
user_schema.legal_name = Some(dto.identity_and_verification.legal_name.clone());
|
||||||
|
user_schema.gender = dto.identity_and_verification.gender.clone();
|
||||||
|
user_schema.domicile = dto.identity_and_verification.domicile.clone();
|
||||||
|
user_schema.phone_for_verification = Some(dto.identity_and_verification.phone_for_verification.clone());
|
||||||
|
// Update personal data from professional_profile
|
||||||
|
user_schema.bio = Some(dto.professional_profile.bio.clone());
|
||||||
|
user_schema.last_education = dto.professional_profile.last_education.clone();
|
||||||
|
user_schema.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||||
|
user_schema.github_url = dto.professional_profile.github_url.clone();
|
||||||
|
user_schema.cv_url = dto.professional_profile.cv_url.clone();
|
||||||
|
user_schema.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||||
user_schema.updated_at = imphnen_utils::get_iso_date();
|
user_schema.updated_at = imphnen_utils::get_iso_date();
|
||||||
|
|
||||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||||
@@ -135,6 +147,18 @@ impl MentorsService {
|
|||||||
fullname: dto.fullname.clone(),
|
fullname: dto.fullname.clone(),
|
||||||
password: hashed_password,
|
password: hashed_password,
|
||||||
phone_number: dto.phone_number.clone(),
|
phone_number: dto.phone_number.clone(),
|
||||||
|
// Store personal data from identity_and_verification in user
|
||||||
|
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||||
|
gender: dto.identity_and_verification.gender.clone(),
|
||||||
|
domicile: dto.identity_and_verification.domicile.clone(),
|
||||||
|
phone_for_verification: Some(dto.identity_and_verification.phone_for_verification.clone()),
|
||||||
|
// Store personal data from professional_profile in user
|
||||||
|
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(),
|
||||||
created_at: imphnen_utils::get_iso_date(),
|
created_at: imphnen_utils::get_iso_date(),
|
||||||
updated_at: imphnen_utils::get_iso_date(),
|
updated_at: imphnen_utils::get_iso_date(),
|
||||||
role: imphnen_utils::make_thing(
|
role: imphnen_utils::make_thing(
|
||||||
@@ -187,11 +211,9 @@ impl MentorsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mentor_schema = MentorSchema::create(
|
let mentor_schema = MentorSchema::create(
|
||||||
dto.identity_and_verification,
|
|
||||||
dto.professional_profile,
|
dto.professional_profile,
|
||||||
dto.mentoring_logistics,
|
dto.mentoring_logistics,
|
||||||
user_id.to_raw(),
|
user_id.to_raw(),
|
||||||
final_user_email.clone(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
||||||
@@ -233,16 +255,27 @@ impl MentorsService {
|
|||||||
|
|
||||||
pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||||
let repo = MentorsRepository::new(state);
|
let repo = MentorsRepository::new(state);
|
||||||
|
let user_repo = UsersRepository::new(state);
|
||||||
|
|
||||||
match repo.query_mentor_list(meta).await {
|
match repo.query_mentor_list(meta).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
let data: Vec<MentorListResponseDto> = result
|
let mut mentor_list_data: Vec<MentorListResponseDto> = Vec::new();
|
||||||
.data
|
|
||||||
.into_iter()
|
for mentor_with_user in result.data {
|
||||||
.map(MentorDetailQueryDto::from)
|
let mentor_dto = MentorDetailQueryDto::from(mentor_with_user);
|
||||||
.map(MentorListResponseDto::from)
|
let mut list_item = MentorListResponseDto::from(mentor_dto.clone());
|
||||||
.collect();
|
|
||||||
|
// 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 {
|
success_list_response(ResponseListSuccessDto {
|
||||||
data,
|
data: mentor_list_data,
|
||||||
meta: result.meta,
|
meta: result.meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -251,12 +284,56 @@ impl MentorsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
|
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
|
||||||
let repo = MentorsRepository::new(state);
|
let mentor_repo = MentorsRepository::new(state);
|
||||||
|
let user_repo = UsersRepository::new(state);
|
||||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||||
match repo.query_mentor_by_id(&thing_id, false).await {
|
|
||||||
|
match mentor_repo.query_mentor_by_id(&thing_id, false).await {
|
||||||
Ok(mentor) => {
|
Ok(mentor) => {
|
||||||
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
|
// Get user data separately
|
||||||
success_response(ResponseSuccessDto { data: dto })
|
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.to_raw(),
|
||||||
|
user_id: mentor.user_id.to_raw(),
|
||||||
|
// Personal data from user
|
||||||
|
fullname: Some(user.fullname),
|
||||||
|
email: Some(user.email),
|
||||||
|
legal_name: user.legal_name,
|
||||||
|
gender: user.gender,
|
||||||
|
domicile: user.domicile,
|
||||||
|
phone_for_verification: user.phone_for_verification,
|
||||||
|
bio: user.bio,
|
||||||
|
last_education: user.last_education,
|
||||||
|
linkedin_url: user.linkedin_url,
|
||||||
|
github_url: user.github_url,
|
||||||
|
cv_url: user.cv_url,
|
||||||
|
portfolio_url: user.portfolio_url,
|
||||||
|
// 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()),
|
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ pub fn mentors_router() -> Router {
|
|||||||
.route("/update/{id}", put(put_update_mentor))
|
.route("/update/{id}", put(put_update_mentor))
|
||||||
.route("/update", put(put_update_mentor_no_id))
|
.route("/update", put(put_update_mentor_no_id))
|
||||||
.route("/delete/{id}", delete(delete_mentor))
|
.route("/delete/{id}", delete(delete_mentor))
|
||||||
.route("/verify/{id}", put(put(put_verify_mentor)))
|
.route("/verify/{id}", put(put_verify_mentor))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ axum.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
surrealdb.workspace = true
|
surrealdb.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ pub mod error {
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("database error: {0}")]
|
#[error("database error: {0}")]
|
||||||
Db(String),
|
Db(String),
|
||||||
|
#[error("anyhow error: {0}")]
|
||||||
|
Anyhow(#[from] anyhow::Error),
|
||||||
|
#[error("HTTP status code error: {0}")]
|
||||||
|
StatusCode(StatusCode),
|
||||||
|
#[error("authentication error: {0}")]
|
||||||
|
Auth(String),
|
||||||
|
#[error("validation error: {0}")]
|
||||||
|
Validation(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for Error {
|
||||||
@@ -18,6 +26,19 @@ pub mod error {
|
|||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Database error: {detail}"),
|
format!("Database error: {detail}"),
|
||||||
),
|
),
|
||||||
|
Error::Anyhow(detail) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Internal server error: {detail}"),
|
||||||
|
),
|
||||||
|
Error::StatusCode(s) => (s, format!("HTTP error: {}", s)),
|
||||||
|
Error::Auth(detail) => (
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
format!("Authentication error: {detail}"),
|
||||||
|
),
|
||||||
|
Error::Validation(detail) => (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("Validation error: {detail}"),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
(status, Json(error_message)).into_response()
|
(status, Json(error_message)).into_response()
|
||||||
}
|
}
|
||||||
@@ -28,4 +49,10 @@ pub mod error {
|
|||||||
Self::Db(error.to_string())
|
Self::Db(error.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<StatusCode> for Error {
|
||||||
|
fn from(status: StatusCode) -> Self {
|
||||||
|
Self::StatusCode(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ pub async fn get_detail_gacha_claim(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::CreateGachaClaims],
|
vec![PermissionsEnum::ReadDetailGachaClaims],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaClaimService::get_gacha_claim_by_id(&state, id).await,
|
Ok((_user, state)) => GachaClaimService::get_gacha_claim_by_id(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,13 +54,13 @@ pub async fn post_create_gacha_claim(
|
|||||||
Json(payload): Json<GachaClaimRequestDto>,
|
Json(payload): Json<GachaClaimRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::CreateGachaClaims],
|
vec![PermissionsEnum::CreateGachaClaims],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaClaimService::create_gacha_claim(&state, payload).await,
|
Ok((_user, state)) => GachaClaimService::create_gacha_claim(&state, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{AppState, ResourceEnum};
|
|||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use imphnen_iam::DetailQueryBuilder;
|
use imphnen_iam::DetailQueryBuilder;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::{instrument, info};
|
||||||
|
|
||||||
pub struct GachaClaimRepository<'a> {
|
pub struct GachaClaimRepository<'a> {
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
@@ -27,6 +27,7 @@ impl<'a> GachaClaimRepository<'a> {
|
|||||||
.with_fetch("item")
|
.with_fetch("item")
|
||||||
.with_fetch("user");
|
.with_fetch("user");
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Option<GachaClaimQueryDto> =
|
let result: Option<GachaClaimQueryDto> =
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -48,6 +49,11 @@ impl<'a> GachaClaimRepository<'a> {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
info!(
|
||||||
|
resource = %ResourceEnum::GachaClaims.to_string(),
|
||||||
|
content = ?data,
|
||||||
|
"Executing SurrealDB create query"
|
||||||
|
);
|
||||||
let record: Option<GachaClaimSchema> = db
|
let record: Option<GachaClaimSchema> = db
|
||||||
.create(ResourceEnum::GachaClaims.to_string())
|
.create(ResourceEnum::GachaClaims.to_string())
|
||||||
.content(data)
|
.content(data)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use anyhow::{Result, bail};
|
|||||||
use imphnen_iam::make_thing;
|
use imphnen_iam::make_thing;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use surrealdb::Uuid;
|
use surrealdb::Uuid;
|
||||||
use tracing::instrument;
|
use tracing::{instrument, info};
|
||||||
|
|
||||||
pub struct GachaCreditRepository<'a> {
|
pub struct GachaCreditRepository<'a> {
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
@@ -27,6 +27,7 @@ impl<'a> GachaCreditRepository<'a> {
|
|||||||
ResourceEnum::GachaCredits,
|
ResourceEnum::GachaCredits,
|
||||||
ResourceEnum::Users
|
ResourceEnum::Users
|
||||||
);
|
);
|
||||||
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Vec<GachaCreditSchema> =
|
let result: Vec<GachaCreditSchema> =
|
||||||
db.query(sql).bind(("user_id", user_id)).await?.take(0)?;
|
db.query(sql).bind(("user_id", user_id)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -66,6 +67,7 @@ impl<'a> GachaCreditRepository<'a> {
|
|||||||
bail!("No extra roll credits remaining");
|
bail!("No extra roll credits remaining");
|
||||||
}
|
}
|
||||||
credit.available_rolls -= 1;
|
credit.available_rolls -= 1;
|
||||||
|
info!(operation = "update", table = %ResourceEnum::GachaCredits.to_string(), id = %credit.id.id.to_raw(), "Executing SurrealDB update for consume_credit");
|
||||||
let _: Option<GachaCreditSchema> = db
|
let _: Option<GachaCreditSchema> = db
|
||||||
.update((
|
.update((
|
||||||
&ResourceEnum::GachaCredits.to_string(),
|
&ResourceEnum::GachaCredits.to_string(),
|
||||||
@@ -91,6 +93,7 @@ impl<'a> GachaCreditRepository<'a> {
|
|||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
|
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
|
||||||
credit.available_rolls += payload.amount;
|
credit.available_rolls += payload.amount;
|
||||||
|
info!(operation = "update", table = %ResourceEnum::GachaCredits.to_string(), id = %credit.id.id.to_raw(), "Executing SurrealDB update for add_credit");
|
||||||
let _: Option<GachaCreditSchema> = db
|
let _: Option<GachaCreditSchema> = db
|
||||||
.update((
|
.update((
|
||||||
&ResourceEnum::GachaCredits.to_string(),
|
&ResourceEnum::GachaCredits.to_string(),
|
||||||
@@ -108,6 +111,7 @@ impl<'a> GachaCreditRepository<'a> {
|
|||||||
available_rolls: payload.amount,
|
available_rolls: payload.amount,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
info!(operation = "create", table = %ResourceEnum::GachaCredits.to_string(), "Executing SurrealDB create for add_credit");
|
||||||
let _: Option<GachaCreditSchema> = db
|
let _: Option<GachaCreditSchema> = db
|
||||||
.create(ResourceEnum::GachaCredits.to_string())
|
.create(ResourceEnum::GachaCredits.to_string())
|
||||||
.content(data)
|
.content(data)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AppState, GachaItemDto, GachaItemRequestDto, GachaItemService, MessageResponseDto,
|
AppState, GachaItemDto, GachaItemRequestDto, GachaItemUpdateRequestDto, GachaItemService, MessageResponseDto,
|
||||||
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -36,13 +36,13 @@ pub async fn get_gacha_item_list(
|
|||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadListGachaItems],
|
vec![PermissionsEnum::ReadListGachaItems],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaItemService::get_gacha_item_list(&state, meta).await,
|
Ok((_user, state)) => GachaItemService::get_gacha_item_list(&state, meta).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,13 +65,13 @@ pub async fn get_gacha_item_by_id(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadDetailGachaItems],
|
vec![PermissionsEnum::ReadDetailGachaItems],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaItemService::get_gacha_item_by_id(&state, id).await,
|
Ok((_user, state)) => GachaItemService::get_gacha_item_by_id(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,13 +94,13 @@ pub async fn post_create_gacha_item(
|
|||||||
Json(payload): Json<GachaItemRequestDto>,
|
Json(payload): Json<GachaItemRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::CreateGachaItems],
|
vec![PermissionsEnum::CreateGachaItems],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaItemService::create_gacha_item(&state, payload).await,
|
Ok((_user, state)) => GachaItemService::create_gacha_item(&state, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ pub async fn post_create_gacha_item(
|
|||||||
security(
|
security(
|
||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
request_body = GachaItemRequestDto,
|
request_body = GachaItemUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Update gacha item", body = MessageResponseDto)
|
(status = 200, description = "Update gacha item", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
@@ -121,16 +121,16 @@ pub async fn put_update_gacha_item(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(payload): Json<GachaItemRequestDto>,
|
Json(payload): Json<GachaItemUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::UpdateGachaItems],
|
vec![PermissionsEnum::UpdateGachaItems],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaItemService::update_gacha_item(&state, payload, id).await,
|
Ok((_user, state)) => GachaItemService::update_gacha_item(&state, payload, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,13 +152,13 @@ pub async fn delete_gacha_item(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::DeleteGachaItems],
|
vec![PermissionsEnum::DeleteGachaItems],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaItemService::delete_gacha_item(&state, id).await,
|
Ok((_user, state)) => GachaItemService::delete_gacha_item(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ pub struct GachaItemRequestDto {
|
|||||||
pub image_url: String,
|
pub image_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct GachaItemUpdateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Item name must not be empty"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[validate(length(min = 1, message = "Image URL must not be empty"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct GachaItemDto {
|
pub struct GachaItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use imphnen_utils::get_iso_date;
|
|||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub struct GachaItemRepository<'a> {
|
pub struct GachaItemRepository<'a> {
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
@@ -25,6 +26,11 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
meta: MetaRequestDto,
|
meta: MetaRequestDto,
|
||||||
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
|
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
let surreal_query = format!(
|
||||||
|
"SELECT * FROM {} WHERE is_deleted = false AND name LIKE ?",
|
||||||
|
ResourceEnum::GachaItems.to_string()
|
||||||
|
);
|
||||||
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
||||||
QueryListBuilder::new(
|
QueryListBuilder::new(
|
||||||
&self.state.surrealdb_ws,
|
&self.state.surrealdb_ws,
|
||||||
@@ -57,6 +63,8 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let surreal_query = format!("SELECT * FROM {} WHERE id = '{}'", ResourceEnum::GachaItems.to_string(), id);
|
||||||
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let result: Option<GachaItemSchema> = db
|
let result: Option<GachaItemSchema> = db
|
||||||
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -79,6 +87,8 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let surreal_query = format!("CREATE {} CONTENT ...", ResourceEnum::GachaItems.to_string());
|
||||||
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let record: Option<GachaItemSchema> = db
|
let record: Option<GachaItemSchema> = db
|
||||||
.create(ResourceEnum::GachaItems.to_string())
|
.create(ResourceEnum::GachaItems.to_string())
|
||||||
.content(data)
|
.content(data)
|
||||||
@@ -111,6 +121,8 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
..data.clone()
|
..data.clone()
|
||||||
};
|
};
|
||||||
|
let surreal_query = format!("UPDATE {:?} MERGE ...", record_key);
|
||||||
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let record: Option<GachaItemSchema> =
|
let record: Option<GachaItemSchema> =
|
||||||
db.update(record_key).merge(merged).await?;
|
db.update(record_key).merge(merged).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -139,6 +151,8 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||||
|
|
||||||
|
let surreal_query = format!("UPDATE {:?} MERGE ...", record_key);
|
||||||
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let record: Option<GachaItemSchema> = db.update(record_key).merge(patch).await?;
|
let record: Option<GachaItemSchema> = db.update(record_key).merge(patch).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
|
|||||||
@@ -43,4 +43,8 @@ impl GachaItemSchema {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_existing(existing: GachaItemSchema) -> Self {
|
||||||
|
existing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AppState, GachaItemDto, GachaItemRepository, GachaItemRequestDto, GachaItemSchema,
|
AppState, GachaItemDto, GachaItemRepository, GachaItemRequestDto, GachaItemUpdateRequestDto, GachaItemSchema,
|
||||||
MetaRequestDto, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
MetaRequestDto, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
common_response, make_thing, success_list_response, success_response,
|
common_response, make_thing, success_list_response, success_response,
|
||||||
validate_request,
|
validate_request,
|
||||||
};
|
};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
|
use imphnen_utils::get_iso_date;
|
||||||
|
|
||||||
pub struct GachaItemService;
|
pub struct GachaItemService;
|
||||||
|
|
||||||
@@ -59,20 +60,33 @@ impl GachaItemService {
|
|||||||
|
|
||||||
pub async fn update_gacha_item(
|
pub async fn update_gacha_item(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
payload: GachaItemRequestDto,
|
payload: GachaItemUpdateRequestDto,
|
||||||
id: String,
|
id: String,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let repo = GachaItemRepository::new(state);
|
let repo = GachaItemRepository::new(state);
|
||||||
let schema = GachaItemSchema {
|
|
||||||
id: make_thing(&ResourceEnum::GachaItems.to_string(), &id),
|
// Get current gacha item data first
|
||||||
name: payload.name,
|
let _thing_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
|
||||||
image_url: payload.image_url,
|
let current_item = match repo.query_gacha_item_by_id(id.clone()).await {
|
||||||
..Default::default()
|
Ok(item) => item,
|
||||||
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Gacha Item not found"),
|
||||||
};
|
};
|
||||||
match repo.query_update_gacha_item(schema).await {
|
|
||||||
|
let mut updated_item = current_item;
|
||||||
|
updated_item.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
// Only update fields that are provided
|
||||||
|
if let Some(name) = payload.name {
|
||||||
|
updated_item.name = name;
|
||||||
|
}
|
||||||
|
if let Some(image_url) = payload.image_url {
|
||||||
|
updated_item.image_url = image_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
match repo.query_update_gacha_item(updated_item).await {
|
||||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.to_string().contains("not found") {
|
if e.to_string().contains("not found") {
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ pub async fn get_detail_gacha_roll(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadDetailGachaRolls],
|
vec![PermissionsEnum::ReadDetailGachaRolls],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaRollService::get_gacha_roll_by_id(&state, id).await,
|
Ok((_user, state)) => GachaRollService::get_gacha_roll_by_id(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,13 +54,13 @@ pub async fn post_create_gacha_roll(
|
|||||||
Json(payload): Json<GachaRollRequestDto>,
|
Json(payload): Json<GachaRollRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::CreateGachaRolls],
|
vec![PermissionsEnum::CreateGachaRolls],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaRollService::create_gacha_roll(&state, payload).await,
|
Ok((_user, state)) => GachaRollService::create_gacha_roll(&state, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,13 +81,13 @@ pub async fn post_execute_gacha_roll(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers.clone(),
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ExecuteGachaRolls],
|
vec![PermissionsEnum::ExecuteGachaRolls],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaRollService::execute_roll_once(headers, &state).await,
|
Ok((_user, state)) => GachaRollService::execute_roll_once(headers, &state).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,13 +110,13 @@ pub async fn delete_gacha_roll(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::DeleteGachaRolls],
|
vec![PermissionsEnum::DeleteGachaRolls],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => GachaRollService::soft_delete_gacha_roll(&state, id).await,
|
Ok((_user, state)) => GachaRollService::soft_delete_gacha_roll(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use rand_distr::weighted::WeightedIndex;
|
|||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub struct GachaRollRepository<'a> {
|
pub struct GachaRollRepository<'a> {
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
@@ -33,6 +34,7 @@ impl<'a> GachaRollRepository<'a> {
|
|||||||
.with_select_fields(vec!["*"])
|
.with_select_fields(vec!["*"])
|
||||||
.with_fetch("item");
|
.with_fetch("item");
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Option<GachaRollQueryDto> =
|
let result: Option<GachaRollQueryDto> =
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -54,6 +56,7 @@ impl<'a> GachaRollRepository<'a> {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
info!(query = "CREATE", "Executing SurrealDB create operation for GachaRolls");
|
||||||
let record: Option<GachaRollSchema> = db
|
let record: Option<GachaRollSchema> = db
|
||||||
.create(ResourceEnum::GachaRolls.to_string())
|
.create(ResourceEnum::GachaRolls.to_string())
|
||||||
.content(data)
|
.content(data)
|
||||||
@@ -77,6 +80,7 @@ impl<'a> GachaRollRepository<'a> {
|
|||||||
let table_name = ResourceEnum::GachaRolls.to_string();
|
let table_name = ResourceEnum::GachaRolls.to_string();
|
||||||
let sql =
|
let sql =
|
||||||
format!("SELECT * FROM {table_name} WHERE is_deleted = false FETCH item");
|
format!("SELECT * FROM {table_name} WHERE is_deleted = false FETCH item");
|
||||||
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Vec<GachaRollQueryDto> = db.query(sql).await?.take(0)?;
|
let result: Vec<GachaRollQueryDto> = db.query(sql).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
@@ -87,6 +91,7 @@ impl<'a> GachaRollRepository<'a> {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
pub fn roll_once(rolls: &[GachaRollQueryDto]) -> Option<GachaRollQueryDto> {
|
pub fn roll_once(rolls: &[GachaRollQueryDto]) -> Option<GachaRollQueryDto> {
|
||||||
let filtered: Vec<_> = rolls
|
let filtered: Vec<_> = rolls
|
||||||
.iter()
|
.iter()
|
||||||
@@ -120,6 +125,7 @@ impl<'a> GachaRollRepository<'a> {
|
|||||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||||
|
|
||||||
|
info!(query = "UPDATE", record_key = ?record_key, "Executing SurrealDB update operation for GachaRolls");
|
||||||
let record: Option<GachaRollSchema> = db.update(record_key).merge(patch).await?;
|
let record: Option<GachaRollSchema> = db.update(record_key).merge(patch).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ use imphnen_iam::{
|
|||||||
RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto,
|
RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto,
|
||||||
UsersListItemDto, UsersUpdateRequestDto, auth, permissions, roles, users,
|
UsersListItemDto, UsersUpdateRequestDto, auth, permissions, roles, users,
|
||||||
};
|
};
|
||||||
|
use imphnen_iam::users::users_controller::FileUploadSchema;
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
Modify, OpenApi,
|
Modify, OpenApi,
|
||||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||||
@@ -53,6 +54,7 @@ use utoipa::{
|
|||||||
users::users_controller::get_user_by_id,
|
users::users_controller::get_user_by_id,
|
||||||
users::users_controller::get_user_me,
|
users::users_controller::get_user_me,
|
||||||
users::users_controller::get_user_list,
|
users::users_controller::get_user_list,
|
||||||
|
users::users_controller::upload_file,
|
||||||
roles::roles_controller::get_role_list,
|
roles::roles_controller::get_role_list,
|
||||||
roles::roles_controller::get_role_by_id,
|
roles::roles_controller::get_role_by_id,
|
||||||
roles::roles_controller::post_create_role,
|
roles::roles_controller::post_create_role,
|
||||||
@@ -114,6 +116,7 @@ use utoipa::{
|
|||||||
UsersListItemDto,
|
UsersListItemDto,
|
||||||
UsersUpdateRequestDto,
|
UsersUpdateRequestDto,
|
||||||
UsersCreateRequestDto,
|
UsersCreateRequestDto,
|
||||||
|
FileUploadSchema,
|
||||||
GachaClaimItemDto,
|
GachaClaimItemDto,
|
||||||
GachaClaimRequestDto,
|
GachaClaimRequestDto,
|
||||||
GachaItemDto,
|
GachaItemDto,
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
imphnen-libs.workspace = true
|
imphnen-libs.workspace = true
|
||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
|
|
||||||
imphnen-entities.workspace = true
|
imphnen-entities.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json = { workspace = true }
|
||||||
|
oauth2 = { workspace = true }
|
||||||
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
@@ -28,7 +31,10 @@ log.workspace = true
|
|||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
axum-extra.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dotenvy.workspace = true
|
dotenvy.workspace = true
|
||||||
|
tokio-test = { workspace = true }
|
||||||
|
mockall = { workspace = true }
|
||||||
http-body-util.workspace = true
|
http-body-util.workspace = true
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use super::{
|
use super::{
|
||||||
AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto,
|
AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto,
|
||||||
AuthResendOtpRequestDto, AuthService, AuthVerifyEmailRequestDto,
|
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto,
|
||||||
};
|
};
|
||||||
use crate::{AppState, v1::AuthLoginResponsetDto};
|
use crate::{AppState, v1::AuthLoginResponsetDto};
|
||||||
use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto};
|
use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto};
|
||||||
use axum::{Extension, Json, response::IntoResponse};
|
use axum::{Extension, Json, response::IntoResponse};
|
||||||
|
use crate::v1::auth::auth_service::AuthServiceTrait;
|
||||||
|
use crate::v1::auth::auth_service::AuthService;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
@@ -137,7 +139,8 @@ pub async fn post_new_password(
|
|||||||
tag = "Authentication"
|
tag = "Authentication"
|
||||||
)]
|
)]
|
||||||
pub async fn post_refresh_token(
|
pub async fn post_refresh_token(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<AuthRefreshTokenRequestDto>,
|
Json(payload): Json<AuthRefreshTokenRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
AuthService::mutation_refresh_token(payload).await
|
AuthService::mutation_refresh_token(payload, &state).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub struct AuthLoginResponsetDto {
|
|||||||
pub user: UsersDetailItemDto,
|
pub user: UsersDetailItemDto,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)]
|
||||||
pub struct TokenDto {
|
pub struct TokenDto {
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
@@ -121,4 +121,4 @@ pub struct AuthSetNewPasswordRequestDto {
|
|||||||
pub struct UserCacheSchema {
|
pub struct UserCacheSchema {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub permissions: Vec<String>,
|
pub permissions: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ use crate::{
|
|||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
|
use tracing::instrument;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub struct AuthRepository<'a> {
|
pub struct AuthRepository<'a> {
|
||||||
pub state: &'a AppState,
|
pub state: &'a AppState,
|
||||||
@@ -17,6 +19,7 @@ impl<'a> AuthRepository<'a> {
|
|||||||
Self { state }
|
Self { state }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, user), err)]
|
||||||
pub async fn query_store_user(&self, user: UsersDetailQueryDto) -> Result<String> {
|
pub async fn query_store_user(&self, user: UsersDetailQueryDto) -> Result<String> {
|
||||||
if user.email.trim().is_empty() {
|
if user.email.trim().is_empty() {
|
||||||
bail!("Email is required");
|
bail!("Email is required");
|
||||||
@@ -30,12 +33,14 @@ impl<'a> AuthRepository<'a> {
|
|||||||
permissions,
|
permissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
info!(query = %format!("DELETE FROM {} WHERE id = '{}'", table, user_id), "Executing SurrealDB query");
|
||||||
let _record: Option<UserCacheSchema> = self
|
let _record: Option<UserCacheSchema> = self
|
||||||
.state
|
.state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
.delete::<Option<UserCacheSchema>>((table.clone(), user_id.clone()))
|
.delete::<Option<UserCacheSchema>>((table.clone(), user_id.clone()))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
info!(query = %format!("CREATE {}:{}", table, user_id), "Executing SurrealDB query");
|
||||||
let record: Option<UserCacheSchema> = self
|
let record: Option<UserCacheSchema> = self
|
||||||
.state
|
.state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
@@ -49,10 +54,12 @@ impl<'a> AuthRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, email), err)]
|
||||||
pub async fn query_get_stored_user(
|
pub async fn query_get_stored_user(
|
||||||
&self,
|
&self,
|
||||||
email: String,
|
email: String,
|
||||||
) -> Result<UsersDetailQueryDto> {
|
) -> Result<UsersDetailQueryDto> {
|
||||||
|
info!(query = %format!("SELECT FROM {} WHERE id = '{}'", ResourceEnum::UsersCache.to_string(), email), "Executing SurrealDB query");
|
||||||
let user_cache: Option<UserCacheSchema> = self
|
let user_cache: Option<UserCacheSchema> = self
|
||||||
.state
|
.state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
@@ -87,13 +94,29 @@ impl<'a> AuthRepository<'a> {
|
|||||||
Ok(UsersDetailQueryDto {
|
Ok(UsersDetailQueryDto {
|
||||||
id: Thing::from(("app_users".to_string(), email.clone())),
|
id: Thing::from(("app_users".to_string(), email.clone())),
|
||||||
fullname: "Cached User".to_string(),
|
fullname: "Cached User".to_string(),
|
||||||
|
legal_name: None,
|
||||||
email: cache.email,
|
email: cache.email,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
phone_number: String::new(),
|
phone_number: String::new(),
|
||||||
|
phone_for_verification: None,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
password: String::new(),
|
password: String::new(),
|
||||||
role: role_detail_query_dto,
|
role: role_detail_query_dto,
|
||||||
created_at: String::new(),
|
created_at: String::new(),
|
||||||
@@ -105,7 +128,9 @@ impl<'a> AuthRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, email), err)]
|
||||||
pub async fn query_delete_stored_user(&self, email: String) -> Result<String> {
|
pub async fn query_delete_stored_user(&self, email: String) -> Result<String> {
|
||||||
|
info!(query = %format!("DELETE FROM {} WHERE id = '{}'", ResourceEnum::UsersCache.to_string(), email), "Executing SurrealDB query");
|
||||||
let record: Option<UsersDetailQueryDto> = self
|
let record: Option<UsersDetailQueryDto> = self
|
||||||
.state
|
.state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
@@ -117,13 +142,16 @@ impl<'a> AuthRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, email), err)]
|
||||||
pub async fn query_get_stored_otp(&self, email: String) -> Result<u32> {
|
pub async fn query_get_stored_otp(&self, email: String) -> Result<u32> {
|
||||||
let table = ResourceEnum::OtpCache.to_string();
|
let table = ResourceEnum::OtpCache.to_string();
|
||||||
let key = (table.as_str(), email.as_str());
|
let key = (table.as_str(), email.as_str());
|
||||||
|
info!(query = %format!("SELECT FROM {} WHERE id = '{}'", table, email), "Executing SurrealDB query");
|
||||||
let result: Option<AuthOtpSchema> = self.state.surrealdb_mem.select(key).await?;
|
let result: Option<AuthOtpSchema> = self.state.surrealdb_mem.select(key).await?;
|
||||||
match result {
|
match result {
|
||||||
Some(data) => match Utc::now() > data.expires_at {
|
Some(data) => match Utc::now() > data.expires_at {
|
||||||
true => {
|
true => {
|
||||||
|
info!(query = %format!("DELETE FROM {} WHERE id = '{}'", table, email), "Executing SurrealDB query");
|
||||||
let _ = self
|
let _ = self
|
||||||
.state
|
.state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
@@ -137,9 +165,11 @@ impl<'a> AuthRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, email, otp), err)]
|
||||||
pub async fn query_store_otp(&self, email: String, otp: u32) -> Result<String> {
|
pub async fn query_store_otp(&self, email: String, otp: u32) -> Result<String> {
|
||||||
let expires_at = Utc::now() + Duration::seconds(300);
|
let expires_at = Utc::now() + Duration::seconds(300);
|
||||||
let table: String = ResourceEnum::OtpCache.to_string();
|
let table: String = ResourceEnum::OtpCache.to_string();
|
||||||
|
info!(query = %format!("CREATE {}:{}", table, email), "Executing SurrealDB query");
|
||||||
let record: Option<AuthOtpSchema> = self
|
let record: Option<AuthOtpSchema> = self
|
||||||
.state
|
.state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
@@ -152,7 +182,9 @@ impl<'a> AuthRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, email), err)]
|
||||||
pub async fn query_delete_stored_otp(&self, email: String) -> Result<String> {
|
pub async fn query_delete_stored_otp(&self, email: String) -> Result<String> {
|
||||||
|
info!(query = %format!("DELETE FROM {} WHERE id = '{}'", ResourceEnum::OtpCache.to_string(), email), "Executing SurrealDB query");
|
||||||
let record: Option<AuthOtpSchema> = self
|
let record: Option<AuthOtpSchema> = self
|
||||||
.state
|
.state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
|
|||||||
@@ -1,38 +1,88 @@
|
|||||||
|
use std::pin::Pin;
|
||||||
|
use std::future::Future;
|
||||||
use super::{
|
use super::{
|
||||||
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
|
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
|
||||||
AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthRepository,
|
AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthRepository,
|
||||||
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto,
|
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, Env, ResourceEnum, ResponseSuccessDto, RolesEnum, RolesRepository,
|
AppState, ResourceEnum, ResponseSuccessDto, RolesEnum, RolesRepository,
|
||||||
UsersDetailItemDto, UsersRepository, UsersSchema, common_response,
|
UsersDetailItemDto, UsersRepository, UsersSchema, common_response,
|
||||||
decode_refresh_token, encode_access_token, encode_refresh_token,
|
decode_refresh_token, encode_access_token, encode_refresh_token,
|
||||||
encode_reset_password_token, extract_email_token, generate_otp, get_iso_date,
|
encode_reset_password_token, extract_email_token_async, generate_otp, get_iso_date,
|
||||||
hash_password, make_thing, send_email, success_response, validate_request,
|
hash_password, make_thing, send_email, success_response, validate_request,
|
||||||
verify_password,
|
verify_password,
|
||||||
};
|
};
|
||||||
use axum::{http::StatusCode, response::Response};
|
use axum::{http::StatusCode, response::Response};
|
||||||
use surrealdb::Uuid;
|
use surrealdb::Uuid;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
use tokio;
|
||||||
|
|
||||||
|
|
||||||
|
pub trait AuthServiceTrait: Send + Sync + 'static {
|
||||||
|
fn mutation_login(
|
||||||
|
payload: AuthLoginRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn mutation_mentor_login(
|
||||||
|
payload: AuthLoginRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn mutation_register(
|
||||||
|
payload: AuthRegisterRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn mutation_resend_otp(
|
||||||
|
payload: AuthResendOtpRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn mutation_refresh_token(
|
||||||
|
payload: AuthRefreshTokenRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn mutation_forgot_password(
|
||||||
|
payload: AuthResendOtpRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn mutation_verify_email(
|
||||||
|
payload: AuthVerifyEmailRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn mutation_new_password(
|
||||||
|
payload: AuthNewPasswordRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)] // Added Clone derive
|
||||||
pub struct AuthService;
|
pub struct AuthService;
|
||||||
|
|
||||||
impl AuthService {
|
|
||||||
pub async fn mutation_login(
|
impl AuthServiceTrait for AuthService {
|
||||||
|
fn mutation_login(
|
||||||
payload: AuthLoginRequestDto,
|
payload: AuthLoginRequestDto,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let payload = payload;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_repo = UsersRepository::new(state);
|
let user_repo = UsersRepository::new(&state);
|
||||||
let auth_repo = AuthRepository::new(state);
|
let auth_repo = AuthRepository::new(&state);
|
||||||
|
|
||||||
match user_repo.query_user_by_email(payload.email.clone()).await {
|
let email = &payload.email;
|
||||||
|
let password = &payload.password;
|
||||||
|
|
||||||
|
match user_repo.query_user_by_email(email.to_string()).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let is_password_correct =
|
let is_password_correct = tokio::task::spawn_blocking({
|
||||||
verify_password(&payload.password, &user.password).unwrap_or(false);
|
let password = password.to_owned();
|
||||||
|
let user_password = user.password.clone();
|
||||||
|
move || verify_password(&password, &user_password).unwrap_or(false)
|
||||||
|
}).await.unwrap_or(false);
|
||||||
|
|
||||||
if !is_password_correct {
|
if !is_password_correct {
|
||||||
return common_response(
|
return common_response(
|
||||||
@@ -48,12 +98,16 @@ impl AuthService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let access_token = match encode_access_token(payload.email.clone()) {
|
// Avoid unnecessary clone of user for caching if not needed
|
||||||
|
let permissions: Vec<String> = user.role.permissions.iter().map(|p| p.name.as_str()).map(str::to_owned).collect();
|
||||||
|
let user_id = user.id.id.to_raw();
|
||||||
|
|
||||||
|
let access_token = match encode_access_token(email.to_string(), user_id.clone(), permissions.clone()) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
error!(
|
error!(
|
||||||
"Failed to generate access token for {}: {}",
|
"Failed to generate access token for {}: {}",
|
||||||
payload.email, _e
|
email, _e
|
||||||
);
|
);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -62,12 +116,12 @@ impl AuthService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
let refresh_token = match encode_refresh_token(email.to_string(), user_id, permissions) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
error!(
|
error!(
|
||||||
"Failed to generate refresh token for {}: {}",
|
"Failed to generate refresh token for {}: {}",
|
||||||
payload.email, _e
|
email, _e
|
||||||
);
|
);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -86,6 +140,7 @@ impl AuthService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only clone user if caching is required
|
||||||
if let Err(err_store) = auth_repo.query_store_user(user.clone()).await {
|
if let Err(err_store) = auth_repo.query_store_user(user.clone()).await {
|
||||||
error!(
|
error!(
|
||||||
"Failed to store user cache for {}: {}",
|
"Failed to store user cache for {}: {}",
|
||||||
@@ -102,23 +157,30 @@ impl AuthService {
|
|||||||
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mutation_mentor_login(
|
fn mutation_mentor_login(
|
||||||
payload: AuthLoginRequestDto,
|
payload: AuthLoginRequestDto,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let payload = payload;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_repo = UsersRepository::new(state);
|
let user_repo = UsersRepository::new(&state);
|
||||||
let auth_repo = AuthRepository::new(state);
|
let auth_repo = AuthRepository::new(&state);
|
||||||
|
|
||||||
match user_repo.query_user_by_email(payload.email.clone()).await {
|
match user_repo.query_user_by_email(payload.email.clone()).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let is_password_correct =
|
let is_password_correct = tokio::task::spawn_blocking({
|
||||||
verify_password(&payload.password, &user.password).unwrap_or(false);
|
let password = payload.password.clone();
|
||||||
|
let user_password = user.password.clone();
|
||||||
|
move || verify_password(&password, &user_password).unwrap_or(false)
|
||||||
|
}).await.unwrap_or(false);
|
||||||
|
|
||||||
if !is_password_correct {
|
if !is_password_correct {
|
||||||
return common_response(
|
return common_response(
|
||||||
@@ -143,7 +205,8 @@ impl AuthService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let access_token = match encode_access_token(payload.email.clone()) {
|
let permissions: Vec<String> = user.role.permissions.iter().map(|p| p.name.clone()).collect();
|
||||||
|
let access_token = match encode_access_token(payload.email.clone(), user.id.id.to_raw(), permissions.clone()) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -157,7 +220,8 @@ impl AuthService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
let permissions: Vec<String> = user.role.permissions.iter().map(|p| p.name.clone()).collect();
|
||||||
|
let refresh_token = match encode_refresh_token(payload.email.clone(), user.id.id.to_raw(), permissions) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -197,18 +261,22 @@ impl AuthService {
|
|||||||
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mutation_register(
|
fn mutation_register(
|
||||||
payload: AuthRegisterRequestDto,
|
payload: AuthRegisterRequestDto,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let payload = payload;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let user_repo = UsersRepository::new(state);
|
let user_repo = UsersRepository::new(&state);
|
||||||
let auth_repo = AuthRepository::new(state);
|
let auth_repo = AuthRepository::new(&state);
|
||||||
let role_repo = RolesRepository::new(state);
|
let role_repo = RolesRepository::new(&state);
|
||||||
let role = match role_repo
|
let role = match role_repo
|
||||||
.query_role_by_name(RolesEnum::User.to_string())
|
.query_role_by_name(RolesEnum::User.to_string())
|
||||||
.await
|
.await
|
||||||
@@ -285,7 +353,7 @@ impl AuthService {
|
|||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
updated_at: get_iso_date(),
|
updated_at: get_iso_date(),
|
||||||
role: role_thing,
|
role: role_thing,
|
||||||
is_active: true,
|
is_active: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -296,16 +364,20 @@ impl AuthService {
|
|||||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err_create.to_string())
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err_create.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mutation_resend_otp(
|
fn mutation_resend_otp(
|
||||||
payload: AuthResendOtpRequestDto,
|
payload: AuthResendOtpRequestDto,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let payload = payload;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let user_repo = UsersRepository::new(state);
|
let user_repo = UsersRepository::new(&state);
|
||||||
if user_repo
|
if user_repo
|
||||||
.query_user_by_email(payload.email.clone())
|
.query_user_by_email(payload.email.clone())
|
||||||
.await
|
.await
|
||||||
@@ -313,7 +385,7 @@ impl AuthService {
|
|||||||
{
|
{
|
||||||
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
}
|
}
|
||||||
let auth_repo = AuthRepository::new(state);
|
let auth_repo = AuthRepository::new(&state);
|
||||||
let _ = auth_repo.query_get_stored_otp(payload.email.clone()).await;
|
let _ = auth_repo.query_get_stored_otp(payload.email.clone()).await;
|
||||||
let otp = generate_otp::OtpManager::generate_otp();
|
let otp = generate_otp::OtpManager::generate_otp();
|
||||||
let message = format!("Your OTP code is {otp}");
|
let message = format!("Your OTP code is {otp}");
|
||||||
@@ -333,34 +405,48 @@ impl AuthService {
|
|||||||
common_response(StatusCode::BAD_REQUEST, &err_store.to_string())
|
common_response(StatusCode::BAD_REQUEST, &err_store.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mutation_refresh_token(
|
fn mutation_refresh_token(
|
||||||
payload: AuthRefreshTokenRequestDto,
|
payload: AuthRefreshTokenRequestDto,
|
||||||
) -> Response {
|
state: &AppState,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let payload = payload;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let email = match decode_refresh_token(&payload.refresh_token) {
|
|
||||||
Ok(token) => token.claims.sub,
|
let user_repo = UsersRepository::new(&state);
|
||||||
|
let user = match decode_refresh_token(&payload.refresh_token) {
|
||||||
|
Ok(token_data) => {
|
||||||
|
match user_repo.query_user_by_email(token_data.claims.sub.clone()).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(_) => return common_response(StatusCode::UNAUTHORIZED, "User not found"),
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token");
|
return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let access_token = match encode_access_token(email.clone()) {
|
|
||||||
|
let permissions: Vec<String> = user.role.permissions.iter().map(|p| p.name.clone()).collect();
|
||||||
|
let access_token = match encode_access_token(user.email.clone(), user.id.id.to_raw(), permissions.clone()) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
error!("Failed to generate access token for {}: {}", email, _e);
|
error!("Failed to generate access token for {}: {}", user.email, _e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Failed to generate access token",
|
"Failed to generate access token",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let refresh_token = match encode_refresh_token(email.clone()) {
|
let refresh_token = match encode_refresh_token(user.email.clone(), user.id.id.to_raw(), permissions) {
|
||||||
Ok(token) => token,
|
Ok(token) => token,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
error!("Failed to generate refresh token for {}: {}", email, _e);
|
error!("Failed to generate refresh token for {}: {}", user.email, _e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Failed to generate refresh token",
|
"Failed to generate refresh token",
|
||||||
@@ -374,148 +460,145 @@ impl AuthService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
success_response(response)
|
success_response(response)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mutation_forgot_password(
|
fn mutation_forgot_password(
|
||||||
payload: AuthResendOtpRequestDto,
|
payload: AuthResendOtpRequestDto,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
let payload = payload;
|
||||||
return common_response(status, &message);
|
let state = state.to_owned();
|
||||||
}
|
Box::pin(async move {
|
||||||
let user_repo = UsersRepository::new(state);
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
let user_result = user_repo.query_user_by_email(payload.email.clone()).await;
|
return common_response(status, &message);
|
||||||
let user = match user_result {
|
}
|
||||||
Ok(user) => user,
|
|
||||||
Err(err_find) if err_find.to_string().contains("User not found") => {
|
tokio::spawn(async move {
|
||||||
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
let user_repo = UsersRepository::new(&state);
|
||||||
}
|
if let Ok(user) = user_repo.query_user_by_email(payload.email.clone()).await {
|
||||||
Err(err_other) => {
|
let permissions: Vec<String> = user.role.permissions.iter().map(|p| p.name.clone()).collect();
|
||||||
error!(
|
let token = match encode_reset_password_token(user.email.clone(), user.id.id.to_raw(), permissions) {
|
||||||
"Error finding user for forgot password {}: {}",
|
Ok(token) => token,
|
||||||
payload.email, err_other
|
Err(_e) => {
|
||||||
);
|
error!("Failed to generate reset password token for {}: {}", user.email, _e);
|
||||||
return common_response(
|
return;
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
}
|
||||||
&err_other.to_string(),
|
};
|
||||||
);
|
|
||||||
}
|
let env = &crate::enviroment::ENV;
|
||||||
};
|
let fe_url = env.fe_url.clone();
|
||||||
let token = match encode_reset_password_token(user.email.clone()) {
|
let message = format!(
|
||||||
Ok(token) => token,
|
"You have requested a password reset. Please click the link below to continue: {fe_url}/auth/reset-password?token={token}"
|
||||||
Err(_e) => {
|
);
|
||||||
error!(
|
|
||||||
"Failed to generate reset password token for {}: {}",
|
if let Err(err_send) = send_email(&payload.email, "Reset Password Request", &message) {
|
||||||
user.email, _e
|
error!("Failed to send reset password email to {}: {}", payload.email, err_send);
|
||||||
);
|
}
|
||||||
return common_response(
|
}
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
});
|
||||||
"Failed to generate access token",
|
|
||||||
);
|
common_response(StatusCode::OK, "If your email is registered, you will receive a password reset link.")
|
||||||
}
|
})
|
||||||
};
|
|
||||||
let env = Env::new();
|
|
||||||
let fe_url = env.fe_url;
|
|
||||||
let message = format!(
|
|
||||||
"You have requested a password reset. Please click the link below to continue: {fe_url}/auth/reset-password?token={token}"
|
|
||||||
);
|
|
||||||
match send_email(&payload.email, "Reset Password Request", &message) {
|
|
||||||
Ok(_) => common_response(StatusCode::OK, "Reset Password request send"),
|
|
||||||
Err(err_send) => {
|
|
||||||
error!(
|
|
||||||
"Failed to send reset password email to {}: {}",
|
|
||||||
payload.email, err_send
|
|
||||||
);
|
|
||||||
common_response(StatusCode::BAD_REQUEST, &err_send.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mutation_verify_email(
|
fn mutation_verify_email(
|
||||||
payload: AuthVerifyEmailRequestDto,
|
payload: AuthVerifyEmailRequestDto,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let payload = payload;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let user_repo = UsersRepository::new(state);
|
let user_repo = UsersRepository::new(&state);
|
||||||
let auth_repo = AuthRepository::new(state);
|
let auth_repo = AuthRepository::new(&state);
|
||||||
let email = payload.email.clone();
|
let email = payload.email.clone();
|
||||||
let user = match user_repo.query_user_by_email(email.clone()).await {
|
let user = match user_repo.query_user_by_email(email.clone()).await {
|
||||||
Ok(user) if !user.is_deleted => user,
|
Ok(user) => user,
|
||||||
_ => {
|
_ => {
|
||||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if user.is_active {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User already active");
|
||||||
|
}
|
||||||
|
|
||||||
let patch = UsersSchema {
|
let patch = UsersSchema {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
is_active: true,
|
is_active: true,
|
||||||
..UsersSchema::from(user.clone())
|
..UsersSchema::from(user.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
match auth_repo.query_get_stored_otp(email.clone()).await {
|
match auth_repo.query_get_stored_otp(email.clone()).await {
|
||||||
Ok(stored_otp) => match stored_otp == payload.otp {
|
Ok(stored_otp) => {
|
||||||
true => match user_repo.query_update_user(patch).await {
|
if stored_otp != payload.otp {
|
||||||
Ok(_) => match auth_repo.query_delete_stored_otp(email.clone()).await {
|
// Delete OTP even if it doesn't match
|
||||||
Ok(_) => common_response(StatusCode::OK, "Email verified successfully"),
|
let _ = auth_repo.query_delete_stored_otp(email.clone()).await;
|
||||||
Err(e_del) => {
|
return common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP");
|
||||||
error!("Failed to delete OTP for {}: {}", email, e_del);
|
}
|
||||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e_del.to_string())
|
|
||||||
|
match user_repo.query_update_user(patch).await {
|
||||||
|
Ok(_) => {
|
||||||
|
match auth_repo.query_delete_stored_otp(email.clone()).await {
|
||||||
|
Ok(_) => common_response(StatusCode::OK, "Email verified successfully"),
|
||||||
|
Err(e_del) => {
|
||||||
|
error!("Failed to delete OTP for {}: {}", email, e_del);
|
||||||
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e_del.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err_update) => {
|
Err(err_update) => common_response(StatusCode::BAD_REQUEST, &err_update.to_string()),
|
||||||
common_response(StatusCode::BAD_REQUEST, &err_update.to_string())
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
false => match auth_repo.query_delete_stored_otp(email.clone()).await {
|
|
||||||
Ok(_) => common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP"),
|
|
||||||
Err(e_del_mismatch) => common_response(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
&format!("Failed to delete OTP: {e_del_mismatch}"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Err(err_get) => common_response(StatusCode::BAD_REQUEST, &err_get.to_string()),
|
Err(err_get) => common_response(StatusCode::BAD_REQUEST, &err_get.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mutation_new_password(
|
fn mutation_new_password(
|
||||||
payload: AuthNewPasswordRequestDto,
|
payload: AuthNewPasswordRequestDto,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let payload = payload;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let repo = UsersRepository::new(state);
|
let repo = UsersRepository::new(&state);
|
||||||
let email = match extract_email_token(payload.token.clone()) {
|
let user_repo = UsersRepository::new(&state);
|
||||||
Some(token) => token,
|
let email = match extract_email_token_async(payload.token.clone()).await {
|
||||||
|
Some(email) => email,
|
||||||
None => {
|
None => {
|
||||||
return common_response(StatusCode::BAD_REQUEST, "Invalid or missing token");
|
return common_response(StatusCode::BAD_REQUEST, "Invalid or missing token");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let user = match user_repo.query_user_by_email(email).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(_) => return common_response(StatusCode::BAD_REQUEST, "User not found"),
|
||||||
|
};
|
||||||
let password = match hash_password(&payload.password) {
|
let password = match hash_password(&payload.password) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
error!("Failed to hash new password for {}: {}", email, _e);
|
error!("Failed to hash new password for {}: {}", user.email, _e);
|
||||||
return common_response(
|
return common_response(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Failed to hash password",
|
"Failed to hash password",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let user = match repo.query_user_by_email(email.clone()).await {
|
|
||||||
Ok(user) if !user.is_deleted => user,
|
|
||||||
_ => {
|
|
||||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let patch = UsersSchema {
|
let patch = UsersSchema {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
password,
|
password,
|
||||||
..Default::default()
|
..UsersSchema::from(user.clone())
|
||||||
};
|
};
|
||||||
match repo.query_update_user(patch).await {
|
match repo.query_update_user(patch).await {
|
||||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
Err(_e) => common_response(StatusCode::BAD_REQUEST, &_e.to_string()),
|
Err(_e) => common_response(StatusCode::BAD_REQUEST, &_e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
response::Redirect,
|
||||||
|
routing::get,
|
||||||
|
Json, Router, Extension,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use imphnen_libs::enviroment::ENV; // Import ENV
|
||||||
|
|
||||||
|
use crate::v1::auth::google::google_oauth_service::{AuthRequest, GoogleOauthService, GoogleOauthServiceImpl};
|
||||||
|
use imphnen_entities::error_dto::error::Error;
|
||||||
|
use crate::v1::auth::AuthLoginResponsetDto;
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GoogleAuthUrlResponse {
|
||||||
|
pub authorize_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GoogleLoginRequest {
|
||||||
|
pub redirect_uri: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GoogleOauthController<T> {
|
||||||
|
google_oauth_service: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GoogleOauthController<GoogleOauthServiceImpl<crate::v1::auth::auth_service::AuthService, crate::v1::users::users_service::UsersService>> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let google_oauth_service = GoogleOauthServiceImpl::<crate::v1::auth::auth_service::AuthService, crate::v1::users::users_service::UsersService>::with_services(
|
||||||
|
crate::v1::auth::auth_service::AuthService {},
|
||||||
|
crate::v1::users::users_service::UsersService {},
|
||||||
|
&ENV, // Pass a reference to the global ENV static
|
||||||
|
);
|
||||||
|
Self::with_service(google_oauth_service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GoogleOauthController<T>
|
||||||
|
where
|
||||||
|
T: GoogleOauthService<crate::v1::auth::auth_service::AuthService, crate::v1::users::users_service::UsersService> + Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn with_service(google_oauth_service: T) -> Self {
|
||||||
|
Self {
|
||||||
|
google_oauth_service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_routes(&self) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/login",
|
||||||
|
get(
|
||||||
|
move |State(controller): State<Arc<Self>>, Query(params): Query<GoogleLoginRequest>| async move {
|
||||||
|
controller.google_oauth_login(params).await
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/callback",
|
||||||
|
get(
|
||||||
|
move |State(controller): State<Arc<Self>>, Extension(app_state): Extension<AppState>, Query(auth_request): Query<AuthRequest>| async move {
|
||||||
|
let controller = Arc::clone(&controller);
|
||||||
|
controller.google_oauth_callback(auth_request, &app_state).await
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_state(Arc::new(self.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn google_oauth_login(&self, params: GoogleLoginRequest) -> Result<Redirect, Error> {
|
||||||
|
let (authorize_url, _csrf_state) = self.google_oauth_service.generate_auth_url(params.redirect_uri);
|
||||||
|
Ok(Redirect::to(authorize_url.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn google_oauth_callback(&self, auth_request: AuthRequest, app_state: &AppState) -> Result<Json<AuthLoginResponsetDto>, Error> {
|
||||||
|
let (user, token) = self.google_oauth_service.google_oauth_callback(auth_request, app_state).await?;
|
||||||
|
let auth_response = AuthLoginResponsetDto {
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
};
|
||||||
|
Ok(Json(auth_response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for GoogleOauthController<T>
|
||||||
|
where
|
||||||
|
T: GoogleOauthService<crate::v1::auth::auth_service::AuthService, crate::v1::users::users_service::UsersService> + Clone,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self::with_service(self.google_oauth_service.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GoogleUser {
|
||||||
|
pub id: String,
|
||||||
|
pub email: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub verified_email: bool,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub given_name: Option<String>,
|
||||||
|
pub family_name: Option<String>,
|
||||||
|
pub picture: Option<String>,
|
||||||
|
pub locale: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GoogleTokenResponse {
|
||||||
|
pub access_token: String,
|
||||||
|
pub expires_in: u64,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub scope: String,
|
||||||
|
pub token_type: String,
|
||||||
|
pub id_token: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
use std::pin::Pin;
|
||||||
|
use std::future::Future;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use oauth2::{
|
||||||
|
AuthUrl, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, PkceCodeVerifier,
|
||||||
|
RedirectUrl, Scope, TokenUrl,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use oauth2::url::Url;
|
||||||
|
use oauth2::TokenResponse;
|
||||||
|
use tracing::{info, error};
|
||||||
|
|
||||||
|
use imphnen_entities::error_dto::error::Error;
|
||||||
|
use imphnen_libs::{jsonwebtoken::{encode_access_token, encode_refresh_token}, enviroment::Env, AppState};
|
||||||
|
use imphnen_utils::{generate_oauth_csrf_token, validate_oauth_csrf_token, validate_csrf_token};
|
||||||
|
use crate::v1::auth::TokenDto;
|
||||||
|
use crate::v1::auth::auth_service::AuthServiceTrait;
|
||||||
|
use crate::v1::users::users_dto::{UsersDetailItemDto, UsersCreateRequestDto};
|
||||||
|
use crate::v1::users::users_service::UsersServiceTrait;
|
||||||
|
|
||||||
|
use super::google_oauth_dto::GoogleUser;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
pub code: String,
|
||||||
|
pub state: String,
|
||||||
|
pub redirect_uri: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthRequest {
|
||||||
|
/// Validate the OAuth callback request
|
||||||
|
pub fn validate(&self) -> Result<(), Error> {
|
||||||
|
// Validate code parameter
|
||||||
|
if self.code.is_empty() || self.code.len() > 2048 {
|
||||||
|
return Err(Error::Validation("Invalid authorization code".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate state parameter
|
||||||
|
if self.state.is_empty() || self.state.len() > 512 {
|
||||||
|
return Err(Error::Validation("Invalid state parameter".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic format validation for authorization code
|
||||||
|
// OAuth 2.0 authorization codes can contain URL-safe characters including base64 characters
|
||||||
|
if !self.code.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~' || c == '/' || c == '+' || c == '=') {
|
||||||
|
return Err(Error::Validation("Authorization code contains invalid characters".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate CSRF state token with signature verification and extract PKCE verifier
|
||||||
|
pub fn validate_csrf_state_and_get_pkce_verifier(&self, secret: &str) -> Result<PkceCodeVerifier, Error> {
|
||||||
|
// Maximum age of 30 minutes for OAuth flow (increased from 10)
|
||||||
|
const MAX_AGE_SECONDS: u64 = 300; // Changed from 30 minutes (1800s) to 5 minutes (300s)
|
||||||
|
|
||||||
|
let pkce_verifier_secret = validate_oauth_csrf_token(&self.state, secret, MAX_AGE_SECONDS)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("OAuth CSRF validation failed: {:?}", e);
|
||||||
|
Error::Auth("Invalid or expired OAuth CSRF state token".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(PkceCodeVerifier::new(pkce_verifier_secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate CSRF state token with signature verification (legacy method for backward compatibility)
|
||||||
|
pub fn validate_csrf_state(&self, secret: &str) -> Result<(), Error> {
|
||||||
|
// Try OAuth CSRF validation first, if it fails, fall back to regular CSRF validation
|
||||||
|
match validate_oauth_csrf_token(&self.state, secret, 1800) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => {
|
||||||
|
// Fallback to regular CSRF validation for backward compatibility
|
||||||
|
validate_csrf_token(&self.state, secret, 600)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("CSRF validation failed: {:?}", e);
|
||||||
|
Error::Auth("Invalid or expired CSRF state token".to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::{RolesRepository, RolesEnum};
|
||||||
|
|
||||||
|
/// Helper function to get default role ID for new OAuth users
|
||||||
|
async fn get_default_role_id(app_state: &AppState) -> Result<String, Error> {
|
||||||
|
let role_repo = RolesRepository::new(app_state);
|
||||||
|
match role_repo.query_role_by_name(RolesEnum::User.to_string()).await {
|
||||||
|
Ok(role) => {
|
||||||
|
info!("Using default User role ID: {}", role.id);
|
||||||
|
Ok(role.id)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to retrieve User role: {:?}", e);
|
||||||
|
Err(Error::Anyhow(anyhow::Error::msg("Failed to get default role ID".to_string())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub trait GoogleOauthService<A: AuthServiceTrait + Send + Sync + 'static, U: UsersServiceTrait + Send + Sync + 'static>: Send + Sync + 'static {
|
||||||
|
// Removed new() from trait
|
||||||
|
fn with_services(auth_service: A, users_service: U, env: &'static Env) -> Self;
|
||||||
|
fn generate_auth_url(&self, custom_redirect_uri: Option<String>) -> (Url, CsrfToken);
|
||||||
|
fn google_oauth_callback(&self, auth_request: AuthRequest, app_state: &AppState) -> Pin<Box<dyn Future<Output = Result<(UsersDetailItemDto, TokenDto), Error>> + Send + '_>>; // Changed return type
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GoogleOauthServiceImpl<A: AuthServiceTrait, U: UsersServiceTrait> {
|
||||||
|
users_service: U,
|
||||||
|
env: &'static Env,
|
||||||
|
_auth_service: A,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GoogleOauthServiceImpl<crate::v1::auth::auth_service::AuthService, crate::v1::users::users_service::UsersService> {
|
||||||
|
/// Generate Google OAuth authorization URL
|
||||||
|
pub fn get_auth_url(&self, custom_redirect_uri: Option<String>) -> String {
|
||||||
|
let (auth_url, _csrf_token) = self.generate_auth_url(custom_redirect_uri);
|
||||||
|
auth_url.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
impl<A, U> GoogleOauthService<A, U> for GoogleOauthServiceImpl<A, U>
|
||||||
|
where
|
||||||
|
A: AuthServiceTrait + Send + Sync + 'static,
|
||||||
|
U: UsersServiceTrait + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn with_services(auth_service: A, users_service: U, env: &'static Env) -> Self {
|
||||||
|
Self {
|
||||||
|
_auth_service: auth_service,
|
||||||
|
users_service,
|
||||||
|
env,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn generate_auth_url(&self, custom_redirect_uri: Option<String>) -> (Url, CsrfToken) {
|
||||||
|
let google_client_id = ClientId::new(self.env.google_client_id.clone());
|
||||||
|
let google_client_secret = ClientSecret::new(self.env.google_client_secret.clone());
|
||||||
|
let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string())
|
||||||
|
.expect("Invalid authorization endpoint URL");
|
||||||
|
let token_url = TokenUrl::new("https://oauth2.googleapis.com/token".to_string())
|
||||||
|
.expect("Invalid token endpoint URL");
|
||||||
|
let redirect_uri = custom_redirect_uri.unwrap_or_else(|| self.env.google_redirect_url.clone());
|
||||||
|
let client = oauth2::basic::BasicClient::new(google_client_id)
|
||||||
|
.set_client_secret(google_client_secret)
|
||||||
|
.set_auth_uri(auth_url)
|
||||||
|
.set_token_uri(token_url)
|
||||||
|
.set_redirect_uri(
|
||||||
|
RedirectUrl::new(redirect_uri.clone())
|
||||||
|
.expect("Invalid redirect URL"),
|
||||||
|
);
|
||||||
|
info!("OAuth client configured with redirect URI: {}", redirect_uri);
|
||||||
|
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
info!("Generated PKCE Code Challenge: {}", pkce_code_challenge.as_str());
|
||||||
|
info!("Generated PKCE Code Verifier: {}", pkce_code_verifier.secret());
|
||||||
|
|
||||||
|
// Generate a signed CSRF token with PKCE verifier for stateless validation
|
||||||
|
let csrf_token_str = generate_oauth_csrf_token(&self.env.access_token_secret, pkce_code_verifier.secret())
|
||||||
|
.unwrap_or_else(|_| uuid::Uuid::new_v4().to_string()); // Fallback to UUID if signing fails
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let (auth_url, csrf_token) = client
|
||||||
|
.authorize_url(|| CsrfToken::new(csrf_token_str.clone()))
|
||||||
|
.add_scope(Scope::new("https://www.googleapis.com/auth/userinfo.email".to_string()))
|
||||||
|
.add_scope(Scope::new("https://www.googleapis.com/auth/userinfo.profile".to_string()))
|
||||||
|
.set_pkce_challenge(pkce_code_challenge)
|
||||||
|
.url();
|
||||||
|
(auth_url, csrf_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn google_oauth_callback(&self, auth_request: AuthRequest, app_state: &AppState) -> Pin<Box<dyn Future<Output = Result<(UsersDetailItemDto, TokenDto), Error>> + Send + '_>> {
|
||||||
|
let self_clone = self; // Use reference instead of clone
|
||||||
|
let app_state = app_state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
// Validate input parameters first
|
||||||
|
info!("Received OAuth callback request with state: {}", auth_request.state);
|
||||||
|
auth_request.validate()?;
|
||||||
|
|
||||||
|
// CRITICAL: Validate CSRF state token and extract PKCE verifier
|
||||||
|
let pkce_verifier = auth_request.validate_csrf_state_and_get_pkce_verifier(&self_clone.env.access_token_secret)?;
|
||||||
|
|
||||||
|
info!("Starting Google OAuth callback process");
|
||||||
|
info!("Redirect URI used: {:?}", auth_request.redirect_uri);
|
||||||
|
info!("PKCE verifier extracted: {}", pkce_verifier.secret());
|
||||||
|
info!("PKCE verifier extracted: {}", pkce_verifier.secret());
|
||||||
|
|
||||||
|
// Use the SAME redirect URI that was used for auth URL generation
|
||||||
|
// This is crucial for OAuth security and consistency
|
||||||
|
let google_client_id = ClientId::new(self_clone.env.google_client_id.clone());
|
||||||
|
let google_client_secret = ClientSecret::new(self_clone.env.google_client_secret.clone());
|
||||||
|
let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string())
|
||||||
|
.expect("Invalid authorization endpoint URL");
|
||||||
|
let token_url = TokenUrl::new("https://oauth2.googleapis.com/token".to_string())
|
||||||
|
.expect("Invalid token endpoint URL");
|
||||||
|
let redirect_uri = auth_request.redirect_uri.clone().unwrap_or_else(|| self_clone.env.google_redirect_url.clone());
|
||||||
|
let client = oauth2::basic::BasicClient::new(google_client_id)
|
||||||
|
.set_client_secret(google_client_secret)
|
||||||
|
.set_auth_uri(auth_url)
|
||||||
|
.set_token_uri(token_url)
|
||||||
|
.set_redirect_uri(
|
||||||
|
RedirectUrl::new(redirect_uri.clone())
|
||||||
|
.expect("Invalid redirect URL"),
|
||||||
|
);
|
||||||
|
info!("OAuth client configured with redirect URI: {}", redirect_uri);
|
||||||
|
|
||||||
|
// Debug the OAuth client configuration
|
||||||
|
let effective_redirect_uri = auth_request.redirect_uri.as_ref().unwrap_or(&self_clone.env.google_redirect_url);
|
||||||
|
info!("Effective redirect URI for OAuth client: {}", effective_redirect_uri);
|
||||||
|
info!("Google Client ID: {}", self_clone.env.google_client_id);
|
||||||
|
|
||||||
|
info!("Attempting to exchange authorization code with Google");
|
||||||
|
info!("Using PKCE verifier for secure exchange");
|
||||||
|
info!("Authorization code length: {}", auth_request.code.len());
|
||||||
|
|
||||||
|
let token_response = client
|
||||||
|
.exchange_code(oauth2::AuthorizationCode::new(auth_request.code.clone()))
|
||||||
|
.set_pkce_verifier(pkce_verifier)
|
||||||
|
.request_async(&reqwest::Client::new())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to exchange OAuth code with Google: {}", e);
|
||||||
|
error!("OAuth code was: {}", auth_request.code);
|
||||||
|
error!("Redirect URI was: {:?}", auth_request.redirect_uri);
|
||||||
|
|
||||||
|
// Debug OAuth client configuration
|
||||||
|
error!("Google Client ID: {}", self_clone.env.google_client_id);
|
||||||
|
error!("OAuth client redirect URI configured: {}",
|
||||||
|
auth_request.redirect_uri.as_ref().unwrap_or(&self_clone.env.google_redirect_url));
|
||||||
|
|
||||||
|
// Try to extract more details from the error
|
||||||
|
match &e {
|
||||||
|
oauth2::RequestTokenError::ServerResponse(response) => {
|
||||||
|
error!("Google OAuth Server Response Error: {:?}", response);
|
||||||
|
},
|
||||||
|
oauth2::RequestTokenError::Request(req_err) => {
|
||||||
|
error!("Google OAuth Request Error: {:?}", req_err);
|
||||||
|
},
|
||||||
|
oauth2::RequestTokenError::Parse(parse_err, response_body) => {
|
||||||
|
error!("Google OAuth Parse Error: {:?}", parse_err);
|
||||||
|
error!("Response body: {:?}", response_body);
|
||||||
|
},
|
||||||
|
oauth2::RequestTokenError::Other(other) => {
|
||||||
|
error!("Google OAuth Other Error: {:?}", other);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Error::Auth("Authentication error: Failed to exchange authorization code".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
info!("Successfully exchanged authorization code for access token");
|
||||||
|
|
||||||
|
// Extract access token from Google's response
|
||||||
|
let access_token = token_response.access_token().secret();
|
||||||
|
info!("Obtained access token from Google, fetching user info...");
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let user_info_url = "https://www.googleapis.com/oauth2/v2/userinfo";
|
||||||
|
let google_user: GoogleUser = client
|
||||||
|
.get(user_info_url)
|
||||||
|
.bearer_auth(access_token)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to fetch user info from Google: {}", e);
|
||||||
|
Error::Auth("Failed to fetch user information".to_string())
|
||||||
|
})?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to parse user info from Google: {}", e);
|
||||||
|
Error::Auth("Failed to parse user information".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
info!("Successfully retrieved user info for email: {}", google_user.email);
|
||||||
|
info!("Google user picture URL: {:?}", google_user.picture);
|
||||||
|
info!("Google user data: name={:?}, given_name={:?}, family_name={:?}, picture={:?}",
|
||||||
|
google_user.name, google_user.given_name, google_user.family_name, google_user.picture);
|
||||||
|
|
||||||
|
let user = self_clone.users_service.get_user_by_email(&google_user.email, &app_state).await?;
|
||||||
|
|
||||||
|
let user = match user {
|
||||||
|
Some(mut user) => {
|
||||||
|
info!("Existing user found for email: {}", google_user.email);
|
||||||
|
|
||||||
|
// Update avatar if user doesn't have one and Google provides one
|
||||||
|
if user.avatar.is_none() && google_user.picture.is_some() {
|
||||||
|
info!("Updating avatar for existing user: {}", google_user.email);
|
||||||
|
match U::update_user_avatar(&google_user.email, google_user.picture.clone(), &app_state).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Successfully updated avatar for user: {}", google_user.email);
|
||||||
|
user.avatar = google_user.picture.clone();
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to update avatar for user {}: {:?}", google_user.email, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
info!("Creating new user for email: {}", google_user.email);
|
||||||
|
|
||||||
|
// Get default role ID using robust lookup
|
||||||
|
let default_role_id = get_default_role_id(&app_state).await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to get default role ID: {:?}", e);
|
||||||
|
Error::Anyhow(anyhow::Error::msg("Failed to get default role ID for new user".to_string()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let new_user = UsersCreateRequestDto {
|
||||||
|
email: google_user.email.clone(),
|
||||||
|
password: format!("GOOGLE_OAUTH_{}", uuid::Uuid::new_v4()), // Random placeholder
|
||||||
|
fullname: google_user.name.clone().unwrap_or_else(|| {
|
||||||
|
// Fallback: use given_name + family_name if available, otherwise use email prefix
|
||||||
|
match (&google_user.given_name, &google_user.family_name) {
|
||||||
|
(Some(given), Some(family)) => format!("{} {}", given, family),
|
||||||
|
(Some(given), None) => given.clone(),
|
||||||
|
(None, Some(family)) => family.clone(),
|
||||||
|
(None, None) => {
|
||||||
|
// Extract email prefix as last resort
|
||||||
|
google_user.email.split('@').next().unwrap_or("User").to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
phone_number: "".to_string(), // Will be updated by user later
|
||||||
|
is_active: true,
|
||||||
|
role_id: default_role_id,
|
||||||
|
avatar: google_user.picture.clone(), // Set avatar from Google user picture
|
||||||
|
};
|
||||||
|
|
||||||
|
self_clone.users_service.create_user_by_dto(new_user, &app_state).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let permissions: Vec<String> = user.role.permissions.iter().map(|p| p.name.clone()).collect();
|
||||||
|
let access_token = encode_access_token(user.email.clone(), user.id.clone(), permissions.clone())
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to generate access token for {}: {:?}", user.email, e);
|
||||||
|
Error::Auth("Failed to generate access token".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let permissions: Vec<String> = user.role.permissions.iter().map(|p| p.name.clone()).collect();
|
||||||
|
let refresh_token = encode_refresh_token(user.email.clone(), user.id.clone(), permissions)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to generate refresh token for {}: {:?}", user.email, e);
|
||||||
|
Error::Auth("Failed to generate refresh token".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let token_dto = TokenDto {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache the user in auth repository for subsequent requests
|
||||||
|
let auth_repo = crate::v1::auth::AuthRepository::new(&app_state);
|
||||||
|
let user_query_dto: crate::v1::users::users_dto::UsersDetailQueryDto = (&user).into();
|
||||||
|
if let Err(err_store) = auth_repo.query_store_user(user_query_dto).await {
|
||||||
|
error!(
|
||||||
|
"Failed to store user cache for {}: {}",
|
||||||
|
user.email, err_store
|
||||||
|
);
|
||||||
|
// Don't fail the login, just log the error
|
||||||
|
error!("Google OAuth login succeeded but caching failed for user: {}", user.email);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
info!("Successfully cached user {} after Google OAuth login", user.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Successfully completed Google OAuth for user: {}", user.email);
|
||||||
|
Ok((user, token_dto))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod google_oauth_controller;
|
||||||
|
pub mod google_oauth_dto;
|
||||||
|
pub mod google_oauth_service;
|
||||||
@@ -5,6 +5,7 @@ pub mod auth_dto;
|
|||||||
pub mod auth_repository;
|
pub mod auth_repository;
|
||||||
pub mod auth_schema;
|
pub mod auth_schema;
|
||||||
pub mod auth_service;
|
pub mod auth_service;
|
||||||
|
pub mod google;
|
||||||
|
|
||||||
pub use auth_dto::*;
|
pub use auth_dto::*;
|
||||||
pub use auth_repository::*;
|
pub use auth_repository::*;
|
||||||
@@ -13,6 +14,7 @@ pub use auth_service::*;
|
|||||||
|
|
||||||
pub fn auth_router() -> Router {
|
pub fn auth_router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.nest("/google", google::google_oauth_controller::GoogleOauthController::new().get_routes())
|
||||||
.route("/forgot", post(auth_controller::post_forgot_password))
|
.route("/forgot", post(auth_controller::post_forgot_password))
|
||||||
.route("/login", post(auth_controller::post_login))
|
.route("/login", post(auth_controller::post_login))
|
||||||
.route("/login-mentor", post(auth_controller::post_login_mentor))
|
.route("/login-mentor", post(auth_controller::post_login_mentor))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
ResponseSuccessDto,
|
ResponseSuccessDto,
|
||||||
v1::{
|
v1::{
|
||||||
permissions_dto::{PermissionsItemDto, PermissionsRequestDto},
|
permissions_dto::{PermissionsItemDto, PermissionsRequestDto, PermissionsUpdateRequestDto},
|
||||||
permissions_service::PermissionsService,
|
permissions_service::PermissionsService,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -41,13 +41,13 @@ pub async fn get_permission_list(
|
|||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadListPermissions],
|
vec![PermissionsEnum::ReadListPermissions],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => PermissionsService::get_permission_list(&state, meta).await,
|
Ok((_claims, state)) => PermissionsService::get_permission_list(&state, meta).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,13 +70,13 @@ pub async fn get_permission_by_id(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadDetailPermissions],
|
vec![PermissionsEnum::ReadDetailPermissions],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => PermissionsService::get_permission_by_id(&state, id).await,
|
Ok((_claims, state)) => PermissionsService::get_permission_by_id(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,13 +99,13 @@ pub async fn post_create_permission(
|
|||||||
Json(payload): Json<PermissionsRequestDto>,
|
Json(payload): Json<PermissionsRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::CreatePermissions],
|
vec![PermissionsEnum::CreatePermissions],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => PermissionsService::create_role(&state, payload).await,
|
Ok((_claims, state)) => PermissionsService::create_role(&state, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ pub async fn post_create_permission(
|
|||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
path = "/v1/permissions/update/{id}",
|
path = "/v1/permissions/update/{id}",
|
||||||
request_body = PermissionsRequestDto,
|
request_body = PermissionsUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Update permission", body = MessageResponseDto)
|
(status = 200, description = "Update permission", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
@@ -126,16 +126,16 @@ pub async fn put_update_permission(
|
|||||||
headers: axum::http::HeaderMap,
|
headers: axum::http::HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(payload): Json<PermissionsRequestDto>,
|
Json(payload): Json<PermissionsUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::UpdatePermissions],
|
vec![PermissionsEnum::UpdatePermissions],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => PermissionsService::update_permission(&state, payload, id).await,
|
Ok((_claims, state)) => PermissionsService::update_permission(&state, payload, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,13 +157,13 @@ pub async fn delete_permission(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::DeletePermissions],
|
vec![PermissionsEnum::DeletePermissions],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => PermissionsService::delete_permission(&state, id).await,
|
Ok((_claims, state)) => PermissionsService::delete_permission(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ pub struct PermissionsRequestDto {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct PermissionsUpdateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Permission name must not be empty"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct PermissionsItemDto {
|
pub struct PermissionsItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@@ -1,38 +1,41 @@
|
|||||||
use super::PermissionsEnum;
|
use super::PermissionsEnum;
|
||||||
use crate::{AppState, AuthRepository, common_response, extract_email};
|
use crate::{AppState, common_response, decode_access_token};
|
||||||
use axum::{
|
use axum::{
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
response::Response,
|
response::Response, Extension,
|
||||||
};
|
};
|
||||||
|
use axum_extra::headers::{authorization::Bearer, Authorization, HeaderMapExt};
|
||||||
|
// Removed imphnen_utils::make_thing as it's no longer needed here
|
||||||
|
|
||||||
pub async fn permissions_guard(
|
pub async fn permissions_guard(
|
||||||
headers: &HeaderMap,
|
headers: HeaderMap,
|
||||||
state: AppState,
|
Extension(state): Extension<AppState>,
|
||||||
required_permissions: Vec<PermissionsEnum>,
|
required_permissions: Vec<PermissionsEnum>,
|
||||||
) -> Result<(), Response> {
|
) -> Result<(imphnen_libs::jsonwebtoken::Claims, AppState), Response> {
|
||||||
let auth_repo = AuthRepository::new(&state);
|
let auth_header = headers
|
||||||
let email = extract_email(headers).ok_or_else(|| {
|
.typed_get::<Authorization<Bearer>>()
|
||||||
common_response(
|
.ok_or_else(|| {
|
||||||
StatusCode::UNAUTHORIZED,
|
common_response(
|
||||||
"Invalid or missing authorization token",
|
StatusCode::UNAUTHORIZED,
|
||||||
)
|
"Invalid or missing authorization token",
|
||||||
})?;
|
)
|
||||||
let raw_user = auth_repo
|
})?;
|
||||||
.query_get_stored_user(email.clone())
|
|
||||||
.await
|
let token = auth_header.token();
|
||||||
|
|
||||||
|
let claims = decode_access_token(token)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
common_response(
|
common_response(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"User session expired or not found",
|
"Invalid or expired token",
|
||||||
)
|
)
|
||||||
})?;
|
})?
|
||||||
let role = raw_user.role;
|
.claims;
|
||||||
let role_permissions: Vec<String> =
|
|
||||||
role.permissions.into_iter().map(|perm| perm.name).collect();
|
|
||||||
|
|
||||||
|
// Use permissions from JWT for the check
|
||||||
for required in &required_permissions {
|
for required in &required_permissions {
|
||||||
let required_str = required.to_string();
|
let required_str = required.to_string();
|
||||||
if !role_permissions.contains(&required_str) {
|
if !claims.permissions.contains(&required_str) {
|
||||||
eprintln!(" MISSING REQUIRED PERMISSION: {required_str}");
|
eprintln!(" MISSING REQUIRED PERMISSION: {required_str}");
|
||||||
return Err(common_response(
|
return Err(common_response(
|
||||||
StatusCode::FORBIDDEN,
|
StatusCode::FORBIDDEN,
|
||||||
@@ -40,5 +43,6 @@ pub async fn permissions_guard(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
Ok((claims, state))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, extract_id};
|
|||||||
use serde_json;
|
use serde_json;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub struct PermissionsRepository<'a> {
|
pub struct PermissionsRepository<'a> {
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
@@ -23,6 +24,7 @@ impl<'a> PermissionsRepository<'a> {
|
|||||||
meta: MetaRequestDto,
|
meta: MetaRequestDto,
|
||||||
) -> Result<ResponseListSuccessDto<Vec<PermissionsItemDto>>> {
|
) -> Result<ResponseListSuccessDto<Vec<PermissionsItemDto>>> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
info!("Executing SurrealDB query: QueryListBuilder for Permissions with meta: {:?}", meta);
|
||||||
let raw_result: ResponseListSuccessDto<Vec<PermissionsSchema>> =
|
let raw_result: ResponseListSuccessDto<Vec<PermissionsSchema>> =
|
||||||
QueryListBuilder::new(
|
QueryListBuilder::new(
|
||||||
&self.state.surrealdb_ws,
|
&self.state.surrealdb_ws,
|
||||||
@@ -61,6 +63,7 @@ impl<'a> PermissionsRepository<'a> {
|
|||||||
) -> Result<PermissionsSchema> {
|
) -> Result<PermissionsSchema> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
info!(id = %id, "Executing SurrealDB select for Permissions");
|
||||||
let result: Option<PermissionsSchema> = db
|
let result: Option<PermissionsSchema> = db
|
||||||
.select((ResourceEnum::Permissions.to_string(), id.clone()))
|
.select((ResourceEnum::Permissions.to_string(), id.clone()))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -82,6 +85,7 @@ impl<'a> PermissionsRepository<'a> {
|
|||||||
id: String,
|
id: String,
|
||||||
) -> Result<PermissionsItemDto> {
|
) -> Result<PermissionsItemDto> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
info!(id = %id, "Executing transformed_query_permission_by_id (delegates to query_permission_by_id)");
|
||||||
let raw_result = self.query_permission_by_id(id.clone()).await?;
|
let raw_result = self.query_permission_by_id(id.clone()).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
@@ -109,6 +113,7 @@ impl<'a> PermissionsRepository<'a> {
|
|||||||
.with_where("name", Some(name.clone()))
|
.with_where("name", Some(name.clone()))
|
||||||
.with_select_fields(vec!["*"]);
|
.with_select_fields(vec!["*"]);
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Option<PermissionsSchema> =
|
let result: Option<PermissionsSchema> =
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -130,6 +135,7 @@ impl<'a> PermissionsRepository<'a> {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
info!("Executing SurrealDB create for Permissions with data: {:?}", data);
|
||||||
let record: Option<PermissionsSchema> = db
|
let record: Option<PermissionsSchema> = db
|
||||||
.create(ResourceEnum::Permissions.to_string())
|
.create(ResourceEnum::Permissions.to_string())
|
||||||
.content(data)
|
.content(data)
|
||||||
@@ -162,6 +168,7 @@ impl<'a> PermissionsRepository<'a> {
|
|||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
..data.clone()
|
..data.clone()
|
||||||
};
|
};
|
||||||
|
info!(record_key = ?record_key, "Executing SurrealDB update for Permissions");
|
||||||
let record: Option<PermissionsSchema> =
|
let record: Option<PermissionsSchema> =
|
||||||
db.update(record_key).merge(merged).await?;
|
db.update(record_key).merge(merged).await?;
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
@@ -188,6 +195,7 @@ impl<'a> PermissionsRepository<'a> {
|
|||||||
bail!("Permission already deleted");
|
bail!("Permission already deleted");
|
||||||
}
|
}
|
||||||
let record_key = get_id(&permission.id)?;
|
let record_key = get_id(&permission.id)?;
|
||||||
|
info!(record_key = ?record_key, "Executing SurrealDB soft delete for Permissions");
|
||||||
let record: Option<PermissionsSchema> = db
|
let record: Option<PermissionsSchema> = db
|
||||||
.update(record_key)
|
.update(record_key)
|
||||||
.merge(serde_json::json!({ "is_deleted": true }))
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{ResourceEnum, make_thing};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::{Uuid, sql::Thing};
|
use surrealdb::{Uuid, sql::Thing};
|
||||||
|
|
||||||
use super::PermissionsItemDto;
|
use super::{PermissionsItemDto, PermissionsQueryDto};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct PermissionsSchema {
|
pub struct PermissionsSchema {
|
||||||
@@ -37,4 +37,14 @@ impl PermissionsSchema {
|
|||||||
updated_at: self.updated_at.clone(),
|
updated_at: self.updated_at.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from(dto: PermissionsQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.name,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: dto.created_at,
|
||||||
|
updated_at: dto.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
|
use imphnen_utils::get_iso_date;
|
||||||
|
|
||||||
use super::PermissionsRequestDto;
|
use super::{PermissionsRequestDto, PermissionsUpdateRequestDto};
|
||||||
|
|
||||||
pub struct PermissionsService;
|
pub struct PermissionsService;
|
||||||
|
|
||||||
@@ -70,21 +71,31 @@ impl PermissionsService {
|
|||||||
|
|
||||||
pub async fn update_permission(
|
pub async fn update_permission(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
payload: PermissionsRequestDto,
|
payload: PermissionsUpdateRequestDto,
|
||||||
id: String,
|
id: String,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let repo = PermissionsRepository::new(state);
|
let repo = PermissionsRepository::new(state);
|
||||||
match repo
|
|
||||||
.query_update_permission(PermissionsSchema {
|
// Get current permission data first
|
||||||
id: make_thing(&ResourceEnum::Permissions.to_string(), &id),
|
let thing_id = make_thing(&ResourceEnum::Permissions.to_string(), &id);
|
||||||
name: payload.name,
|
let current_permission = match repo.query_permission_by_id(id.clone()).await {
|
||||||
..Default::default()
|
Ok(permission) => permission,
|
||||||
})
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Permission not found"),
|
||||||
.await
|
};
|
||||||
{
|
|
||||||
|
let mut updated_permission = current_permission;
|
||||||
|
updated_permission.id = thing_id;
|
||||||
|
updated_permission.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
// Only update fields that are provided
|
||||||
|
if let Some(name) = payload.name {
|
||||||
|
updated_permission.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
match repo.query_update_permission(updated_permission).await {
|
||||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.to_string().contains("not found") {
|
if e.to_string().contains("not found") {
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ pub async fn get_role_list(
|
|||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadListRoles],
|
vec![PermissionsEnum::ReadListRoles],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => RolesService::get_role_list(&state, meta).await,
|
Ok((_claims, state)) => RolesService::get_role_list(&state, meta).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,13 +68,13 @@ pub async fn get_role_by_id(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadDetailRoles],
|
vec![PermissionsEnum::ReadDetailRoles],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => RolesService::get_role_by_id(&state, id).await,
|
Ok((_claims, state)) => RolesService::get_role_by_id(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,13 +97,13 @@ pub async fn post_create_role(
|
|||||||
Json(payload): Json<RolesRequestCreateDto>,
|
Json(payload): Json<RolesRequestCreateDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::CreateRoles],
|
vec![PermissionsEnum::CreateRoles],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => RolesService::create_role(&state, payload).await,
|
Ok((_claims, state)) => RolesService::create_role(&state, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,13 +127,13 @@ pub async fn put_update_role(
|
|||||||
Json(payload): Json<RolesRequestUpdateDto>,
|
Json(payload): Json<RolesRequestUpdateDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::UpdateRoles],
|
vec![PermissionsEnum::UpdateRoles],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => RolesService::update_role(&state, id, payload).await,
|
Ok((_claims, state)) => RolesService::update_role(&state, id, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,13 +155,13 @@ pub async fn delete_role(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::DeleteRoles],
|
vec![PermissionsEnum::DeleteRoles],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => RolesService::delete_role(&state, id).await,
|
Ok((_claims, state)) => RolesService::delete_role(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub struct RolesListItemDto {
|
|||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)] // Added Default derive
|
||||||
pub struct RolesDetailItemDto {
|
pub struct RolesDetailItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ pub fn users_router() -> Router {
|
|||||||
.route("/detail/{id}", get(get_user_by_id))
|
.route("/detail/{id}", get(get_user_by_id))
|
||||||
.route("/update/{id}", put(put_update_user))
|
.route("/update/{id}", put(put_update_user))
|
||||||
.route("/update/me", put(put_update_user_me))
|
.route("/update/me", put(put_update_user_me))
|
||||||
|
.route("/upload", post(upload_file))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
use crate::{AppState, MetaRequestDto, v1::users_service::UsersService};
|
use crate::{AppState, MetaRequestDto};
|
||||||
use crate::{
|
use crate::{
|
||||||
MessageResponseDto, PermissionsEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
MessageResponseDto, PermissionsEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
UsersCreateRequestDto, UsersDetailItemDto, permissions_guard,
|
UsersCreateRequestDto, UsersDetailItemDto, permissions_guard,
|
||||||
};
|
};
|
||||||
use axum::extract::{Path, Query};
|
use axum::extract::{Path, Multipart};
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Extension, Json};
|
use axum::{Extension, Json};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
UsersActiveInactiveRequestDto, UsersListItemDto, UsersUpdateRequestDto,
|
UsersActiveInactiveRequestDto, UsersListItemDto, UsersUpdateRequestDto,
|
||||||
};
|
};
|
||||||
|
use crate::v1::users::users_service::{UsersServiceTrait, UsersService};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
#[schema(description = "File upload form data for multipart/form-data")]
|
||||||
|
pub struct FileUploadSchema {
|
||||||
|
/// Binary file data to upload
|
||||||
|
#[schema(format = "binary")]
|
||||||
|
pub file: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -35,33 +46,33 @@ use super::{
|
|||||||
pub async fn get_user_list(
|
pub async fn get_user_list(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Query(meta): Query<MetaRequestDto>,
|
axum::extract::Query(meta): axum::extract::Query<MetaRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadListUsers],
|
vec![PermissionsEnum::ReadListUsers],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => UsersService::get_user_list(&state, meta).await,
|
Ok((_claims, state)) => UsersService::get_user_list(&state, meta).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
security(
|
security(
|
||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
path = "/v1/users/detail/{id}",
|
path = "/v1/users/detail/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "User ID")
|
("id" = String, Path, description = "User ID")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get user by ID", body = ResponseSuccessDto<UsersDetailItemDto>)
|
(status = 200, description = "Get user by ID", body = ResponseSuccessDto<UsersDetailItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Users"
|
tag = "Users"
|
||||||
)]
|
)]
|
||||||
pub async fn get_user_by_id(
|
pub async fn get_user_by_id(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
@@ -69,34 +80,34 @@ pub async fn get_user_by_id(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ReadDetailUsers],
|
vec![PermissionsEnum::ReadDetailUsers],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => UsersService::get_user_by_id(&state, id).await,
|
Ok((_claims, state)) => UsersService::get_user_by_id(&state, id).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
security(
|
security(
|
||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
path = "/v1/users/me",
|
path = "/v1/users/me",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get user by ID", body = ResponseSuccessDto<UsersDetailItemDto>)
|
(status = 200, description = "Get user by ID", body = ResponseSuccessDto<UsersDetailItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Users"
|
tag = "Users"
|
||||||
)]
|
)]
|
||||||
pub async fn get_user_me(
|
pub async fn get_user_me(
|
||||||
Extension(state): Extension<AppState>,
|
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(&headers, state.clone(), vec![]).await {
|
match permissions_guard(headers, Extension(state), vec![]).await {
|
||||||
Ok(_) => UsersService::get_user_me(headers, &state).await,
|
Ok((claims, state)) => UsersService::get_user_me(claims, &state).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +120,7 @@ pub async fn get_user_me(
|
|||||||
path = "/v1/users/create",
|
path = "/v1/users/create",
|
||||||
request_body = UsersCreateRequestDto,
|
request_body = UsersCreateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "Create new user", body = MessageResponseDto)
|
(status = 200, description = "Create new user", body = ResponseSuccessDto<UsersDetailItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Users"
|
tag = "Users"
|
||||||
)]
|
)]
|
||||||
@@ -119,13 +130,13 @@ pub async fn post_create_user(
|
|||||||
Json(payload): Json<UsersCreateRequestDto>,
|
Json(payload): Json<UsersCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::CreateUsers],
|
vec![PermissionsEnum::CreateUsers],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => UsersService::create_user(&state, payload).await,
|
Ok((_claims, state)) => UsersService::create_user(&state, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,9 +147,12 @@ pub async fn post_create_user(
|
|||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
path = "/v1/users/update/{id}",
|
path = "/v1/users/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "User ID")
|
||||||
|
),
|
||||||
request_body = UsersUpdateRequestDto,
|
request_body = UsersUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Update user", body = MessageResponseDto)
|
(status = 200, description = "Update user", body = ResponseSuccessDto<UsersDetailItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Users"
|
tag = "Users"
|
||||||
)]
|
)]
|
||||||
@@ -149,13 +163,13 @@ pub async fn put_update_user(
|
|||||||
Json(payload): Json<UsersUpdateRequestDto>,
|
Json(payload): Json<UsersUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::UpdateUsers],
|
vec![PermissionsEnum::UpdateUsers],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => UsersService::update_user(&state, id, payload).await,
|
Ok((_claims, state)) => UsersService::update_user(&state, id, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +182,7 @@ pub async fn put_update_user(
|
|||||||
path = "/v1/users/update/me",
|
path = "/v1/users/update/me",
|
||||||
request_body = UsersUpdateRequestDto,
|
request_body = UsersUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Update user me", body = MessageResponseDto)
|
(status = 200, description = "Update current user", body = ResponseSuccessDto<UsersDetailItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Users"
|
tag = "Users"
|
||||||
)]
|
)]
|
||||||
@@ -177,8 +191,8 @@ pub async fn put_update_user_me(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<UsersUpdateRequestDto>,
|
Json(payload): Json<UsersUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(&headers, state.clone(), vec![]).await {
|
match permissions_guard(headers.clone(), Extension(state), vec![]).await {
|
||||||
Ok(_) => UsersService::update_user_me(&state, headers, payload).await,
|
Ok((claims, state)) => UsersService::update_user_me(claims, &state, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,9 +203,12 @@ pub async fn put_update_user_me(
|
|||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
path = "/v1/users/activate/{id}",
|
path = "/v1/users/activate/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "User ID")
|
||||||
|
),
|
||||||
request_body = UsersActiveInactiveRequestDto,
|
request_body = UsersActiveInactiveRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Set user active/inactive", body = MessageResponseDto)
|
(status = 200, description = "Set user active status", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Users"
|
tag = "Users"
|
||||||
)]
|
)]
|
||||||
@@ -202,13 +219,13 @@ pub async fn patch_user_active_status(
|
|||||||
Json(payload): Json<UsersActiveInactiveRequestDto>,
|
Json(payload): Json<UsersActiveInactiveRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::ActivateUsers],
|
vec![PermissionsEnum::ActivateUsers],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => UsersService::set_user_active_status(&state, id, payload).await,
|
Ok((_claims, state)) => UsersService::set_user_active_status(&state, id, payload).await,
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,13 +247,56 @@ pub async fn delete_user(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
match permissions_guard(
|
||||||
&headers,
|
headers,
|
||||||
state.clone(),
|
Extension(state),
|
||||||
vec![PermissionsEnum::DeleteUsers],
|
vec![PermissionsEnum::DeleteUsers],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => UsersService::delete_user(&state, id).await,
|
Ok((_claims, state)) => UsersService::delete_user(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/upload",
|
||||||
|
request_body(
|
||||||
|
content = FileUploadSchema,
|
||||||
|
description = "Upload file with multipart form data. Only 'file' field is required - file type will be detected automatically from the uploaded file.",
|
||||||
|
content_type = "multipart/form-data"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Upload file successfully", body = ResponseSuccessDto<serde_json::Value>),
|
||||||
|
(status = 400, description = "Bad request"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn upload_file(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
multipart: Multipart,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Check authentication first
|
||||||
|
match permissions_guard(
|
||||||
|
headers,
|
||||||
|
Extension(state),
|
||||||
|
vec![], // No specific permission needed, just authentication
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((claims, state)) => {
|
||||||
|
// Extract user ID from user data
|
||||||
|
let user_id = claims.user_id.clone(); // Use claims.user_id directly
|
||||||
|
|
||||||
|
// Process upload - don't use match here since it returns Response directly
|
||||||
|
UsersService::upload_file(&state, user_id, multipart).await
|
||||||
|
},
|
||||||
Err(response) => response,
|
Err(response) => response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,31 @@ use serde::{Deserialize, Serialize};
|
|||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
use crate::UsersSchema; // Import UsersSchema
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref PASSWORD_REGEX: regex::Regex =
|
static ref PASSWORD_REGEX: regex::Regex =
|
||||||
regex::Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap();
|
regex::Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ExperienceDto {
|
||||||
|
pub id: String,
|
||||||
|
pub company: String,
|
||||||
|
pub position: String,
|
||||||
|
pub duration: String,
|
||||||
|
pub period: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EducationDto {
|
||||||
|
pub id: String,
|
||||||
|
pub institution: String,
|
||||||
|
pub degree: String,
|
||||||
|
pub field: String,
|
||||||
|
pub period: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct UsersActiveInactiveRequestDto {
|
pub struct UsersActiveInactiveRequestDto {
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
@@ -49,6 +68,7 @@ pub struct UsersCreateRequestDto {
|
|||||||
pub phone_number: String,
|
pub phone_number: String,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub role_id: String,
|
pub role_id: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
@@ -57,60 +77,173 @@ pub struct UsersUpdateRequestDto {
|
|||||||
length(min = 1, message = "Email cannot be empty"),
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
email(message = "Email not valid")
|
email(message = "Email not valid")
|
||||||
)]
|
)]
|
||||||
pub email: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub email: Option<String>,
|
||||||
#[validate(length(
|
#[validate(length(
|
||||||
min = 8,
|
min = 8,
|
||||||
message = "Password must have at least 8 characters"
|
message = "Password must have at least 8 characters"
|
||||||
))]
|
))]
|
||||||
pub password: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub password: Option<String>,
|
||||||
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||||
pub fullname: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fullname: Option<String>,
|
||||||
|
#[validate(length(min = 2, message = "Legal name at least have 2 character"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub legal_name: Option<String>,
|
||||||
#[validate(length(
|
#[validate(length(
|
||||||
min = 10,
|
min = 10,
|
||||||
message = "Phone number at least have 10 character"
|
message = "Phone number at least have 10 character"
|
||||||
))]
|
))]
|
||||||
pub phone_number: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub is_active: bool,
|
pub phone_number: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_active: Option<bool>,
|
||||||
#[validate(length(min = 1, message = "Gender is required"))]
|
#[validate(length(min = 1, message = "Gender is required"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub gender: Option<String>,
|
pub gender: Option<String>,
|
||||||
#[validate(length(min = 1, message = "Birthdate is required"))]
|
#[validate(length(min = 1, message = "Birthdate is required"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub birthdate: Option<String>,
|
pub birthdate: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub domicile: 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(url(message = "Invalid website URL"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub website_url: Option<String>,
|
||||||
|
#[validate(url(message = "Invalid Twitter URL"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub twitter_url: Option<String>,
|
||||||
#[validate(length(min = 1, message = "Avatar is required"))]
|
#[validate(length(min = 1, message = "Avatar is required"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub role_id: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub role_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub location: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub skills: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub experience: Option<Vec<ExperienceDto>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub education: Option<Vec<EducationDto>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub career_status: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)]
|
||||||
pub struct UsersDetailItemDto {
|
pub struct UsersDetailItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub role: RolesDetailItemDto,
|
pub role: RolesDetailItemDto,
|
||||||
pub fullname: String,
|
pub fullname: String,
|
||||||
|
pub legal_name: Option<String>,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub phone_number: String,
|
pub phone_number: String,
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub gender: Option<String>,
|
pub gender: Option<String>,
|
||||||
pub birthdate: Option<String>,
|
pub birthdate: Option<String>,
|
||||||
|
pub domicile: 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 website_url: Option<String>,
|
||||||
|
pub twitter_url: Option<String>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub skills: Option<Vec<String>>,
|
||||||
|
pub experience: Option<Vec<ExperienceDto>>,
|
||||||
|
pub education: Option<Vec<EducationDto>>,
|
||||||
|
pub career_status: Option<String>,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UsersDetailItemDto {
|
impl UsersDetailItemDto {
|
||||||
pub fn from(dto: &UsersDetailQueryDto) -> Self {
|
pub fn from(dto: &UsersDetailQueryDto) -> Self { // Reverted to taking a reference
|
||||||
Self {
|
Self {
|
||||||
id: dto.id.id.to_raw().clone(),
|
id: dto.id.id.to_raw().clone(),
|
||||||
role: RolesDetailItemDto::from(&dto.role),
|
role: RolesDetailItemDto::from(&dto.role),
|
||||||
fullname: dto.fullname.clone(),
|
fullname: dto.fullname.clone(),
|
||||||
|
legal_name: dto.legal_name.clone(),
|
||||||
email: dto.email.clone(),
|
email: dto.email.clone(),
|
||||||
avatar: dto.avatar.clone(),
|
avatar: dto.avatar.clone(),
|
||||||
phone_number: dto.phone_number.clone(),
|
phone_number: dto.phone_number.clone(), // Corrected from dto.phone.clone()
|
||||||
|
phone_for_verification: dto.phone_for_verification.clone(),
|
||||||
is_active: dto.is_active,
|
is_active: dto.is_active,
|
||||||
gender: dto.gender.clone(),
|
gender: dto.gender.clone(),
|
||||||
birthdate: dto.birthdate.clone(),
|
birthdate: dto.birthdate.clone(),
|
||||||
|
domicile: dto.domicile.clone(),
|
||||||
|
bio: dto.bio.clone(),
|
||||||
|
last_education: dto.last_education.clone(),
|
||||||
|
linkedin_url: dto.linkedin_url.clone(),
|
||||||
|
github_url: dto.github_url.clone(),
|
||||||
|
cv_url: dto.cv_url.clone(),
|
||||||
|
portfolio_url: dto.portfolio_url.clone(),
|
||||||
|
website_url: dto.website_url.clone(),
|
||||||
|
twitter_url: dto.twitter_url.clone(),
|
||||||
|
location: dto.location.clone(),
|
||||||
|
skills: dto.skills.clone(),
|
||||||
|
experience: dto.experience.clone(),
|
||||||
|
education: dto.education.clone(),
|
||||||
|
career_status: dto.career_status.clone(),
|
||||||
created_at: dto.created_at.clone(),
|
created_at: dto.created_at.clone(),
|
||||||
updated_at: dto.updated_at.clone(),
|
updated_at: dto.updated_at.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_schema(schema: &UsersSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: schema.id.id.to_raw(),
|
||||||
|
role: RolesDetailItemDto::default(), // Placeholder, role needs to be fetched
|
||||||
|
fullname: schema.fullname.clone(),
|
||||||
|
legal_name: schema.legal_name.clone(),
|
||||||
|
email: schema.email.clone(),
|
||||||
|
avatar: schema.avatar.clone(),
|
||||||
|
phone_number: schema.phone_number.clone(),
|
||||||
|
phone_for_verification: schema.phone_for_verification.clone(),
|
||||||
|
is_active: schema.is_active,
|
||||||
|
gender: schema.gender.clone(),
|
||||||
|
birthdate: schema.birthdate.clone(),
|
||||||
|
domicile: schema.domicile.clone(),
|
||||||
|
bio: schema.bio.clone(),
|
||||||
|
last_education: schema.last_education.clone(),
|
||||||
|
linkedin_url: schema.linkedin_url.clone(),
|
||||||
|
github_url: schema.github_url.clone(),
|
||||||
|
cv_url: schema.cv_url.clone(),
|
||||||
|
portfolio_url: schema.portfolio_url.clone(),
|
||||||
|
website_url: schema.website_url.clone(),
|
||||||
|
twitter_url: schema.twitter_url.clone(),
|
||||||
|
location: schema.location.clone(),
|
||||||
|
skills: schema.skills.clone(),
|
||||||
|
experience: schema.experience.clone(),
|
||||||
|
education: schema.education.clone(),
|
||||||
|
career_status: schema.career_status.clone(),
|
||||||
|
created_at: schema.created_at.clone(),
|
||||||
|
updated_at: schema.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
@@ -131,7 +264,7 @@ pub struct UsersListQueryDto {
|
|||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub role: RolesDetailQueryDto,
|
pub role: RolesDetailQueryDto,
|
||||||
pub fullname: String,
|
pub fullname: String,
|
||||||
pub email: String,
|
pub email: String, // Corrected from pub pub email: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub phone_number: String,
|
pub phone_number: String,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
@@ -159,13 +292,29 @@ impl UsersListQueryDto {
|
|||||||
pub struct UsersDetailQueryDto {
|
pub struct UsersDetailQueryDto {
|
||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub fullname: String,
|
pub fullname: String,
|
||||||
|
pub legal_name: Option<String>,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub phone_number: String,
|
pub phone_number: String,
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
pub gender: Option<String>,
|
pub gender: Option<String>,
|
||||||
pub birthdate: Option<String>,
|
pub birthdate: Option<String>,
|
||||||
|
pub domicile: 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 website_url: Option<String>,
|
||||||
|
pub twitter_url: Option<String>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub skills: Option<Vec<String>>,
|
||||||
|
pub experience: Option<Vec<ExperienceDto>>,
|
||||||
|
pub education: Option<Vec<EducationDto>>,
|
||||||
|
pub career_status: Option<String>,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub role: RolesDetailQueryDto,
|
pub role: RolesDetailQueryDto,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
@@ -179,12 +328,28 @@ impl UsersDetailQueryDto {
|
|||||||
id: self.id.clone(),
|
id: self.id.clone(),
|
||||||
role: self.role.clone(),
|
role: self.role.clone(),
|
||||||
fullname: self.fullname.clone(),
|
fullname: self.fullname.clone(),
|
||||||
|
legal_name: self.legal_name.clone(),
|
||||||
email: self.email.clone(),
|
email: self.email.clone(),
|
||||||
avatar: self.avatar.clone(),
|
avatar: self.avatar.clone(),
|
||||||
phone_number: self.phone_number.clone(),
|
phone_number: self.phone_number.clone(),
|
||||||
|
phone_for_verification: self.phone_for_verification.clone(),
|
||||||
is_active: self.is_active,
|
is_active: self.is_active,
|
||||||
mentor_id: self.mentor_id.clone(),
|
mentor_id: self.mentor_id.clone(),
|
||||||
gender: self.gender.clone(),
|
gender: self.gender.clone(),
|
||||||
|
domicile: self.domicile.clone(),
|
||||||
|
bio: self.bio.clone(),
|
||||||
|
last_education: self.last_education.clone(),
|
||||||
|
linkedin_url: self.linkedin_url.clone(),
|
||||||
|
github_url: self.github_url.clone(),
|
||||||
|
cv_url: self.cv_url.clone(),
|
||||||
|
portfolio_url: self.portfolio_url.clone(),
|
||||||
|
website_url: self.website_url.clone(),
|
||||||
|
twitter_url: self.twitter_url.clone(),
|
||||||
|
location: self.location.clone(),
|
||||||
|
skills: self.skills.clone(),
|
||||||
|
experience: self.experience.clone(),
|
||||||
|
education: self.education.clone(),
|
||||||
|
career_status: self.career_status.clone(),
|
||||||
is_deleted: self.is_deleted,
|
is_deleted: self.is_deleted,
|
||||||
password: self.password.clone(),
|
password: self.password.clone(),
|
||||||
birthdate: self.birthdate.clone(),
|
birthdate: self.birthdate.clone(),
|
||||||
@@ -199,18 +364,40 @@ impl From<&UsersDetailItemDto> for UsersDetailQueryDto {
|
|||||||
Self {
|
Self {
|
||||||
id: crate::make_thing(&imphnen_libs::ResourceEnum::Users.to_string(), &dto.id),
|
id: crate::make_thing(&imphnen_libs::ResourceEnum::Users.to_string(), &dto.id),
|
||||||
fullname: dto.fullname.clone(),
|
fullname: dto.fullname.clone(),
|
||||||
|
legal_name: dto.legal_name.clone(),
|
||||||
email: dto.email.clone(),
|
email: dto.email.clone(),
|
||||||
avatar: dto.avatar.clone(),
|
avatar: dto.avatar.clone(),
|
||||||
phone_number: dto.phone_number.clone(),
|
phone_number: dto.phone_number.clone(),
|
||||||
|
phone_for_verification: dto.phone_for_verification.clone(),
|
||||||
is_active: dto.is_active,
|
is_active: dto.is_active,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
gender: dto.gender.clone(),
|
gender: dto.gender.clone(),
|
||||||
birthdate: dto.birthdate.clone(),
|
birthdate: dto.birthdate.clone(),
|
||||||
|
domicile: dto.domicile.clone(),
|
||||||
|
bio: dto.bio.clone(),
|
||||||
|
last_education: dto.last_education.clone(),
|
||||||
|
linkedin_url: dto.linkedin_url.clone(),
|
||||||
|
github_url: dto.github_url.clone(),
|
||||||
|
cv_url: dto.cv_url.clone(),
|
||||||
|
portfolio_url: dto.portfolio_url.clone(),
|
||||||
|
website_url: dto.website_url.clone(),
|
||||||
|
twitter_url: dto.twitter_url.clone(),
|
||||||
|
location: dto.location.clone(),
|
||||||
|
skills: dto.skills.clone(),
|
||||||
|
experience: dto.experience.clone(),
|
||||||
|
education: dto.education.clone(),
|
||||||
|
career_status: dto.career_status.clone(),
|
||||||
password: String::new(),
|
password: String::new(),
|
||||||
role: RolesDetailQueryDto::default(),
|
role: RolesDetailQueryDto::default(),
|
||||||
created_at: dto.created_at.clone(),
|
created_at: dto.created_at.clone(),
|
||||||
updated_at: dto.updated_at.clone(),
|
updated_at: dto.updated_at.clone(),
|
||||||
mentor_id: None,
|
mentor_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UsersDetailItemDto {
|
||||||
|
pub fn extract_permissions_from_user_role(&self) -> Vec<String> {
|
||||||
|
self.role.permissions.iter().map(|p| p.name.clone()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::{UsersCreateRequestDto, UsersDetailQueryDto, UsersUpdateRequestDto};
|
use super::{UsersCreateRequestDto, UsersDetailQueryDto, UsersUpdateRequestDto, ExperienceDto, EducationDto};
|
||||||
use imphnen_libs::{ResourceEnum, hash_password};
|
use imphnen_libs::{ResourceEnum, hash_password};
|
||||||
use imphnen_utils::extract_id;
|
use imphnen_utils::extract_id;
|
||||||
use imphnen_utils::{get_iso_date, make_thing};
|
use imphnen_utils::{get_iso_date, make_thing};
|
||||||
@@ -9,11 +9,15 @@ use surrealdb::{Uuid, sql::Thing};
|
|||||||
pub struct UsersSchema {
|
pub struct UsersSchema {
|
||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub fullname: String,
|
pub fullname: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub legal_name: Option<String>,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub phone_number: String,
|
pub phone_number: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -22,6 +26,34 @@ pub struct UsersSchema {
|
|||||||
pub gender: Option<String>,
|
pub gender: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub birthdate: Option<String>,
|
pub birthdate: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub domicile: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bio: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_education: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub linkedin_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub github_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cv_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub portfolio_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub website_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub twitter_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub location: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub skills: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub experience: Option<Vec<ExperienceDto>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub education: Option<Vec<EducationDto>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub career_status: Option<String>,
|
||||||
pub role: Thing,
|
pub role: Thing,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
@@ -35,18 +67,31 @@ impl Default for UsersSchema {
|
|||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
fullname: String::new(),
|
fullname: String::new(),
|
||||||
|
legal_name: None,
|
||||||
email: String::new(),
|
email: String::new(),
|
||||||
password: hash_password("").unwrap(),
|
password: hash_password("").unwrap(),
|
||||||
avatar: None,
|
avatar: None,
|
||||||
phone_number: String::new(),
|
phone_number: String::new(),
|
||||||
|
phone_for_verification: None,
|
||||||
is_active: false,
|
is_active: false,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
mentor_id: Some(make_thing(
|
mentor_id: None, // Regular users should not have a mentor_id by default
|
||||||
&ResourceEnum::Users.to_string(),
|
|
||||||
&Uuid::new_v4().to_string(),
|
|
||||||
)),
|
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
role: make_thing(
|
role: make_thing(
|
||||||
&ResourceEnum::Roles.to_string(),
|
&ResourceEnum::Roles.to_string(),
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
@@ -62,19 +107,30 @@ impl UsersSchema {
|
|||||||
Self {
|
Self {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
fullname: dto.fullname,
|
fullname: dto.fullname,
|
||||||
|
legal_name: dto.legal_name,
|
||||||
email: dto.email,
|
email: dto.email,
|
||||||
avatar: dto.avatar,
|
avatar: dto.avatar,
|
||||||
phone_number: dto.phone_number,
|
phone_number: dto.phone_number,
|
||||||
|
phone_for_verification: dto.phone_for_verification,
|
||||||
is_active: dto.is_active,
|
is_active: dto.is_active,
|
||||||
is_deleted: dto.is_deleted,
|
is_deleted: dto.is_deleted,
|
||||||
mentor_id: Some(dto.mentor_id.unwrap_or_else(|| {
|
mentor_id: dto.mentor_id, // Use the actual mentor_id from the DTO, could be None
|
||||||
make_thing(
|
|
||||||
&ResourceEnum::Users.to_string(),
|
|
||||||
&Uuid::new_v4().to_string(),
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
gender: dto.gender,
|
gender: dto.gender,
|
||||||
birthdate: dto.birthdate,
|
birthdate: dto.birthdate,
|
||||||
|
domicile: dto.domicile,
|
||||||
|
bio: dto.bio,
|
||||||
|
last_education: dto.last_education,
|
||||||
|
linkedin_url: dto.linkedin_url,
|
||||||
|
github_url: dto.github_url,
|
||||||
|
cv_url: dto.cv_url,
|
||||||
|
portfolio_url: dto.portfolio_url,
|
||||||
|
website_url: dto.website_url,
|
||||||
|
twitter_url: dto.twitter_url,
|
||||||
|
location: dto.location,
|
||||||
|
skills: dto.skills,
|
||||||
|
experience: dto.experience,
|
||||||
|
education: dto.education,
|
||||||
|
career_status: dto.career_status,
|
||||||
password: dto.password,
|
password: dto.password,
|
||||||
created_at: dto.created_at,
|
created_at: dto.created_at,
|
||||||
updated_at: dto.updated_at,
|
updated_at: dto.updated_at,
|
||||||
@@ -82,23 +138,101 @@ impl UsersSchema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(user: UsersUpdateRequestDto, id: String) -> Self {
|
pub fn update(_user: UsersUpdateRequestDto, id: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: make_thing(&ResourceEnum::Users.to_string(), &id),
|
id: make_thing(&ResourceEnum::Users.to_string(), &id),
|
||||||
fullname: user.fullname,
|
|
||||||
email: user.email,
|
|
||||||
phone_number: user.phone_number,
|
|
||||||
is_active: user.is_active,
|
|
||||||
gender: user.gender,
|
|
||||||
birthdate: user.birthdate,
|
|
||||||
avatar: user.avatar,
|
|
||||||
is_deleted: false,
|
|
||||||
role: make_thing(&ResourceEnum::Roles.to_string(), &user.role_id),
|
|
||||||
updated_at: get_iso_date(),
|
updated_at: get_iso_date(),
|
||||||
|
// Set defaults for required fields - these should be overridden by actual data from DB
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn partial_update(current_user: UsersDetailQueryDto, user: UsersUpdateRequestDto) -> Self {
|
||||||
|
let mut schema = Self::from(current_user);
|
||||||
|
schema.updated_at = get_iso_date();
|
||||||
|
|
||||||
|
// Only update fields that are provided (Some)
|
||||||
|
if let Some(fullname) = user.fullname {
|
||||||
|
schema.fullname = fullname;
|
||||||
|
}
|
||||||
|
if let Some(email) = user.email {
|
||||||
|
schema.email = email;
|
||||||
|
}
|
||||||
|
if let Some(password) = user.password {
|
||||||
|
schema.password = hash_password(&password).unwrap_or_else(|_| password);
|
||||||
|
}
|
||||||
|
if let Some(phone_number) = user.phone_number {
|
||||||
|
schema.phone_number = phone_number;
|
||||||
|
}
|
||||||
|
if let Some(is_active) = user.is_active {
|
||||||
|
schema.is_active = is_active;
|
||||||
|
}
|
||||||
|
if let Some(role_id) = user.role_id {
|
||||||
|
schema.role = make_thing(&ResourceEnum::Roles.to_string(), &role_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional fields - only update if provided
|
||||||
|
if let Some(legal_name) = user.legal_name {
|
||||||
|
schema.legal_name = Some(legal_name);
|
||||||
|
}
|
||||||
|
if let Some(phone_for_verification) = user.phone_for_verification {
|
||||||
|
schema.phone_for_verification = Some(phone_for_verification);
|
||||||
|
}
|
||||||
|
if let Some(gender) = user.gender {
|
||||||
|
schema.gender = Some(gender);
|
||||||
|
}
|
||||||
|
if let Some(birthdate) = user.birthdate {
|
||||||
|
schema.birthdate = Some(birthdate);
|
||||||
|
}
|
||||||
|
if let Some(domicile) = user.domicile {
|
||||||
|
schema.domicile = Some(domicile);
|
||||||
|
}
|
||||||
|
if let Some(bio) = user.bio {
|
||||||
|
schema.bio = Some(bio);
|
||||||
|
}
|
||||||
|
if let Some(last_education) = user.last_education {
|
||||||
|
schema.last_education = Some(last_education);
|
||||||
|
}
|
||||||
|
if let Some(linkedin_url) = user.linkedin_url {
|
||||||
|
schema.linkedin_url = Some(linkedin_url);
|
||||||
|
}
|
||||||
|
if let Some(github_url) = user.github_url {
|
||||||
|
schema.github_url = Some(github_url);
|
||||||
|
}
|
||||||
|
if let Some(cv_url) = user.cv_url {
|
||||||
|
schema.cv_url = Some(cv_url);
|
||||||
|
}
|
||||||
|
if let Some(portfolio_url) = user.portfolio_url {
|
||||||
|
schema.portfolio_url = Some(portfolio_url);
|
||||||
|
}
|
||||||
|
if let Some(website_url) = user.website_url {
|
||||||
|
schema.website_url = Some(website_url);
|
||||||
|
}
|
||||||
|
if let Some(twitter_url) = user.twitter_url {
|
||||||
|
schema.twitter_url = Some(twitter_url);
|
||||||
|
}
|
||||||
|
if let Some(location) = user.location {
|
||||||
|
schema.location = Some(location);
|
||||||
|
}
|
||||||
|
if let Some(skills) = user.skills {
|
||||||
|
schema.skills = Some(skills);
|
||||||
|
}
|
||||||
|
if let Some(experience) = user.experience {
|
||||||
|
schema.experience = Some(experience);
|
||||||
|
}
|
||||||
|
if let Some(education) = user.education {
|
||||||
|
schema.education = Some(education);
|
||||||
|
}
|
||||||
|
if let Some(career_status) = user.career_status {
|
||||||
|
schema.career_status = Some(career_status);
|
||||||
|
}
|
||||||
|
if let Some(avatar) = user.avatar {
|
||||||
|
schema.avatar = Some(avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
schema
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create(user: UsersCreateRequestDto) -> Self {
|
pub fn create(user: UsersCreateRequestDto) -> Self {
|
||||||
let password = hash_password(&user.password).unwrap();
|
let password = hash_password(&user.password).unwrap();
|
||||||
Self {
|
Self {
|
||||||
@@ -107,17 +241,30 @@ impl UsersSchema {
|
|||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
fullname: user.fullname,
|
fullname: user.fullname,
|
||||||
|
legal_name: None,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
password,
|
password,
|
||||||
phone_number: user.phone_number,
|
phone_number: user.phone_number,
|
||||||
|
phone_for_verification: None,
|
||||||
is_active: false,
|
is_active: false,
|
||||||
mentor_id: Some(make_thing(
|
mentor_id: None, // Regular users should not have a mentor_id by default
|
||||||
&ResourceEnum::Users.to_string(),
|
|
||||||
&Uuid::new_v4().to_string(),
|
|
||||||
)),
|
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
avatar: None,
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
|
avatar: user.avatar,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
role: make_thing(&ResourceEnum::Roles.to_string(), &user.role_id),
|
role: make_thing(&ResourceEnum::Roles.to_string(), &user.role_id),
|
||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
@@ -136,10 +283,7 @@ impl UsersSchema {
|
|||||||
pub fn update_mentor_id(mut self, mentor_id: Option<String>) -> Self {
|
pub fn update_mentor_id(mut self, mentor_id: Option<String>) -> Self {
|
||||||
self.mentor_id = match mentor_id {
|
self.mentor_id = match mentor_id {
|
||||||
Some(id) => Some(make_thing(&ResourceEnum::Users.to_string(), &id)),
|
Some(id) => Some(make_thing(&ResourceEnum::Users.to_string(), &id)),
|
||||||
None => Some(make_thing(
|
None => None, // Set to None if no mentor_id provided
|
||||||
&ResourceEnum::Users.to_string(),
|
|
||||||
&Uuid::new_v4().to_string(),
|
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
self.updated_at = get_iso_date();
|
self.updated_at = get_iso_date();
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -1,26 +1,68 @@
|
|||||||
use super::{
|
use super::{
|
||||||
UsersActiveInactiveRequestDto, UsersCreateRequestDto, UsersDetailItemDto,
|
UsersActiveInactiveRequestDto, UsersCreateRequestDto,
|
||||||
UsersSetNewPasswordRequestDto, UsersUpdateRequestDto,
|
UsersSetNewPasswordRequestDto, UsersUpdateRequestDto,
|
||||||
|
users_dto::UsersDetailQueryDto, // Add this line
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, MetaRequestDto, ResponseListSuccessDto, UsersRepository, UsersSchema,
|
AppState, MetaRequestDto, ResponseListSuccessDto, UsersRepository, UsersSchema,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
ResponseSuccessDto, common_response, extract_email, success_list_response,
|
ResponseSuccessDto, common_response, success_list_response,
|
||||||
success_response, validate_request,
|
success_response, validate_request,
|
||||||
};
|
};
|
||||||
use axum::http::HeaderMap;
|
use axum::{http::StatusCode, response::Response, extract::Multipart};
|
||||||
use axum::{http::StatusCode, response::Response};
|
use imphnen_libs::{ResourceEnum, hash_password, verify_password, MinioConfig, FileType, decode_base64_file, extract_content_type_from_data_url, create_minio_service_from_config};
|
||||||
use imphnen_libs::{ResourceEnum, hash_password, verify_password};
|
|
||||||
use imphnen_utils::make_thing;
|
use imphnen_utils::make_thing;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::future::Future;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use tracing::info;
|
||||||
|
use tracing::error;
|
||||||
|
use crate::v1::users::users_dto::{UsersDetailItemDto as UserDto, UsersCreateRequestDto as CreateUserDto};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
|
||||||
pub struct UsersService;
|
pub trait UsersServiceTrait: Send + Sync + 'static {
|
||||||
|
fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
impl UsersService {
|
fn get_user_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
pub async fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
fn get_user_me(claims: imphnen_libs::jsonwebtoken::Claims, state: &AppState) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
let repo = UsersRepository::new(state);
|
fn create_user(state: &AppState, new_user: UsersCreateRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn update_user(state: &AppState, id: String, user: UsersUpdateRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn update_user_me(claims: imphnen_libs::jsonwebtoken::Claims, state: &AppState, user: UsersUpdateRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn set_user_active_status(state: &AppState, id: String, payload: UsersActiveInactiveRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn update_user_password(state: &AppState, email: String, payload: UsersSetNewPasswordRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn get_user_by_mentor_id(state: &AppState, mentor_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn delete_user(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn get_user_by_id_internal(&self, id: &surrealdb::sql::Thing, state: &AppState) -> Pin<Box<dyn Future<Output = Result<UsersDetailQueryDto>> + Send>>;
|
||||||
|
|
||||||
|
fn get_user_by_email(&self, email: &str, state: &AppState) -> Pin<Box<dyn Future<Output = Result<Option<UserDto>>> + Send>>;
|
||||||
|
fn create_user_by_dto(&self, new_user: CreateUserDto, state: &AppState) -> Pin<Box<dyn Future<Output = Result<UserDto>> + Send>>;
|
||||||
|
fn update_user_avatar(email: &str, avatar_url: Option<String>, state: &AppState) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>;
|
||||||
|
fn upload_file(state: &AppState, user_id: String, multipart: Multipart) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UsersService;
|
||||||
|
|
||||||
|
impl UsersService {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl UsersServiceTrait for UsersService {
|
||||||
|
fn get_user_by_id_internal(&self, id: &surrealdb::sql::Thing, state: &AppState) -> Pin<Box<dyn Future<Output = Result<UsersDetailQueryDto>> + Send>> {
|
||||||
|
let id = id.to_owned();
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = crate::UsersRepository::new(&state);
|
||||||
|
repo.query_user_by_id(&id).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
match repo.query_user_list(meta).await {
|
match repo.query_user_list(meta).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let response = ResponseListSuccessDto {
|
let response = ResponseListSuccessDto {
|
||||||
@@ -31,46 +73,55 @@ impl UsersService {
|
|||||||
}
|
}
|
||||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_by_id(state: &AppState, id: String) -> Response {
|
fn get_user_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
let id = id.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if Uuid::parse_str(&id).is_err() {
|
if Uuid::parse_str(&id).is_err() {
|
||||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||||
}
|
}
|
||||||
let repo = UsersRepository::new(state);
|
let repo = UsersRepository::new(&state);
|
||||||
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
match repo.query_user_by_id(&thing_id).await {
|
match repo.query_user_by_id(&thing_id).await {
|
||||||
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
||||||
data: UsersDetailItemDto::from(&user),
|
data: UserDto::from(&user), // Corrected to use UserDto::from by reference
|
||||||
}),
|
}),
|
||||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_me(headers: HeaderMap, state: &AppState) -> Response {
|
fn get_user_me(claims: imphnen_libs::jsonwebtoken::Claims, state: &AppState) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
let repo = UsersRepository::new(state);
|
let claims = claims.to_owned();
|
||||||
let email = match extract_email(&headers) {
|
let state = state.to_owned();
|
||||||
Some(email) => email,
|
Box::pin(async move {
|
||||||
None => return common_response(StatusCode::UNAUTHORIZED, "Invalid token"),
|
let repo = UsersRepository::new(&state);
|
||||||
};
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &claims.user_id);
|
||||||
match repo.query_user_by_email(email).await {
|
match repo.query_user_by_id(&thing_id).await {
|
||||||
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
||||||
data: UsersDetailItemDto::from(&user),
|
data: UserDto::from(&user),
|
||||||
}),
|
}),
|
||||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
fn create_user(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
new_user: UsersCreateRequestDto,
|
new_user: UsersCreateRequestDto,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
let new_user = new_user;
|
||||||
|
Box::pin(async move {
|
||||||
if let Err((status, message)) = validate_request(&new_user) {
|
if let Err((status, message)) = validate_request(&new_user) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let repo = UsersRepository::new(state);
|
let repo = UsersRepository::new(&state);
|
||||||
if repo
|
if repo
|
||||||
.query_user_by_email(new_user.email.clone())
|
.query_user_by_email(new_user.email.clone())
|
||||||
.await
|
.await
|
||||||
@@ -84,60 +135,83 @@ impl UsersService {
|
|||||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_user(
|
fn update_user(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
id: String,
|
id: String,
|
||||||
user: UsersUpdateRequestDto,
|
user: UsersUpdateRequestDto,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
let id = id.to_owned();
|
||||||
|
let user = user;
|
||||||
|
Box::pin(async move {
|
||||||
if Uuid::parse_str(&id).is_err() {
|
if Uuid::parse_str(&id).is_err() {
|
||||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||||
}
|
}
|
||||||
let repo = UsersRepository::new(state);
|
let repo = UsersRepository::new(&state);
|
||||||
if let Err((status, message)) = validate_request(&user) {
|
if let Err((status, message)) = validate_request(&user) {
|
||||||
return common_response(status, &message);
|
return common_response(status, &message);
|
||||||
}
|
}
|
||||||
let updated_user = UsersSchema::update(user, id);
|
|
||||||
match repo.query_update_user(updated_user).await {
|
// Get current user data first
|
||||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
let current_user = match repo.query_user_by_id(&thing_id).await {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_user_me(
|
|
||||||
state: &AppState,
|
|
||||||
headers: HeaderMap,
|
|
||||||
user: UsersUpdateRequestDto,
|
|
||||||
) -> Response {
|
|
||||||
let repo = UsersRepository::new(state);
|
|
||||||
let email = match extract_email(&headers) {
|
|
||||||
Some(email) => email,
|
|
||||||
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
|
||||||
};
|
|
||||||
let user_data = match repo.query_user_by_email(email.clone()).await {
|
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
};
|
};
|
||||||
if let Err((status, message)) = validate_request(&user) {
|
|
||||||
return common_response(status, &message);
|
let updated_user = UsersSchema::partial_update(current_user, user);
|
||||||
}
|
|
||||||
let updated_user = UsersSchema::update(user, user_data.id.id.to_raw());
|
|
||||||
match repo.query_update_user(updated_user).await {
|
match repo.query_update_user(updated_user).await {
|
||||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_user_active_status(
|
fn update_user_me(
|
||||||
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
||||||
|
state: &AppState,
|
||||||
|
user_update_dto: UsersUpdateRequestDto,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let claims = claims.to_owned();
|
||||||
|
let state = state.to_owned();
|
||||||
|
let user_update_dto = user_update_dto;
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
|
||||||
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &claims.user_id);
|
||||||
|
let user_data = match repo.query_user_by_id(&thing_id).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err((status, message)) = validate_request(&user_update_dto) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated_user = UsersSchema::partial_update(user_data, user_update_dto);
|
||||||
|
match repo.query_update_user(updated_user).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_user_active_status(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
id: String,
|
id: String,
|
||||||
payload: UsersActiveInactiveRequestDto,
|
payload: UsersActiveInactiveRequestDto,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
let id = id.to_owned();
|
||||||
|
let payload = payload;
|
||||||
|
Box::pin(async move {
|
||||||
if Uuid::parse_str(&id).is_err() {
|
if Uuid::parse_str(&id).is_err() {
|
||||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||||
}
|
}
|
||||||
let repo = UsersRepository::new(state);
|
let repo = UsersRepository::new(&state);
|
||||||
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
match repo.query_user_by_id(&thing_id).await {
|
match repo.query_user_by_id(&thing_id).await {
|
||||||
Ok(user) if !user.is_deleted => {
|
Ok(user) if !user.is_deleted => {
|
||||||
@@ -154,14 +228,19 @@ impl UsersService {
|
|||||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_user_password(
|
fn update_user_password(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
email: String,
|
email: String,
|
||||||
payload: UsersSetNewPasswordRequestDto,
|
payload: UsersSetNewPasswordRequestDto,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
let repo = UsersRepository::new(state);
|
let state = state.to_owned();
|
||||||
|
let email = email.to_owned();
|
||||||
|
let payload = payload;
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
let user = match repo.query_user_by_email(email.clone()).await {
|
let user = match repo.query_user_by_email(email.clone()).await {
|
||||||
Ok(user) if !user.is_deleted => user,
|
Ok(user) if !user.is_deleted => user,
|
||||||
_ => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
_ => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
@@ -197,28 +276,36 @@ impl UsersService {
|
|||||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_by_mentor_id(
|
fn get_user_by_mentor_id(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
mentor_id: String,
|
mentor_id: String,
|
||||||
) -> Response {
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
let repo = UsersRepository::new(state);
|
let state = state.to_owned();
|
||||||
|
let mentor_id = mentor_id.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
let thing_id = make_thing(&ResourceEnum::Mentors.to_string(), &mentor_id);
|
let thing_id = make_thing(&ResourceEnum::Mentors.to_string(), &mentor_id);
|
||||||
match repo.query_user_by_id(&thing_id).await {
|
match repo.query_user_by_id(&thing_id).await {
|
||||||
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
||||||
data: UsersDetailItemDto::from(&user),
|
data: UserDto::from(&user), // Corrected to use UserDto::from by reference
|
||||||
}),
|
}),
|
||||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_user(state: &AppState, id: String) -> Response {
|
fn delete_user(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
let id = id.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
if Uuid::parse_str(&id).is_err() {
|
if Uuid::parse_str(&id).is_err() {
|
||||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||||
}
|
}
|
||||||
let repo = UsersRepository::new(state);
|
let repo = UsersRepository::new(&state);
|
||||||
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
if repo.query_user_by_id(&thing_id).await.is_err() {
|
if repo.query_user_by_id(&thing_id).await.is_err() {
|
||||||
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
@@ -227,5 +314,306 @@ impl UsersService {
|
|||||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fn get_user_by_email(&self, email: &str, state: &AppState) -> Pin<Box<dyn Future<Output = Result<Option<UserDto>>> + Send>> {
|
||||||
|
let email = email.to_owned();
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let user = repo.query_user_by_email(email.to_string()).await;
|
||||||
|
match user {
|
||||||
|
Ok(u) => Ok(Some(UserDto::from(&u))),
|
||||||
|
Err(e) if e.to_string().contains("User not found") => Ok(None),
|
||||||
|
Err(e) => Err(anyhow::anyhow!(e.to_string())),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_user_by_dto(&self, new_user: CreateUserDto, state: &AppState) -> Pin<Box<dyn Future<Output = Result<UserDto>> + Send>> {
|
||||||
|
let new_user = new_user;
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let email_clone = new_user.email.clone();
|
||||||
|
let user_schema = UsersSchema {
|
||||||
|
email: new_user.email,
|
||||||
|
password: new_user.password,
|
||||||
|
fullname: new_user.fullname,
|
||||||
|
phone_number: new_user.phone_number,
|
||||||
|
is_active: new_user.is_active,
|
||||||
|
avatar: new_user.avatar,
|
||||||
|
role: make_thing(&ResourceEnum::Roles.to_string(), &new_user.role_id),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match repo.query_create_user(user_schema).await {
|
||||||
|
Ok(_msg) => {
|
||||||
|
let created_user = repo.query_user_by_email(email_clone).await?;
|
||||||
|
Ok(UserDto::from(&created_user))
|
||||||
|
},
|
||||||
|
Err(e) => Err(anyhow::anyhow!(e.to_string())),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_user_avatar(email: &str, avatar_url: Option<String>, state: &AppState) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
|
||||||
|
let email = email.to_owned();
|
||||||
|
let avatar_url = avatar_url.to_owned();
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
|
||||||
|
let mut user = repo.query_user_by_email(email.to_string()).await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to get user: {}", e))?;
|
||||||
|
|
||||||
|
user.avatar = avatar_url;
|
||||||
|
|
||||||
|
let user_schema = UsersSchema::from(user);
|
||||||
|
match repo.query_update_user(user_schema).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Successfully updated avatar for user: {}", email);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Err(e) => Err(anyhow::anyhow!("Failed to update user avatar: {}", e)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_file(state: &AppState, user_id: String, mut multipart: Multipart) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
let user_id = user_id.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
info!("Entering upload_file function for user_id: {}", user_id);
|
||||||
|
|
||||||
|
// Initialize MinIO configuration
|
||||||
|
let minio_config = match MinioConfig::from_env() {
|
||||||
|
Ok(config) => {
|
||||||
|
info!("MinIO config loaded successfully.");
|
||||||
|
config
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to load MinIO config: {}", e);
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"MinIO configuration error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store bucket name before minio_config is moved
|
||||||
|
let bucket_name = minio_config.bucket_name.clone();
|
||||||
|
info!("MinIO bucket name: {}", bucket_name);
|
||||||
|
|
||||||
|
// Initialize MinIO service
|
||||||
|
let minio_service = match create_minio_service_from_config(minio_config).await {
|
||||||
|
Ok(service) => {
|
||||||
|
info!("MinIO service initialized successfully.");
|
||||||
|
service
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to initialize MinIO service: {}", e);
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"MinIO service initialization error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get actual user data from database using user_id (which is a UUID)
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &user_id);
|
||||||
|
let user_data = match repo.query_user_by_id(&thing_id).await {
|
||||||
|
Ok(user) => {
|
||||||
|
info!("Found user in DB. User ID: {}, Email: {}", user.id.id.to_raw(), user.email);
|
||||||
|
user
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to find user in DB for ID {}: {}", user_id, e);
|
||||||
|
return common_response(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"User not found",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let actual_user_id = user_data.id.id.to_raw();
|
||||||
|
let user_email = user_data.email;
|
||||||
|
|
||||||
|
let mut file_data: Option<Vec<u8>> = None;
|
||||||
|
let mut filename: Option<String> = None;
|
||||||
|
let mut content_type: Option<String> = None;
|
||||||
|
|
||||||
|
info!("Starting multipart form processing.");
|
||||||
|
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
||||||
|
let name = field.name().unwrap_or("").to_string();
|
||||||
|
info!("Processing multipart field: {}", name);
|
||||||
|
|
||||||
|
match name.as_str() {
|
||||||
|
"file" => {
|
||||||
|
filename = field.file_name().map(|s| s.to_string());
|
||||||
|
content_type = field.content_type().map(|s| s.to_string());
|
||||||
|
info!("Detected file field. Filename: {:?}, Content-Type: {:?}", filename, content_type);
|
||||||
|
|
||||||
|
match field.bytes().await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
file_data = Some(bytes.to_vec());
|
||||||
|
info!("Successfully read file data, size: {} bytes", file_data.as_ref().map_or(0, |d| d.len()));
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to read file data from multipart: {}", e);
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Failed to read file data",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"base64_data" => {
|
||||||
|
let base64_str = field.text().await.unwrap_or_default();
|
||||||
|
info!("Detected base64_data field, length: {}", base64_str.len());
|
||||||
|
if !base64_str.is_empty() {
|
||||||
|
match decode_base64_file(&base64_str) {
|
||||||
|
Ok(decoded_data) => {
|
||||||
|
file_data = Some(decoded_data);
|
||||||
|
info!("Successfully decoded base64 data, size: {} bytes", file_data.as_ref().map_or(0, |d| d.len()));
|
||||||
|
if let Some(detected_type) = extract_content_type_from_data_url(&base64_str) {
|
||||||
|
content_type = Some(detected_type);
|
||||||
|
info!("Detected content type from base64 data URL: {}", content_type.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to decode base64 data: {}", e);
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Invalid base64 data",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"filename" => {
|
||||||
|
filename = Some(field.text().await.unwrap_or_default());
|
||||||
|
info!("Received filename from field: {:?}", filename);
|
||||||
|
}
|
||||||
|
"content_type" => {
|
||||||
|
content_type = Some(field.text().await.unwrap_or_default());
|
||||||
|
info!("Received content_type from field: {:?}", content_type);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
info!("Skipping unknown multipart field: {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("Finished multipart form processing.");
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
let file_data = match file_data {
|
||||||
|
Some(data) => data,
|
||||||
|
None => {
|
||||||
|
error!("File data is missing after multipart processing.");
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"file data is required",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("File data extracted, size: {} bytes.", file_data.len());
|
||||||
|
|
||||||
|
let filename = filename.unwrap_or_else(|| {
|
||||||
|
info!("Filename not provided, defaulting to 'unnamed_file'.");
|
||||||
|
"unnamed_file".to_string()
|
||||||
|
});
|
||||||
|
let content_type = content_type.unwrap_or_else(|| {
|
||||||
|
info!("Content type not provided, defaulting to 'application/octet-stream'.");
|
||||||
|
"application/octet-stream".to_string()
|
||||||
|
});
|
||||||
|
info!("Final filename: {}, Content-Type: {}", filename, content_type);
|
||||||
|
|
||||||
|
// Auto-detect file type based on content type and filename
|
||||||
|
let file_type = FileType::from_content_type(&content_type);
|
||||||
|
let file_type = if matches!(file_type, FileType::Unknown) {
|
||||||
|
info!("Content type detection failed, trying from filename.");
|
||||||
|
FileType::from_filename(&filename)
|
||||||
|
} else {
|
||||||
|
file_type
|
||||||
|
};
|
||||||
|
info!("Detected file type: {:?}", file_type);
|
||||||
|
|
||||||
|
// Validate file type is supported
|
||||||
|
if matches!(file_type, FileType::Unknown) {
|
||||||
|
error!("Unsupported file type detected: {:?}", file_type);
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Unsupported file type. Supported types: JPEG, PNG, WEBP, GIF, PDF, DOC, DOCX",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file type matches content type
|
||||||
|
if !file_type.allowed_types().contains(&content_type.as_str()) {
|
||||||
|
error!("File type '{:?}' does not match content type '{}'.", file_type, content_type);
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
&format!("File type '{:?}' does not match content type '{}'", file_type, content_type),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size
|
||||||
|
if file_data.len() > file_type.max_size() {
|
||||||
|
error!("File too large. Current size: {} bytes, Max size for {:?}: {} bytes",
|
||||||
|
file_data.len(), file_type, file_type.max_size());
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
&format!("File too large. Maximum size for {:?} is {} bytes",
|
||||||
|
file_type, file_type.max_size()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
info!("File size validated: {} bytes.", file_data.len());
|
||||||
|
|
||||||
|
// Create secure upload path with user ID (sanitized for filesystem)
|
||||||
|
let sanitized_user_id = user_email
|
||||||
|
.replace("%", "")
|
||||||
|
.replace(":", "_")
|
||||||
|
.replace("@", "_at_")
|
||||||
|
.replace(".", "_");
|
||||||
|
info!("Sanitized user ID for folder path: {}", sanitized_user_id);
|
||||||
|
|
||||||
|
let folder = format!("{}/{}", file_type.as_folder(), sanitized_user_id);
|
||||||
|
info!("Upload folder: {}", folder);
|
||||||
|
|
||||||
|
// Upload file to MinIO with deduplication
|
||||||
|
info!("Attempting to upload file to MinIO.");
|
||||||
|
match minio_service.upload_file_with_deduplication(&file_data, &content_type, &folder, &filename).await {
|
||||||
|
Ok(object_path) => {
|
||||||
|
info!("File uploaded successfully to MinIO. Object path: {}", object_path);
|
||||||
|
// Create permanent URL (no expiration)
|
||||||
|
let permanent_url = format!("https://cdn.asepharyana.tech/{}/{}",
|
||||||
|
bucket_name, object_path);
|
||||||
|
info!("Permanent URL: {}", permanent_url);
|
||||||
|
|
||||||
|
let response_data = json!({
|
||||||
|
"filename": filename,
|
||||||
|
"original_filename": filename,
|
||||||
|
"uploaded_path": object_path,
|
||||||
|
"url": permanent_url,
|
||||||
|
"size": file_data.len(),
|
||||||
|
"content_type": content_type,
|
||||||
|
"file_type": format!("{:?}", file_type).to_lowercase(),
|
||||||
|
"user_id": actual_user_id,
|
||||||
|
"email": user_email
|
||||||
|
});
|
||||||
|
|
||||||
|
success_response(ResponseSuccessDto {
|
||||||
|
data: response_data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to upload file to MinIO: {}", e);
|
||||||
|
common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("Upload failed: {}", e),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
imphnen-entities.workspace = true
|
imphnen-entities.workspace = true
|
||||||
|
once_cell = { workspace = true }
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
@@ -15,3 +16,11 @@ chrono.workspace = true
|
|||||||
surrealdb.workspace = true
|
surrealdb.workspace = true
|
||||||
jsonwebtoken.workspace = true
|
jsonwebtoken.workspace = true
|
||||||
dotenvy.workspace = true
|
dotenvy.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
hmac.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
urlencoding.workspace = true
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ use argon2::{
|
|||||||
|
|
||||||
pub fn hash_password(password: &str) -> Result<String, Error> {
|
pub fn hash_password(password: &str) -> Result<String, Error> {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
let argon2 = Argon2::default();
|
let argon2 = Argon2::new(
|
||||||
|
argon2::Algorithm::Argon2id,
|
||||||
|
argon2::Version::V0x13,
|
||||||
|
argon2::Params::new(1024, 1, 1, None).unwrap() // 1MB, 1 iteration, 1 thread (faster, less secure)
|
||||||
|
);
|
||||||
let password_hash = argon2
|
let password_hash = argon2
|
||||||
.hash_password(password.as_bytes(), &salt)?
|
.hash_password(password.as_bytes(), &salt)?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use crate::{Env, load_env, surrealdb_init_mem, surrealdb_init_ws};
|
use crate::{surrealdb_init_mem, surrealdb_init_ws};
|
||||||
use axum::{Router, serve};
|
use axum::{Router, serve};
|
||||||
use imphnen_entities::{SurrealMemClient, SurrealWsClient};
|
use imphnen_entities::{SurrealMemClient, SurrealWsClient};
|
||||||
use std::{future::Future, net::SocketAddr};
|
use std::{future::Future, net::SocketAddr};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
use crate::enviroment::ENV;
|
||||||
|
|
||||||
pub async fn axum_init<F, Fut>(router_fn: F)
|
pub async fn axum_init<F, Fut>(router_fn: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(SurrealWsClient, SurrealMemClient) -> Fut,
|
F: FnOnce(SurrealWsClient, SurrealMemClient) -> Fut,
|
||||||
Fut: Future<Output = Router>,
|
Fut: Future<Output = Router>,
|
||||||
{
|
{
|
||||||
load_env();
|
let env = &ENV;
|
||||||
let env = Env::new();
|
|
||||||
|
|
||||||
let surrealdb_ws = surrealdb_init_ws().await.expect("Failed surrealdb ws");
|
let surrealdb_ws = surrealdb_init_ws().await.expect("Failed surrealdb ws");
|
||||||
|
|
||||||
|
|||||||
@@ -1,100 +1,128 @@
|
|||||||
|
//! Environment configuration module using once_cell::sync::Lazy for one-time loading.
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
// Logging for warnings if .env is missing
|
||||||
|
use log::{warn, info};
|
||||||
|
|
||||||
pub fn load_env() {
|
/// Struct holding all environment configuration.
|
||||||
if dotenvy::dotenv().is_ok() {}
|
#[derive(Clone)]
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Env {
|
pub struct Env {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub access_token_secret: String,
|
pub access_token_secret: String,
|
||||||
pub refresh_token_secret: String,
|
pub refresh_token_secret: String,
|
||||||
pub surrealdb_url: String,
|
pub surrealdb_url: String,
|
||||||
pub surrealdb_username: String,
|
pub surrealdb_username: String,
|
||||||
pub surrealdb_password: String,
|
pub surrealdb_password: String,
|
||||||
pub surrealdb_namespace: String,
|
pub surrealdb_namespace: String,
|
||||||
pub surrealdb_dbname: String,
|
pub surrealdb_dbname: String,
|
||||||
pub surrealdb_url_ws: String,
|
pub surrealdb_url_ws: String,
|
||||||
pub smtp_email: String,
|
pub smtp_email: String,
|
||||||
pub smtp_password: String,
|
pub smtp_password: String,
|
||||||
pub smtp_name: String,
|
pub smtp_name: String,
|
||||||
pub smtp_host: String,
|
pub smtp_host: String,
|
||||||
pub redisdb_url: String,
|
pub redisdb_url: String,
|
||||||
pub fe_url: String,
|
pub fe_url: String,
|
||||||
pub rust_env: String,
|
pub rust_env: String,
|
||||||
pub minio_endpoint: String,
|
pub minio_endpoint: String,
|
||||||
pub minio_bucket_name: String,
|
pub minio_bucket_name: String,
|
||||||
pub minio_access_key: String,
|
pub minio_access_key: String,
|
||||||
pub minio_secret_key: String,
|
pub minio_secret_key: String,
|
||||||
|
pub minio_region: String,
|
||||||
|
pub minio_secure: bool,
|
||||||
|
// Google OAuth 2.1
|
||||||
|
pub google_client_id: String,
|
||||||
|
pub google_client_secret: String,
|
||||||
|
pub google_redirect_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Env {
|
// Custom Debug implementation to mask secrets in logs
|
||||||
#[must_use]
|
impl std::fmt::Debug for Env {
|
||||||
pub fn new() -> Self {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
Self {
|
f.debug_struct("Env")
|
||||||
port: env::var("PORT")
|
.field("port", &self.port)
|
||||||
.unwrap_or_else(|_| "3000".to_string())
|
.field("access_token_secret", &"***")
|
||||||
.parse()
|
.field("refresh_token_secret", &"***")
|
||||||
.unwrap_or(3000),
|
.field("surrealdb_url", &self.surrealdb_url)
|
||||||
|
.field("surrealdb_username", &self.surrealdb_username)
|
||||||
rust_env: env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()),
|
.field("surrealdb_password", &"***")
|
||||||
|
.field("surrealdb_namespace", &self.surrealdb_namespace)
|
||||||
access_token_secret: env::var("ACCESS_TOKEN_SECRET")
|
.field("surrealdb_dbname", &self.surrealdb_dbname)
|
||||||
.unwrap_or_else(|_| "default_access_secret".to_string()),
|
.field("surrealdb_url_ws", &self.surrealdb_url_ws)
|
||||||
|
.field("smtp_email", &self.smtp_email)
|
||||||
refresh_token_secret: env::var("REFRESH_TOKEN_SECRET")
|
.field("smtp_password", &"***")
|
||||||
.unwrap_or_else(|_| "default_refresh_secret".to_string()),
|
.field("smtp_name", &self.smtp_name)
|
||||||
|
.field("smtp_host", &self.smtp_host)
|
||||||
surrealdb_url: env::var("SURREALDB_URL")
|
.field("redisdb_url", &self.redisdb_url)
|
||||||
.unwrap_or_else(|_| "http://localhost:8000".to_string()),
|
.field("fe_url", &self.fe_url)
|
||||||
|
.field("rust_env", &self.rust_env)
|
||||||
surrealdb_username: env::var("SURREALDB_USERNAME")
|
.field("minio_endpoint", &self.minio_endpoint)
|
||||||
.unwrap_or_else(|_| "root".to_string()),
|
.field("minio_bucket_name", &self.minio_bucket_name)
|
||||||
|
.field("minio_access_key", &"***")
|
||||||
surrealdb_password: env::var("SURREALDB_PASSWORD")
|
.field("minio_secret_key", &"***")
|
||||||
.unwrap_or_else(|_| "password".to_string()),
|
.field("minio_region", &self.minio_region)
|
||||||
|
.field("minio_secure", &self.minio_secure)
|
||||||
surrealdb_namespace: env::var("SURREALDB_NAMESPACE")
|
.field("google_client_id", &self.google_client_id)
|
||||||
.unwrap_or_else(|_| "namespace".to_string()),
|
.field("google_client_secret", &"***")
|
||||||
|
.field("google_redirect_url", &self.google_redirect_url)
|
||||||
surrealdb_dbname: env::var("SURREALDB_DBNAME")
|
.finish()
|
||||||
.unwrap_or_else(|_| "database".to_string()),
|
}
|
||||||
|
|
||||||
surrealdb_url_ws: env::var("SURREALDB_URL_WS")
|
|
||||||
.unwrap_or_else(|_| "ws://localhost:8000/rpc".to_string()),
|
|
||||||
|
|
||||||
smtp_email: env::var("SMTP_EMAIL")
|
|
||||||
.unwrap_or_else(|_| "no-reply@example.com".to_string()),
|
|
||||||
|
|
||||||
smtp_password: env::var("SMTP_PASSWORD")
|
|
||||||
.unwrap_or_else(|_| "default_smtp_password".to_string()),
|
|
||||||
|
|
||||||
smtp_name: env::var("SMTP_NAME").unwrap_or_else(|_| "MyApp SMTP".to_string()),
|
|
||||||
|
|
||||||
smtp_host: env::var("SMTP_HOST")
|
|
||||||
.unwrap_or_else(|_| "smtp.gmail.com".to_string()),
|
|
||||||
|
|
||||||
redisdb_url: env::var("REDISDB_URL")
|
|
||||||
.unwrap_or_else(|_| "localhost".to_string()),
|
|
||||||
|
|
||||||
fe_url: env::var("FE_URL").unwrap_or_else(|_| "http://localhost".to_string()),
|
|
||||||
|
|
||||||
minio_endpoint: env::var("MINIO_ENDPOINT")
|
|
||||||
.unwrap_or_else(|_| "http://localhost:9000".to_string()),
|
|
||||||
|
|
||||||
minio_bucket_name: env::var("MINIO_BUCKET_NAME")
|
|
||||||
.unwrap_or_else(|_| "default_bucket".to_string()),
|
|
||||||
|
|
||||||
minio_access_key: env::var("MINIO_ACCESS_KEY")
|
|
||||||
.unwrap_or_else(|_| "minio_access".to_string()),
|
|
||||||
|
|
||||||
minio_secret_key: env::var("MINIO_SECRET_KEY")
|
|
||||||
.unwrap_or_else(|_| "minio_secret".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Env {
|
/// Helper to get env var with warning if not set.
|
||||||
fn default() -> Self {
|
fn get_env_with_warning(key: &str, default: &str) -> String {
|
||||||
Self::new()
|
match env::var(key) {
|
||||||
}
|
Ok(val) => val,
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Environment variable '{}' is not set. Using default: '{}'", key, default);
|
||||||
|
default.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads environment variables from .env and system, only once.
|
||||||
|
pub static ENV: Lazy<Env> = Lazy::new(|| {
|
||||||
|
// Try to load .env file, log a warning if not found, proceed regardless.
|
||||||
|
match dotenvy::dotenv() {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(dotenvy::Error::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
warn!(".env file not found, falling back to system environment variables");
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let env = Env {
|
||||||
|
port: get_env_with_warning("PORT", "3000")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(3000),
|
||||||
|
access_token_secret: get_env_with_warning("ACCESS_TOKEN_SECRET", "default_access_secret"),
|
||||||
|
refresh_token_secret: get_env_with_warning("REFRESH_TOKEN_SECRET", "default_refresh_secret"),
|
||||||
|
surrealdb_url: get_env_with_warning("SURREALDB_URL", "http://localhost:8000"),
|
||||||
|
surrealdb_username: get_env_with_warning("SURREALDB_USERNAME", "root"),
|
||||||
|
surrealdb_password: get_env_with_warning("SURREALDB_PASSWORD", "root"),
|
||||||
|
surrealdb_namespace: get_env_with_warning("SURREALDB_NAMESPACE", "namespace"),
|
||||||
|
surrealdb_dbname: get_env_with_warning("SURREALDB_DBNAME", "database"),
|
||||||
|
smtp_email: get_env_with_warning("SMTP_EMAIL", "no-reply@example.com"),
|
||||||
|
smtp_password: get_env_with_warning("SMTP_PASSWORD", "default_smtp_password"),
|
||||||
|
smtp_name: get_env_with_warning("SMTP_NAME", "MyApp SMTP"),
|
||||||
|
smtp_host: get_env_with_warning("SMTP_HOST", "smtp.gmail.com"),
|
||||||
|
redisdb_url: get_env_with_warning("REDISDB_URL", "localhost"),
|
||||||
|
fe_url: get_env_with_warning("FE_URL", "http://localhost"),
|
||||||
|
rust_env: get_env_with_warning("RUST_ENV", "development"),
|
||||||
|
minio_endpoint: get_env_with_warning("MINIO_ENDPOINT", "http://localhost:9000"),
|
||||||
|
minio_bucket_name: get_env_with_warning("MINIO_BUCKET_NAME", "imphnen-uploads"),
|
||||||
|
minio_access_key: get_env_with_warning("MINIO_ACCESS_KEY", "minio_access"),
|
||||||
|
minio_secret_key: get_env_with_warning("MINIO_SECRET_KEY", "minio_secret"),
|
||||||
|
minio_region: get_env_with_warning("MINIO_REGION", "us-east-1"),
|
||||||
|
minio_secure: get_env_with_warning("MINIO_SECURE", "false")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(false),
|
||||||
|
surrealdb_url_ws: String::new(),
|
||||||
|
// Google OAuth 2.1
|
||||||
|
google_client_id: get_env_with_warning("GOOGLE_CLIENT_ID", "default_google_client_id"),
|
||||||
|
google_client_secret: get_env_with_warning("GOOGLE_CLIENT_SECRET", "default_google_client_secret"),
|
||||||
|
google_redirect_url: get_env_with_warning("GOOGLE_REDIRECT_URL", "http://localhost:8000/api/v1/auth/google/callback"),
|
||||||
|
};
|
||||||
|
info!("Loaded environment configuration: {:?}", env);
|
||||||
|
env
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::Env;
|
use crate::enviroment::ENV;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use chrono::{Duration, TimeDelta, Utc};
|
use chrono::{Duration, TimeDelta, Utc};
|
||||||
use jsonwebtoken::{
|
use jsonwebtoken::{
|
||||||
@@ -11,32 +11,36 @@ pub struct Claims {
|
|||||||
pub exp: usize,
|
pub exp: usize,
|
||||||
pub iat: usize,
|
pub iat: usize,
|
||||||
pub sub: String,
|
pub sub: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub permissions: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_access_token(sub: String) -> Result<String, StatusCode> {
|
static ACCESS_HEADER: once_cell::sync::Lazy<Header> = once_cell::sync::Lazy::new(Header::default);
|
||||||
let env = Env::new();
|
static ACCESS_KEY: once_cell::sync::Lazy<EncodingKey> = once_cell::sync::Lazy::new(|| {
|
||||||
let secret: String = env.access_token_secret;
|
EncodingKey::from_secret(ENV.access_token_secret.as_ref())
|
||||||
|
});
|
||||||
|
pub fn encode_access_token(sub: String, user_id: String, permissions: Vec<String>) -> Result<String, StatusCode> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let expire: TimeDelta = Duration::minutes(15);
|
let expire: TimeDelta = Duration::minutes(15);
|
||||||
let exp: usize = (now + expire).timestamp() as usize;
|
let exp: usize = (now + expire).timestamp() as usize;
|
||||||
let iat: usize = now.timestamp() as usize;
|
let iat: usize = now.timestamp() as usize;
|
||||||
let claim = Claims { iat, exp, sub };
|
let claim = Claims { iat, exp, sub, user_id, permissions };
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&ACCESS_HEADER,
|
||||||
&claim,
|
&claim,
|
||||||
&EncodingKey::from_secret(secret.as_ref()),
|
&ACCESS_KEY,
|
||||||
)
|
)
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_reset_password_token(sub: String) -> Result<String, StatusCode> {
|
pub fn encode_reset_password_token(sub: String, user_id: String, permissions: Vec<String>) -> Result<String, StatusCode> {
|
||||||
let env = Env::new();
|
let env = &ENV;
|
||||||
let secret: String = env.access_token_secret;
|
let secret: String = env.access_token_secret.clone();
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let expire: TimeDelta = Duration::minutes(5);
|
let expire: TimeDelta = Duration::minutes(5);
|
||||||
let exp: usize = (now + expire).timestamp() as usize;
|
let exp: usize = (now + expire).timestamp() as usize;
|
||||||
let iat: usize = now.timestamp() as usize;
|
let iat: usize = now.timestamp() as usize;
|
||||||
let claim = Claims { iat, exp, sub };
|
let claim = Claims { iat, exp, sub, user_id, permissions };
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
&claim,
|
&claim,
|
||||||
@@ -48,8 +52,8 @@ pub fn encode_reset_password_token(sub: String) -> Result<String, StatusCode> {
|
|||||||
pub fn decode_access_token(
|
pub fn decode_access_token(
|
||||||
jwt_token: &str,
|
jwt_token: &str,
|
||||||
) -> Result<TokenData<Claims>, StatusCode> {
|
) -> Result<TokenData<Claims>, StatusCode> {
|
||||||
let env = Env::new();
|
let env = &ENV;
|
||||||
let secret: String = env.access_token_secret;
|
let secret: String = env.access_token_secret.clone();
|
||||||
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
||||||
jwt_token,
|
jwt_token,
|
||||||
&DecodingKey::from_secret(secret.as_ref()),
|
&DecodingKey::from_secret(secret.as_ref()),
|
||||||
@@ -59,18 +63,20 @@ pub fn decode_access_token(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_refresh_token(sub: String) -> Result<String, StatusCode> {
|
static REFRESH_HEADER: once_cell::sync::Lazy<Header> = once_cell::sync::Lazy::new(Header::default);
|
||||||
let env = Env::new();
|
static REFRESH_KEY: once_cell::sync::Lazy<EncodingKey> = once_cell::sync::Lazy::new(|| {
|
||||||
let secret: String = env.refresh_token_secret;
|
EncodingKey::from_secret(ENV.refresh_token_secret.as_ref())
|
||||||
|
});
|
||||||
|
pub fn encode_refresh_token(sub: String, user_id: String, permissions: Vec<String>) -> Result<String, StatusCode> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let expire: TimeDelta = Duration::days(1);
|
let expire: TimeDelta = Duration::days(1);
|
||||||
let exp: usize = (now + expire).timestamp() as usize;
|
let exp: usize = (now + expire).timestamp() as usize;
|
||||||
let iat: usize = now.timestamp() as usize;
|
let iat: usize = now.timestamp() as usize;
|
||||||
let claim = Claims { iat, exp, sub };
|
let claim = Claims { iat, exp, sub, user_id, permissions };
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&REFRESH_HEADER,
|
||||||
&claim,
|
&claim,
|
||||||
&EncodingKey::from_secret(secret.as_ref()),
|
&REFRESH_KEY,
|
||||||
)
|
)
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
@@ -78,13 +84,17 @@ pub fn encode_refresh_token(sub: String) -> Result<String, StatusCode> {
|
|||||||
pub fn decode_refresh_token(
|
pub fn decode_refresh_token(
|
||||||
jwt_token: &str,
|
jwt_token: &str,
|
||||||
) -> Result<TokenData<Claims>, StatusCode> {
|
) -> Result<TokenData<Claims>, StatusCode> {
|
||||||
let env = Env::new();
|
let env = &ENV;
|
||||||
let secret: String = env.refresh_token_secret;
|
let secret: String = env.refresh_token_secret.clone();
|
||||||
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
||||||
jwt_token,
|
jwt_token,
|
||||||
&DecodingKey::from_secret(secret.as_ref()),
|
&DecodingKey::from_secret(secret.as_ref()),
|
||||||
&Validation::default(),
|
&Validation::default(),
|
||||||
)
|
)
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR);
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
result
|
result // Explicitly return result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_jwt(user_id: &str, permissions: Vec<String>) -> Result<String, StatusCode> {
|
||||||
|
encode_access_token(user_id.to_string(), user_id.to_string(), permissions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::Env;
|
use crate::enviroment::ENV;
|
||||||
use lettre::message::Mailbox;
|
use lettre::message::Mailbox;
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
@@ -9,11 +9,11 @@ pub fn send_email(
|
|||||||
subject: &str,
|
subject: &str,
|
||||||
body: &str,
|
body: &str,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let env = Env::new();
|
let env = &ENV;
|
||||||
let host = env.smtp_host;
|
let host = env.smtp_host.clone();
|
||||||
let sender_email = env.smtp_email;
|
let sender_email = env.smtp_email.clone();
|
||||||
let sender_name = env.smtp_name;
|
let sender_name = env.smtp_name.clone();
|
||||||
let sender_password = env.smtp_password;
|
let sender_password = env.smtp_password.clone();
|
||||||
let recipient_email = to;
|
let recipient_email = to;
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from(Mailbox::new(
|
.from(Mailbox::new(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod axum;
|
|||||||
pub mod enviroment;
|
pub mod enviroment;
|
||||||
pub mod jsonwebtoken;
|
pub mod jsonwebtoken;
|
||||||
pub mod lettre;
|
pub mod lettre;
|
||||||
|
pub mod minio;
|
||||||
pub mod surrealdb;
|
pub mod surrealdb;
|
||||||
|
|
||||||
pub use argon::*;
|
pub use argon::*;
|
||||||
@@ -13,4 +14,5 @@ pub use enviroment::*;
|
|||||||
pub use imphnen_entities::*;
|
pub use imphnen_entities::*;
|
||||||
pub use jsonwebtoken::*;
|
pub use jsonwebtoken::*;
|
||||||
pub use lettre::*;
|
pub use lettre::*;
|
||||||
|
pub use minio::*;
|
||||||
pub use surrealdb::*;
|
pub use surrealdb::*;
|
||||||
|
|||||||
@@ -0,0 +1,730 @@
|
|||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use chrono::Utc;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::enviroment::ENV;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- Struct Konfigurasi MinIO ---
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MinioConfig {
|
||||||
|
pub endpoint: String,
|
||||||
|
pub access_key: String,
|
||||||
|
pub secret_key: String,
|
||||||
|
pub bucket_name: String,
|
||||||
|
pub region: String,
|
||||||
|
pub secure: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinioConfig {
|
||||||
|
/// Memuat konfigurasi MinIO dari variabel lingkungan.
|
||||||
|
pub fn from_env() -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
endpoint: ENV.minio_endpoint.clone(),
|
||||||
|
access_key: ENV.minio_access_key.clone(),
|
||||||
|
secret_key: ENV.minio_secret_key.clone(),
|
||||||
|
bucket_name: ENV.minio_bucket_name.clone(),
|
||||||
|
region: ENV.minio_region.clone(),
|
||||||
|
secure: ENV.minio_secure,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mendapatkan URL endpoint lengkap (http atau https).
|
||||||
|
pub fn endpoint_url(&self) -> String {
|
||||||
|
// If endpoint already has protocol, use it as-is
|
||||||
|
if self.endpoint.starts_with("http://") || self.endpoint.starts_with("https://") {
|
||||||
|
self.endpoint.clone()
|
||||||
|
} else {
|
||||||
|
// Only add protocol if not present
|
||||||
|
let protocol = if self.secure { "https" } else { "http" };
|
||||||
|
format!("{}://{}", protocol, self.endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Layanan MinIO ---
|
||||||
|
pub struct MinioService {
|
||||||
|
endpoint: String,
|
||||||
|
access_key: String,
|
||||||
|
secret_key: String,
|
||||||
|
bucket_name: String,
|
||||||
|
region: String,
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinioService {
|
||||||
|
/// Membuat instance layanan MinIO baru.
|
||||||
|
pub async fn new(
|
||||||
|
endpoint: &str,
|
||||||
|
access_key: &str,
|
||||||
|
secret_key: &str,
|
||||||
|
bucket_name: &str,
|
||||||
|
region: &str,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let service = Self {
|
||||||
|
endpoint: endpoint.to_string(),
|
||||||
|
access_key: access_key.to_string(),
|
||||||
|
secret_key: secret_key.to_string(),
|
||||||
|
bucket_name: bucket_name.to_string(),
|
||||||
|
region: region.to_string(),
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
};
|
||||||
|
Ok(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mengunggah file biner ke MinIO dengan deduplication berdasarkan hash.
|
||||||
|
pub async fn upload_file_with_deduplication(
|
||||||
|
&self,
|
||||||
|
file_data: &[u8],
|
||||||
|
content_type: &str,
|
||||||
|
folder: &str,
|
||||||
|
original_filename: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
Self::validate_file_type(content_type, file_data)?;
|
||||||
|
|
||||||
|
// Calculate file hash
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(file_data);
|
||||||
|
let file_hash = format!("{:x}", hasher.finalize());
|
||||||
|
let short_hash = &file_hash[..16]; // Use first 16 characters for filename
|
||||||
|
|
||||||
|
// Check if file with same hash already exists
|
||||||
|
if let Some(existing_file) = self.check_file_exists_by_hash(folder, short_hash).await? {
|
||||||
|
log::info!("File with same content already exists: {}", existing_file);
|
||||||
|
return Ok(existing_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_extension = Self::get_file_extension(original_filename);
|
||||||
|
let unique_filename = format!("{}/{}-{}.{}", folder, short_hash, Uuid::new_v4(), file_extension);
|
||||||
|
let object_name = &unique_filename;
|
||||||
|
|
||||||
|
// Extract host from endpoint (remove protocol)
|
||||||
|
let host = self.endpoint
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.trim_start_matches("http://");
|
||||||
|
|
||||||
|
let url = format!("https://{}/{}/{}", host, self.bucket_name, object_name);
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log::debug!("MinIO Endpoint config: {}", self.endpoint);
|
||||||
|
log::debug!("MinIO Region config: {}", self.region);
|
||||||
|
log::debug!("Upload URL: {}", url);
|
||||||
|
log::debug!("Object name: {}", object_name);
|
||||||
|
log::debug!("File hash: {}", short_hash);
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
|
||||||
|
let date_stamp = now.format("%Y%m%d").to_string();
|
||||||
|
|
||||||
|
// Use UNSIGNED-PAYLOAD for simpler signature
|
||||||
|
let payload_hash = "UNSIGNED-PAYLOAD".to_string();
|
||||||
|
|
||||||
|
let canonical_headers = format!(
|
||||||
|
"host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n",
|
||||||
|
host, payload_hash, amz_date
|
||||||
|
);
|
||||||
|
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
|
||||||
|
|
||||||
|
// For path-style, canonical URI should be /bucket/object
|
||||||
|
let canonical_uri = format!("/{}/{}", self.bucket_name, object_name);
|
||||||
|
let canonical_request = format!(
|
||||||
|
"PUT\n{}\n\n{}\n{}\n{}",
|
||||||
|
canonical_uri, canonical_headers, signed_headers, payload_hash
|
||||||
|
);
|
||||||
|
|
||||||
|
log::debug!("Canonical request:\n{}", canonical_request);
|
||||||
|
|
||||||
|
let scope = format!("{}/{}/s3/aws4_request", date_stamp, self.region);
|
||||||
|
let string_to_sign = format!(
|
||||||
|
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
|
||||||
|
amz_date,
|
||||||
|
scope,
|
||||||
|
hex::encode(Sha256::digest(canonical_request.as_bytes()))
|
||||||
|
);
|
||||||
|
|
||||||
|
log::debug!("Scope: {}", scope);
|
||||||
|
log::debug!("String to sign:\n{}", string_to_sign);
|
||||||
|
|
||||||
|
let signing_key = self.get_signature_key(&date_stamp)?;
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
|
||||||
|
mac.update(string_to_sign.as_bytes());
|
||||||
|
let signature = hex::encode(mac.finalize().into_bytes());
|
||||||
|
|
||||||
|
log::debug!("Generated signature: {}", signature);
|
||||||
|
|
||||||
|
let auth_header = format!(
|
||||||
|
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
|
||||||
|
self.access_key, scope, signed_headers, signature
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5) Send request: Content-Type included but NOT signed
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.put(&url)
|
||||||
|
.header("x-amz-date", &amz_date)
|
||||||
|
.header("x-amz-content-sha256", &payload_hash)
|
||||||
|
.header("Authorization", &auth_header)
|
||||||
|
.header("Content-Type", content_type)
|
||||||
|
// Don't set Host header manually - let reqwest handle it
|
||||||
|
// Add headers for reverse proxy support (not signed)
|
||||||
|
.header("X-Forwarded-Proto", "https")
|
||||||
|
.header("X-Forwarded-Host", host)
|
||||||
|
.body(file_data.to_vec())
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_body = response.text().await?;
|
||||||
|
bail!(
|
||||||
|
"Gagal mengunggah file ke MinIO. Status: {}. Pesan: {}",
|
||||||
|
status,
|
||||||
|
error_body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Unggahan berhasil: {} byte ke {}", file_data.len(), unique_filename);
|
||||||
|
Ok(unique_filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mengunggah file biner ke MinIO.
|
||||||
|
pub async fn upload_file(
|
||||||
|
&self,
|
||||||
|
file_data: &[u8],
|
||||||
|
content_type: &str,
|
||||||
|
folder: &str,
|
||||||
|
original_filename: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
Self::validate_file_type(content_type, file_data)?;
|
||||||
|
|
||||||
|
let file_extension = Self::get_file_extension(original_filename);
|
||||||
|
let unique_filename = format!("{}/{}.{}", folder, Uuid::new_v4(), file_extension);
|
||||||
|
let object_name = &unique_filename;
|
||||||
|
|
||||||
|
// Extract host from endpoint (remove protocol)
|
||||||
|
let host = self.endpoint
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.trim_start_matches("http://");
|
||||||
|
|
||||||
|
let url = format!("https://{}/{}/{}", host, self.bucket_name, object_name);
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log::debug!("MinIO Endpoint config: {}", self.endpoint);
|
||||||
|
log::debug!("MinIO Region config: {}", self.region);
|
||||||
|
log::debug!("MinIO Access Key: {}", self.access_key);
|
||||||
|
log::debug!("MinIO Bucket: {}", self.bucket_name);
|
||||||
|
log::debug!("Extracted host: {}", host);
|
||||||
|
log::debug!("Final URL: {}", url);
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
|
||||||
|
let date_stamp = now.format("%Y%m%d").to_string();
|
||||||
|
|
||||||
|
// 1) Use UNSIGNED-PAYLOAD for HTTPS uploads (safer for proxies)
|
||||||
|
let payload_hash = "UNSIGNED-PAYLOAD".to_string();
|
||||||
|
|
||||||
|
// 2) Path-style canonical URI: /{bucket}/{object}
|
||||||
|
let canonical_uri = format!("/{}/{}", self.bucket_name, object_name);
|
||||||
|
|
||||||
|
// 3) ONLY sign essential headers (no content-type to avoid proxy issues)
|
||||||
|
let canonical_headers = format!(
|
||||||
|
"host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n",
|
||||||
|
host, payload_hash, amz_date
|
||||||
|
);
|
||||||
|
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
|
||||||
|
|
||||||
|
// 4) Canonical request
|
||||||
|
let canonical_request = format!(
|
||||||
|
"PUT\n{}\n\n{}\n{}\n{}",
|
||||||
|
canonical_uri, canonical_headers, signed_headers, payload_hash
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log::debug!("URL: {}", url);
|
||||||
|
log::debug!("Host: {}", host);
|
||||||
|
log::debug!("Bucket: {}", self.bucket_name);
|
||||||
|
log::debug!("Object: {}", object_name);
|
||||||
|
log::debug!("Canonical URI: {}", canonical_uri);
|
||||||
|
log::debug!("Payload hash: {}", payload_hash);
|
||||||
|
log::debug!("Canonical Request:\n{}", canonical_request);
|
||||||
|
|
||||||
|
let scope = format!("{}/{}/s3/aws4_request", date_stamp, self.region);
|
||||||
|
let string_to_sign = format!(
|
||||||
|
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
|
||||||
|
amz_date,
|
||||||
|
scope,
|
||||||
|
hex::encode(Sha256::digest(canonical_request.as_bytes()))
|
||||||
|
);
|
||||||
|
|
||||||
|
log::debug!("Scope: {}", scope);
|
||||||
|
log::debug!("String to sign:\n{}", string_to_sign);
|
||||||
|
|
||||||
|
let signing_key = self.get_signature_key(&date_stamp)?;
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
|
||||||
|
mac.update(string_to_sign.as_bytes());
|
||||||
|
let signature = hex::encode(mac.finalize().into_bytes());
|
||||||
|
|
||||||
|
log::debug!("Generated signature: {}", signature);
|
||||||
|
|
||||||
|
let auth_header = format!(
|
||||||
|
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
|
||||||
|
self.access_key, scope, signed_headers, signature
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5) Send request: Content-Type included but NOT signed
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.put(&url)
|
||||||
|
.header("x-amz-date", &amz_date)
|
||||||
|
.header("x-amz-content-sha256", &payload_hash)
|
||||||
|
.header("Authorization", &auth_header)
|
||||||
|
.header("Content-Type", content_type)
|
||||||
|
// Don't set Host header manually - let reqwest handle it
|
||||||
|
// Add headers for reverse proxy support (not signed)
|
||||||
|
.header("X-Forwarded-Proto", "https")
|
||||||
|
.header("X-Forwarded-Host", host)
|
||||||
|
.body(file_data.to_vec())
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_body = response.text().await?;
|
||||||
|
bail!(
|
||||||
|
"Gagal mengunggah file ke MinIO. Status: {}. Pesan: {}",
|
||||||
|
status,
|
||||||
|
error_body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Unggahan berhasil: {} byte ke {}", file_data.len(), unique_filename);
|
||||||
|
Ok(unique_filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mengunggah file yang dikodekan base64 ke MinIO.
|
||||||
|
pub async fn upload_base64_file(
|
||||||
|
&self,
|
||||||
|
base64_data: &str,
|
||||||
|
content_type: &str,
|
||||||
|
folder: &str,
|
||||||
|
original_filename: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let file_data = decode_base64_file(base64_data)?;
|
||||||
|
self.upload_file(&file_data, content_type, folder, original_filename)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Menghasilkan URL yang telah ditandatangani sebelumnya untuk mengunduh objek.
|
||||||
|
pub async fn get_presigned_url(&self, object_name: &str, expiry_seconds: u32) -> Result<String> {
|
||||||
|
// Extract host from endpoint (remove protocol)
|
||||||
|
let host = self.endpoint
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.trim_start_matches("http://");
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
|
||||||
|
let date_stamp = now.format("%Y%m%d").to_string();
|
||||||
|
let scope = format!("{}/{}/s3/aws4_request", date_stamp, self.region);
|
||||||
|
let credential = format!("{}/{}", self.access_key, scope);
|
||||||
|
|
||||||
|
let expires_str = expiry_seconds.to_string();
|
||||||
|
let mut query_params = std::collections::BTreeMap::new();
|
||||||
|
query_params.insert("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
|
||||||
|
query_params.insert("X-Amz-Credential", &credential);
|
||||||
|
query_params.insert("X-Amz-Date", &amz_date);
|
||||||
|
query_params.insert("X-Amz-Expires", &expires_str);
|
||||||
|
query_params.insert("X-Amz-SignedHeaders", "host");
|
||||||
|
|
||||||
|
let canonical_query_string = query_params
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
let canonical_request = format!(
|
||||||
|
"GET\n/{}/{}\n{}\nhost:{}\n\nhost\nUNSIGNED-PAYLOAD",
|
||||||
|
self.bucket_name, object_name, canonical_query_string, host
|
||||||
|
);
|
||||||
|
|
||||||
|
let string_to_sign = format!(
|
||||||
|
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
|
||||||
|
amz_date,
|
||||||
|
scope,
|
||||||
|
hex::encode(Sha256::digest(canonical_request.as_bytes()))
|
||||||
|
);
|
||||||
|
|
||||||
|
let signing_key = self.get_signature_key(&date_stamp)?;
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
|
||||||
|
mac.update(string_to_sign.as_bytes());
|
||||||
|
let signature = hex::encode(mac.finalize().into_bytes());
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"https://{}/{}/{}?{}&X-Amz-Signature={}",
|
||||||
|
host, self.bucket_name, object_name, canonical_query_string, signature
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mengecek apakah file dengan hash tertentu sudah ada di bucket
|
||||||
|
pub async fn check_file_exists_by_hash(&self, folder: &str, file_hash: &str) -> Result<Option<String>> {
|
||||||
|
// Extract host from endpoint (remove protocol)
|
||||||
|
let host = self.endpoint
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.trim_start_matches("http://");
|
||||||
|
|
||||||
|
let url = format!("https://{}/{}?list-type=2&prefix={}", host, self.bucket_name, folder);
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
|
||||||
|
let date_stamp = now.format("%Y%m%d").to_string();
|
||||||
|
let payload_hash = hex::encode(Sha256::digest(b""));
|
||||||
|
|
||||||
|
let canonical_query_string = format!("list-type=2&prefix={}", urlencoding::encode(folder));
|
||||||
|
let canonical_headers = format!("host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n", host, payload_hash, amz_date);
|
||||||
|
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
|
||||||
|
let canonical_request = format!(
|
||||||
|
"GET\n/{}\n{}\n{}\n{}\n{}",
|
||||||
|
self.bucket_name, canonical_query_string, canonical_headers, signed_headers, payload_hash
|
||||||
|
);
|
||||||
|
|
||||||
|
let scope = format!("{}/{}/s3/aws4_request", date_stamp, self.region);
|
||||||
|
let string_to_sign = format!(
|
||||||
|
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
|
||||||
|
amz_date,
|
||||||
|
scope,
|
||||||
|
hex::encode(Sha256::digest(canonical_request.as_bytes()))
|
||||||
|
);
|
||||||
|
|
||||||
|
let signing_key = self.get_signature_key(&date_stamp)?;
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
|
||||||
|
mac.update(string_to_sign.as_bytes());
|
||||||
|
let signature = hex::encode(mac.finalize().into_bytes());
|
||||||
|
|
||||||
|
let auth_header = format!(
|
||||||
|
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
|
||||||
|
self.access_key, scope, signed_headers, signature
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.header("x-amz-date", &amz_date)
|
||||||
|
.header("x-amz-content-sha256", &payload_hash)
|
||||||
|
.header("Authorization", &auth_header)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = response.text().await?;
|
||||||
|
|
||||||
|
// Simple XML parsing to find files with matching hash
|
||||||
|
// Look for any file that contains the hash in its name
|
||||||
|
if body.contains(file_hash) {
|
||||||
|
// Extract the full file path from XML response
|
||||||
|
// This is a simplified approach - in production you might want proper XML parsing
|
||||||
|
for line in body.lines() {
|
||||||
|
if line.contains("<Key>") && line.contains(file_hash) {
|
||||||
|
if let Some(start) = line.find("<Key>") {
|
||||||
|
if let Some(end) = line.find("</Key>") {
|
||||||
|
let file_path = &line[start + 5..end];
|
||||||
|
return Ok(Some(file_path.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Menghapus file dari MinIO.
|
||||||
|
pub async fn delete_file(&self, object_name: &str) -> Result<()> {
|
||||||
|
// Extract host from endpoint (remove protocol)
|
||||||
|
let host = self.endpoint
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.trim_start_matches("http://");
|
||||||
|
|
||||||
|
let url = format!("https://{}/{}/{}", host, self.bucket_name, object_name);
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
|
||||||
|
let date_stamp = now.format("%Y%m%d").to_string();
|
||||||
|
let payload_hash = hex::encode(Sha256::digest(b""));
|
||||||
|
|
||||||
|
let canonical_headers = format!("host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n", host, payload_hash, amz_date);
|
||||||
|
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
|
||||||
|
let canonical_request = format!(
|
||||||
|
"DELETE\n/{}/{}\n\n{}\n{}\n{}",
|
||||||
|
self.bucket_name, object_name, canonical_headers, signed_headers, payload_hash
|
||||||
|
);
|
||||||
|
|
||||||
|
let scope = format!("{}/{}/s3/aws4_request", date_stamp, self.region);
|
||||||
|
let string_to_sign = format!(
|
||||||
|
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
|
||||||
|
amz_date,
|
||||||
|
scope,
|
||||||
|
hex::encode(Sha256::digest(canonical_request.as_bytes()))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debug logging for signature calculation
|
||||||
|
log::debug!("Region: {}", self.region);
|
||||||
|
log::debug!("Scope: {}", scope);
|
||||||
|
log::debug!("String to sign:\n{}", string_to_sign);
|
||||||
|
|
||||||
|
let signing_key = self.get_signature_key(&date_stamp)?;
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
|
||||||
|
mac.update(string_to_sign.as_bytes());
|
||||||
|
let signature = hex::encode(mac.finalize().into_bytes());
|
||||||
|
|
||||||
|
log::debug!("Final signature: {}", signature);
|
||||||
|
|
||||||
|
let auth_header = format!(
|
||||||
|
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
|
||||||
|
self.access_key, scope, signed_headers, signature
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.delete(&url)
|
||||||
|
.header("Host", host)
|
||||||
|
.header("x-amz-date", &amz_date)
|
||||||
|
.header("x-amz-content-sha256", &payload_hash)
|
||||||
|
.header("Authorization", &auth_header)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_body = response.text().await?;
|
||||||
|
bail!(
|
||||||
|
"Gagal menghapus file dari MinIO. Status: {}. Pesan: {}",
|
||||||
|
status,
|
||||||
|
error_body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("File berhasil dihapus: {}", object_name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fungsi pembantu untuk menghasilkan kunci tanda tangan AWS v4.
|
||||||
|
fn get_signature_key(&self, date_stamp: &str) -> Result<Vec<u8>> {
|
||||||
|
let secret = format!("AWS4{}", self.secret_key);
|
||||||
|
let mut mac1 = Hmac::<Sha256>::new_from_slice(secret.as_bytes())?;
|
||||||
|
mac1.update(date_stamp.as_bytes());
|
||||||
|
let date_key = mac1.finalize().into_bytes();
|
||||||
|
|
||||||
|
let mut mac2 = Hmac::<Sha256>::new_from_slice(&date_key)?;
|
||||||
|
mac2.update(self.region.as_bytes());
|
||||||
|
let date_region_key = mac2.finalize().into_bytes();
|
||||||
|
|
||||||
|
let mut mac3 = Hmac::<Sha256>::new_from_slice(&date_region_key)?;
|
||||||
|
mac3.update(b"s3");
|
||||||
|
let date_region_service_key = mac3.finalize().into_bytes();
|
||||||
|
|
||||||
|
let mut mac4 = Hmac::<Sha256>::new_from_slice(&date_region_service_key)?;
|
||||||
|
mac4.update(b"aws4_request");
|
||||||
|
Ok(mac4.finalize().into_bytes().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Memvalidasi jenis file dan ukuran.
|
||||||
|
fn validate_file_type(content_type: &str, file_data: &[u8]) -> Result<()> {
|
||||||
|
const MAX_SIZE: usize = 10 * 1024 * 1024; // 10MB
|
||||||
|
if file_data.len() > MAX_SIZE {
|
||||||
|
bail!("Ukuran file melebihi batas 10MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
match content_type {
|
||||||
|
"image/jpeg" | "image/jpg" => {
|
||||||
|
if !file_data.starts_with(&[0xFF, 0xD8, 0xFF]) {
|
||||||
|
bail!("File JPEG tidak valid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"image/png" => {
|
||||||
|
if !file_data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
|
||||||
|
bail!("File PNG tidak valid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"application/pdf" => {
|
||||||
|
if !file_data.starts_with(b"%PDF") {
|
||||||
|
bail!("File PDF tidak valid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"image/webp" => {
|
||||||
|
if !file_data.starts_with(b"RIFF")
|
||||||
|
|| !file_data.get(8..12).map_or(false, |s| s == b"WEBP")
|
||||||
|
{
|
||||||
|
bail!("File WEBP tidak valid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"application/msword" | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => {
|
||||||
|
if file_data.len() < 512 {
|
||||||
|
bail!("File dokumen tidak valid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bail!("Jenis file tidak didukung: {}", content_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mendapatkan ekstensi file dari nama file.
|
||||||
|
fn get_file_extension(filename: &str) -> String {
|
||||||
|
std::path::Path::new(filename)
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.unwrap_or("bin")
|
||||||
|
.to_lowercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Struct dan Enum Pembantu ---
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UploadResult {
|
||||||
|
pub object_name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub size: usize,
|
||||||
|
pub content_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum FileType {
|
||||||
|
Jpeg,
|
||||||
|
Png,
|
||||||
|
Webp,
|
||||||
|
Gif,
|
||||||
|
Pdf,
|
||||||
|
Doc,
|
||||||
|
Docx,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileType {
|
||||||
|
pub fn as_folder(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
FileType::Jpeg | FileType::Png | FileType::Webp | FileType::Gif => "profiles",
|
||||||
|
FileType::Pdf | FileType::Doc | FileType::Docx => "documents",
|
||||||
|
FileType::Unknown => "misc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_size(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
FileType::Jpeg | FileType::Png | FileType::Webp | FileType::Gif => 5 * 1024 * 1024, // 5MB for images
|
||||||
|
FileType::Pdf | FileType::Doc | FileType::Docx => 10 * 1024 * 1024, // 10MB for documents
|
||||||
|
FileType::Unknown => 5 * 1024 * 1024, // 5MB default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allowed_types(&self) -> Vec<&str> {
|
||||||
|
match self {
|
||||||
|
FileType::Jpeg => vec!["image/jpeg", "image/jpg"],
|
||||||
|
FileType::Png => vec!["image/png"],
|
||||||
|
FileType::Webp => vec!["image/webp"],
|
||||||
|
FileType::Gif => vec!["image/gif"],
|
||||||
|
FileType::Pdf => vec!["application/pdf"],
|
||||||
|
FileType::Doc => vec!["application/msword"],
|
||||||
|
FileType::Docx => vec!["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
||||||
|
FileType::Unknown => vec![], // No allowed types for unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_content_type(content_type: &str) -> Self {
|
||||||
|
match content_type {
|
||||||
|
"image/jpeg" | "image/jpg" => FileType::Jpeg,
|
||||||
|
"image/png" => FileType::Png,
|
||||||
|
"image/webp" => FileType::Webp,
|
||||||
|
"image/gif" => FileType::Gif,
|
||||||
|
"application/pdf" => FileType::Pdf,
|
||||||
|
"application/msword" => FileType::Doc,
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" => FileType::Docx,
|
||||||
|
_ => FileType::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_filename(filename: &str) -> Self {
|
||||||
|
let filename_lower = filename.to_lowercase();
|
||||||
|
if filename_lower.ends_with(".jpg") || filename_lower.ends_with(".jpeg") {
|
||||||
|
FileType::Jpeg
|
||||||
|
} else if filename_lower.ends_with(".png") {
|
||||||
|
FileType::Png
|
||||||
|
} else if filename_lower.ends_with(".webp") {
|
||||||
|
FileType::Webp
|
||||||
|
} else if filename_lower.ends_with(".gif") {
|
||||||
|
FileType::Gif
|
||||||
|
} else if filename_lower.ends_with(".pdf") {
|
||||||
|
FileType::Pdf
|
||||||
|
} else if filename_lower.ends_with(".doc") {
|
||||||
|
FileType::Doc
|
||||||
|
} else if filename_lower.ends_with(".docx") {
|
||||||
|
FileType::Docx
|
||||||
|
} else {
|
||||||
|
FileType::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UploadRequest {
|
||||||
|
pub user_id: String,
|
||||||
|
pub file_type: FileType,
|
||||||
|
pub filename: String,
|
||||||
|
pub content_type: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileMetadata {
|
||||||
|
pub filename: String,
|
||||||
|
pub content_type: String,
|
||||||
|
pub size: usize,
|
||||||
|
pub path: String,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fungsi Pembantu ---
|
||||||
|
|
||||||
|
/// Membuat instance MinioService dari struct MinioConfig.
|
||||||
|
pub async fn create_minio_service_from_config(config: MinioConfig) -> Result<MinioService> {
|
||||||
|
MinioService::new(
|
||||||
|
&config.endpoint, // Use raw endpoint, not endpoint_url()
|
||||||
|
&config.access_key,
|
||||||
|
&config.secret_key,
|
||||||
|
&config.bucket_name,
|
||||||
|
&config.region,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mendekode data file base64.
|
||||||
|
pub fn decode_base64_file(base64_data: &str) -> Result<Vec<u8>> {
|
||||||
|
let clean_data = if base64_data.contains(',') {
|
||||||
|
base64_data.split(',').nth(1).unwrap_or(base64_data)
|
||||||
|
} else {
|
||||||
|
base64_data
|
||||||
|
};
|
||||||
|
|
||||||
|
general_purpose::STANDARD
|
||||||
|
.decode(clean_data)
|
||||||
|
.map_err(|e| anyhow!("Gagal mendekode data base64: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mengekstrak tipe konten dari URL data.
|
||||||
|
pub fn extract_content_type_from_data_url(data_url: &str) -> Option<String> {
|
||||||
|
if data_url.starts_with("data:") {
|
||||||
|
if let Some(type_part) = data_url.split(';').next() {
|
||||||
|
return Some(type_part.replace("data:", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use super::Env;
|
use crate::enviroment::ENV;
|
||||||
use crate::SurrealMemClient;
|
use crate::SurrealMemClient;
|
||||||
use crate::enviroment::load_env;
|
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
use surrealdb::engine::local::Mem;
|
use surrealdb::engine::local::Mem;
|
||||||
use surrealdb::opt::auth::Root;
|
use surrealdb::opt::auth::Root;
|
||||||
@@ -10,8 +9,7 @@ pub mod resource;
|
|||||||
pub use resource::*;
|
pub use resource::*;
|
||||||
|
|
||||||
pub async fn surrealdb_init_ws() -> Result<Surreal<any::Any>> {
|
pub async fn surrealdb_init_ws() -> Result<Surreal<any::Any>> {
|
||||||
load_env();
|
let env = &ENV;
|
||||||
let env = Env::new();
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
|
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
@@ -26,7 +24,7 @@ pub async fn surrealdb_init_ws() -> Result<Surreal<any::Any>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn surrealdb_init_mem() -> Result<SurrealMemClient> {
|
pub async fn surrealdb_init_mem() -> Result<SurrealMemClient> {
|
||||||
let env = Env::new();
|
let env = &ENV;
|
||||||
let db = Surreal::new::<Mem>(()).await?;
|
let db = Surreal::new::<Mem>(()).await?;
|
||||||
db.use_ns(&env.surrealdb_namespace)
|
db.use_ns(&env.surrealdb_namespace)
|
||||||
.use_db(&env.surrealdb_dbname)
|
.use_db(&env.surrealdb_dbname)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ imphnen-libs.workspace = true
|
|||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
imphnen-entities.workspace = true
|
imphnen-entities.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
|
axum-extra.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
|
|||||||
@@ -2,43 +2,72 @@ use axum::{
|
|||||||
Extension, extract::Request, http::StatusCode, middleware::Next,
|
Extension, extract::Request, http::StatusCode, middleware::Next,
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use imphnen_iam::{UsersDetailQueryDto, UsersRepository};
|
use imphnen_libs::{AppState, jsonwebtoken::decode_access_token};
|
||||||
use imphnen_libs::AppState;
|
use imphnen_iam::v1::users::users_dto::UsersDetailQueryDto;
|
||||||
use imphnen_utils::{common_response, extract_email};
|
use imphnen_utils::common_response;
|
||||||
|
use axum_extra::headers::{authorization::Bearer, Authorization, HeaderMapExt};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
use imphnen_iam::v1::users::{users_service::{UsersService, UsersServiceTrait}};
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
|
use imphnen_utils::make_thing;
|
||||||
|
|
||||||
pub async fn auth_middleware(
|
pub async fn auth_middleware(
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
mut req: Request,
|
mut req: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, Infallible> {
|
) -> Result<Response, Infallible> {
|
||||||
let headers = req.headers();
|
let auth_header = match req
|
||||||
let email = match extract_email(headers) {
|
.headers()
|
||||||
Some(email) => email,
|
.typed_get::<Authorization<Bearer>>() {
|
||||||
|
Some(header) => header,
|
||||||
|
None => return Ok(common_response(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid or missing authorization token",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = auth_header.token();
|
||||||
|
|
||||||
|
let claims = match decode_access_token(token) {
|
||||||
|
Ok(token_data) => token_data.claims,
|
||||||
|
Err(_) => return Ok(common_response(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid or expired token",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.user_id.clone();
|
||||||
|
|
||||||
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &user_id);
|
||||||
|
|
||||||
|
// Try SurrealDB mem first
|
||||||
|
let mem_db = &state.surrealdb_mem;
|
||||||
|
let mut user_data: Option<UsersDetailQueryDto> = None;
|
||||||
|
if let Ok(opt_user) = mem_db.select(("users", &user_id)).await {
|
||||||
|
if let Some(user) = opt_user {
|
||||||
|
let user: imphnen_iam::v1::users::users_dto::UsersDetailQueryDto = user;
|
||||||
|
if !user.is_deleted && !user.role.is_deleted {
|
||||||
|
user_data = Some(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to main DB if not found in mem
|
||||||
|
let user_data = match user_data {
|
||||||
|
Some(user) => user,
|
||||||
None => {
|
None => {
|
||||||
return Ok(common_response(
|
let repo = UsersService {};
|
||||||
StatusCode::UNAUTHORIZED,
|
match repo.get_user_by_id_internal(&thing_id, &state).await {
|
||||||
"Invalid or expired token",
|
Ok(user) => {
|
||||||
));
|
// Optionally: insert into mem for future requests
|
||||||
|
let _: Result<Vec<imphnen_iam::v1::users::users_dto::UsersDetailQueryDto>, _> = mem_db.update(&thing_id.id.to_raw()).content(user.clone()).await;
|
||||||
|
user
|
||||||
|
},
|
||||||
|
Err(_) => return Ok(common_response(StatusCode::UNAUTHORIZED, "User not found")),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let repository = UsersRepository::new(&state);
|
|
||||||
let user: Option<UsersDetailQueryDto> =
|
req.extensions_mut().insert(user_data);
|
||||||
match repository.query_user_by_email(email).await {
|
|
||||||
Ok(user) => Some(user),
|
|
||||||
Err(err) => {
|
|
||||||
return Ok(common_response(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
&err.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if user.is_none() {
|
|
||||||
return Ok(common_response(
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
"Unauthorized user",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
req.extensions_mut().insert(user.unwrap());
|
|
||||||
Ok(next.run(req).await)
|
Ok(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
use axum::http::{HeaderValue, Method, header};
|
use axum::http::{HeaderValue, Method, header};
|
||||||
use imphnen_libs::Env;
|
use imphnen_libs::enviroment::ENV;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
pub fn cors_middleware() -> CorsLayer {
|
pub fn cors_middleware() -> CorsLayer {
|
||||||
let env = Env::new();
|
let env = &ENV;
|
||||||
let cors_origins = match env.rust_env.as_str() {
|
let cors_origins = match env.rust_env.as_str() {
|
||||||
"development" => vec!["http://localhost:3000"],
|
"development" => {
|
||||||
|
let mut origins = vec!["http://localhost:3000".to_string()];
|
||||||
|
origins.push(format!("http://localhost:{}", env.port));
|
||||||
|
origins
|
||||||
|
},
|
||||||
"production" => {
|
"production" => {
|
||||||
vec![
|
vec![
|
||||||
"https://gacha.imphnen.dev",
|
"https://gacha.imphnen.dev".to_string(),
|
||||||
"https://imphnen.dev",
|
"https://imphnen.dev".to_string(),
|
||||||
"https://dimentorin.imphnen.dev",
|
"https://dimentorin.imphnen.dev".to_string(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
_ => vec![
|
_ => vec![
|
||||||
"http://localhost:3000",
|
"http://localhost:3000".to_string(),
|
||||||
"https://gacha.imphnen.dev",
|
"https://gacha.imphnen.dev".to_string(),
|
||||||
"https://imphnen.dev",
|
"https://imphnen.dev".to_string(),
|
||||||
"https://dimentorin.imphnen.dev",
|
"https://dimentorin.imphnen.dev".to_string(),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
let allowed_origins: Vec<HeaderValue> = cors_origins
|
let allowed_origins: Vec<HeaderValue> = cors_origins
|
||||||
@@ -27,7 +31,7 @@ pub fn cors_middleware() -> CorsLayer {
|
|||||||
|
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_origin(allowed_origins)
|
.allow_origin(allowed_origins)
|
||||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
|
||||||
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
|
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
|
||||||
.allow_credentials(true)
|
.allow_credentials(true)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ use axum::{
|
|||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use imphnen_iam::{AuthRepository, PermissionsEnum};
|
use imphnen_iam::{AuthRepository, PermissionsEnum};
|
||||||
use imphnen_libs::AppState;
|
use imphnen_libs::AppState;
|
||||||
use imphnen_utils::{common_response, extract_email};
|
use imphnen_utils::{common_response, extract_email, extract_email_async};
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use tower::{Layer, Service};
|
use tower::{Layer, Service};
|
||||||
|
|
||||||
@@ -59,15 +59,24 @@ where
|
|||||||
let permissions = self.permissions.clone();
|
let permissions = self.permissions.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let headers = req.headers();
|
let headers = req.headers();
|
||||||
|
|
||||||
|
// Try synchronous email extraction first (for internal JWT tokens)
|
||||||
let email = match extract_email(headers) {
|
let email = match extract_email(headers) {
|
||||||
Some(email) => email,
|
Some(email) => email,
|
||||||
None => {
|
None => {
|
||||||
return Ok(common_response(
|
// If sync extraction fails, try async (for Google tokens)
|
||||||
StatusCode::UNAUTHORIZED,
|
match extract_email_async(headers).await {
|
||||||
"Invalid or missing authorization token",
|
Some(email) => email,
|
||||||
));
|
None => {
|
||||||
|
return Ok(common_response(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid or missing authorization token",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth_repo = AuthRepository::new(&app_state);
|
let auth_repo = AuthRepository::new(&app_state);
|
||||||
let user = match auth_repo.query_get_stored_user(email).await {
|
let user = match auth_repo.query_get_stored_user(email).await {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
|
|||||||
@@ -19,3 +19,8 @@ strum.workspace = true
|
|||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
dotenvy = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
use tracing::{info};
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
use surrealdb::method::Query;
|
use surrealdb::method::Query;
|
||||||
|
|
||||||
|
/// Binds a filter value to the query under the key "filter".
|
||||||
pub fn bind_filter_value(
|
pub fn bind_filter_value(
|
||||||
query: Query<'_, any::Any>,
|
query: Query<'_, any::Any>,
|
||||||
val: String,
|
val: String,
|
||||||
) -> Query<'_, any::Any> {
|
) -> Query<'_, any::Any> {
|
||||||
query.bind(("filter", val))
|
info!(?val, "bind_filter_value called with arguments");
|
||||||
|
let result = query.bind(("filter", val.clone()));
|
||||||
|
info!(?val, "bind_filter_value returning query with bound filter");
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
use imphnen_entities::error_dto::error::Error;
|
||||||
|
use tracing::{info, error}; // Added this line
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct CsrfPayload {
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub random: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct OAuthCsrfPayload {
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub random: String,
|
||||||
|
pub pkce_verifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a signed CSRF token that can be validated without server-side storage
|
||||||
|
pub fn generate_csrf_token(secret: &str) -> Result<String, Error> {
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| Error::Auth("Failed to get timestamp".to_string()))?
|
||||||
|
.as_secs();
|
||||||
|
info!("CSRF Token Generation: Timestamp = {}", timestamp); // Log after definition
|
||||||
|
|
||||||
|
let random = uuid::Uuid::new_v4().to_string();
|
||||||
|
info!("CSRF Token Generation: Random string generated."); // Log after definition
|
||||||
|
|
||||||
|
let payload = CsrfPayload {
|
||||||
|
timestamp,
|
||||||
|
random,
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload_json = serde_json::to_string(&payload)
|
||||||
|
.map_err(|e| { // Changed to capture error
|
||||||
|
error!("CSRF Token Generation: Failed to serialize CSRF payload: {:?}", e);
|
||||||
|
Error::Auth("Failed to serialize CSRF payload".to_string())
|
||||||
|
})?;
|
||||||
|
info!("CSRF Token Generation: Payload JSON = {}", payload_json); // Log after definition
|
||||||
|
|
||||||
|
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
|
||||||
|
info!("CSRF Token Generation: Payload Base64 = {}", payload_b64); // Log after definition
|
||||||
|
|
||||||
|
// Create signature
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(payload_b64.as_bytes());
|
||||||
|
hasher.update(secret.as_bytes());
|
||||||
|
let signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
|
||||||
|
info!("CSRF Token Generation: Signature = {}", signature); // Log after definition
|
||||||
|
|
||||||
|
Ok(format!("{}.{}", payload_b64, signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a signed OAuth CSRF token with PKCE verifier
|
||||||
|
pub fn generate_oauth_csrf_token(secret: &str, pkce_verifier: &str) -> Result<String, Error> {
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| Error::Auth("Failed to get timestamp".to_string()))?
|
||||||
|
.as_secs();
|
||||||
|
info!("OAuth CSRF Token Generation: Timestamp = {}", timestamp); // Log after definition
|
||||||
|
|
||||||
|
let random = uuid::Uuid::new_v4().to_string();
|
||||||
|
info!("OAuth CSRF Token Generation: Random string generated."); // Log after definition
|
||||||
|
|
||||||
|
let payload = OAuthCsrfPayload {
|
||||||
|
timestamp,
|
||||||
|
random,
|
||||||
|
pkce_verifier: pkce_verifier.to_string(),
|
||||||
|
};
|
||||||
|
info!("OAuth CSRF Token Generation: PKCE Verifier = {}", pkce_verifier); // Log after use in payload
|
||||||
|
|
||||||
|
let payload_json = serde_json::to_string(&payload)
|
||||||
|
.map_err(|e| { // Changed to capture error
|
||||||
|
error!("OAuth CSRF Token Generation: Failed to serialize payload: {:?}", e);
|
||||||
|
Error::Auth("Failed to serialize OAuth CSRF payload".to_string())
|
||||||
|
})?;
|
||||||
|
info!("OAuth CSRF Token Generation: Payload JSON = {}", payload_json); // Log after definition
|
||||||
|
|
||||||
|
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
|
||||||
|
info!("OAuth CSRF Token Generation: Payload Base64 = {}", payload_b64); // Log after definition
|
||||||
|
|
||||||
|
// Create signature
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(payload_b64.as_bytes());
|
||||||
|
hasher.update(secret.as_bytes());
|
||||||
|
let signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
|
||||||
|
info!("OAuth CSRF Token Generation: Signature = {}", signature); // Log after definition
|
||||||
|
|
||||||
|
Ok(format!("{}.{}", payload_b64, signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a CSRF token
|
||||||
|
pub fn validate_csrf_token(token: &str, secret: &str, max_age_seconds: u64) -> Result<(), Error> {
|
||||||
|
let parts: Vec<&str> = token.split('.').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(Error::Auth("Invalid CSRF token format".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_b64 = parts[0];
|
||||||
|
let provided_signature = parts[1];
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(payload_b64.as_bytes());
|
||||||
|
hasher.update(secret.as_bytes());
|
||||||
|
let expected_signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
|
||||||
|
|
||||||
|
if provided_signature != expected_signature {
|
||||||
|
return Err(Error::Auth("Invalid CSRF token signature".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and validate payload
|
||||||
|
let payload_json = URL_SAFE_NO_PAD.decode(payload_b64)
|
||||||
|
.map_err(|_| Error::Auth("Failed to decode CSRF token".to_string()))?;
|
||||||
|
|
||||||
|
let payload_str = String::from_utf8(payload_json)
|
||||||
|
.map_err(|_| Error::Auth("Invalid CSRF token encoding".to_string()))?;
|
||||||
|
|
||||||
|
let payload: CsrfPayload = serde_json::from_str(&payload_str)
|
||||||
|
.map_err(|_| Error::Auth("Failed to parse CSRF token".to_string()))?;
|
||||||
|
|
||||||
|
// Check timestamp
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| Error::Auth("Failed to get current timestamp".to_string()))?
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
if now > payload.timestamp + max_age_seconds {
|
||||||
|
return Err(Error::Auth("CSRF token has expired".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.timestamp > now + 60 { // Allow 1 minute clock skew
|
||||||
|
return Err(Error::Auth("CSRF token timestamp is in the future".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate OAuth CSRF token and extract PKCE verifier
|
||||||
|
pub fn validate_oauth_csrf_token(token: &str, secret: &str, max_age_seconds: u64) -> Result<String, Error> {
|
||||||
|
let parts: Vec<&str> = token.split('.').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(Error::Auth("Invalid OAuth CSRF token format".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_b64 = parts[0];
|
||||||
|
let provided_signature = parts[1];
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(payload_b64.as_bytes());
|
||||||
|
hasher.update(secret.as_bytes());
|
||||||
|
let expected_signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
|
||||||
|
|
||||||
|
if provided_signature != expected_signature {
|
||||||
|
return Err(Error::Auth("Invalid OAuth CSRF token signature".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and validate payload
|
||||||
|
let payload_json = URL_SAFE_NO_PAD.decode(payload_b64)
|
||||||
|
.map_err(|_| Error::Auth("Failed to decode OAuth CSRF token".to_string()))?;
|
||||||
|
|
||||||
|
let payload_str = String::from_utf8(payload_json)
|
||||||
|
.map_err(|_| Error::Auth("Invalid OAuth CSRF token encoding".to_string()))?;
|
||||||
|
|
||||||
|
let payload: OAuthCsrfPayload = serde_json::from_str(&payload_str)
|
||||||
|
.map_err(|_| Error::Auth("Failed to parse OAuth CSRF token".to_string()))?;
|
||||||
|
|
||||||
|
// Check timestamp
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| Error::Auth("Failed to get current timestamp".to_string()))?
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
if now > payload.timestamp + max_age_seconds {
|
||||||
|
return Err(Error::Auth("OAuth CSRF token has expired".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.timestamp > now + 60 { // Allow 1 minute clock skew
|
||||||
|
return Err(Error::Auth("OAuth CSRF token timestamp is in the future".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(payload.pkce_verifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_csrf_token_generation_and_validation() {
|
||||||
|
let secret = "test_secret";
|
||||||
|
|
||||||
|
// Generate token
|
||||||
|
let token = generate_csrf_token(secret).unwrap();
|
||||||
|
|
||||||
|
// Validate token (should pass)
|
||||||
|
assert!(validate_csrf_token(&token, secret, 300).is_ok());
|
||||||
|
|
||||||
|
// Validate with wrong secret (should fail)
|
||||||
|
assert!(validate_csrf_token(&token, "wrong_secret", 300).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_csrf_token_expiration() {
|
||||||
|
let secret = "test_secret";
|
||||||
|
let token = generate_csrf_token(secret).unwrap();
|
||||||
|
|
||||||
|
// Add a 2 second delay to ensure the token expires when max_age is 1 second
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
|
|
||||||
|
// Should fail with 1 second max age (token is now 2 seconds old)
|
||||||
|
assert!(validate_csrf_token(&token, secret, 1).is_err());
|
||||||
|
|
||||||
|
// Should still work with a large max age
|
||||||
|
assert!(validate_csrf_token(&token, secret, 300).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_csrf_token_format() {
|
||||||
|
let secret = "test_secret";
|
||||||
|
|
||||||
|
// Invalid format (no dot)
|
||||||
|
assert!(validate_csrf_token("invalid_token", secret, 300).is_err());
|
||||||
|
|
||||||
|
// Invalid format (too many dots)
|
||||||
|
assert!(validate_csrf_token("a.b.c", secret, 300).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,170 @@
|
|||||||
|
use tracing::{info, error};
|
||||||
use crate::decode_access_token;
|
use crate::decode_access_token;
|
||||||
use axum::http::{HeaderMap, header::AUTHORIZATION};
|
use axum::http::{HeaderMap, header::AUTHORIZATION};
|
||||||
|
|
||||||
|
/// Extracts the email from the Authorization header, if present and valid.
|
||||||
|
/// Supports both our internal JWT tokens and Google access tokens.
|
||||||
pub fn extract_email(headers: &HeaderMap) -> Option<String> {
|
pub fn extract_email(headers: &HeaderMap) -> Option<String> {
|
||||||
let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?;
|
info!(?headers, "extract_email called with headers");
|
||||||
let token = auth_header.strip_prefix("Bearer ")?;
|
let auth_header = match headers.get(AUTHORIZATION) {
|
||||||
|
Some(h) => h,
|
||||||
match decode_access_token(token) {
|
None => {
|
||||||
Ok(data) => Some(data.claims.sub),
|
error!("Authorization header missing in extract_email");
|
||||||
Err(_e) => None,
|
return None;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
let auth_str = match auth_header.to_str() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = ?e, "Failed to convert Authorization header to str in extract_email");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let token = match auth_str.strip_prefix("Bearer ") {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
error!(auth_str, "Authorization header does not start with 'Bearer ' in extract_email");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!(token, "Extracted bearer token in extract_email");
|
||||||
|
|
||||||
|
// First try to decode as our internal JWT token
|
||||||
|
match decode_access_token(token) {
|
||||||
|
Ok(data) => {
|
||||||
|
info!(email = %data.claims.sub, "Successfully decoded internal access token in extract_email");
|
||||||
|
Some(data.claims.sub)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!("Failed to decode as internal JWT, checking if it's a Google token");
|
||||||
|
// If it fails, it might be a Google access token
|
||||||
|
// For Google tokens, we need async validation, so we'll return None here
|
||||||
|
// and handle Google tokens separately in the calling code
|
||||||
|
error!("Token is not a valid internal JWT. If this is a Google token, please use extract_email_async or handle Google OAuth flow properly.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Async version that can handle Google access tokens
|
||||||
|
pub async fn extract_email_async(headers: &HeaderMap) -> Option<String> {
|
||||||
|
info!(?headers, "extract_email_async called with headers");
|
||||||
|
let auth_header = match headers.get(AUTHORIZATION) {
|
||||||
|
Some(h) => h,
|
||||||
|
None => {
|
||||||
|
error!("Authorization header missing in extract_email_async");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let auth_str = match auth_header.to_str() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = ?e, "Failed to convert Authorization header to str in extract_email_async");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let token = match auth_str.strip_prefix("Bearer ") {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
error!(auth_str, "Authorization header does not start with 'Bearer ' in extract_email_async");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!(token, "Extracted bearer token in extract_email_async");
|
||||||
|
|
||||||
|
// First try to decode as our internal JWT token
|
||||||
|
match decode_access_token(token) {
|
||||||
|
Ok(data) => {
|
||||||
|
info!(email = %data.claims.sub, "Successfully decoded internal access token in extract_email_async");
|
||||||
|
Some(data.claims.sub)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!("Failed to decode as internal JWT, trying Google token validation");
|
||||||
|
// If it fails, try to validate as Google access token
|
||||||
|
extract_email_from_google_token(token).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts email from Google access token by calling Google's tokeninfo endpoint
|
||||||
|
async fn extract_email_from_google_token(token: &str) -> Option<String> {
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let tokeninfo_url = format!("https://oauth2.googleapis.com/tokeninfo?access_token={}", token);
|
||||||
|
|
||||||
|
match client.get(&tokeninfo_url).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
match response.json::<Value>().await {
|
||||||
|
Ok(token_info) => {
|
||||||
|
if let Some(email) = token_info.get("email").and_then(|e| e.as_str()) {
|
||||||
|
info!(email = %email, "Successfully extracted email from Google token");
|
||||||
|
Some(email.to_string())
|
||||||
|
} else {
|
||||||
|
error!("Email not found in Google token info response");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = ?e, "Failed to parse Google token info response");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!(status = %response.status(), "Google token validation failed");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = ?e, "Failed to validate Google token");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the email from a JWT token string.
|
||||||
|
/// Supports both our internal JWT tokens and Google access tokens.
|
||||||
pub fn extract_email_token(token: String) -> Option<String> {
|
pub fn extract_email_token(token: String) -> Option<String> {
|
||||||
let token_data = decode_access_token(&token).ok()?;
|
info!(token = %token, "extract_email_token called with token");
|
||||||
Some(token_data.claims.sub)
|
match decode_access_token(&token) {
|
||||||
|
Ok(data) => {
|
||||||
|
info!(email = %data.claims.sub, "Successfully decoded token in extract_email_token");
|
||||||
|
Some(data.claims.sub)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!("Failed to decode as internal JWT in extract_email_token, checking if it's a Google token");
|
||||||
|
// If it fails, it might be a Google access token
|
||||||
|
// For Google tokens, we need async validation, so we'll return None here
|
||||||
|
// and handle Google tokens separately in the calling code
|
||||||
|
error!("Token is not a valid internal JWT. If this is a Google token, please use extract_email_token_async or handle Google OAuth flow properly.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A simple helper to check if a token string looks like a JWT.
|
||||||
|
fn is_jwt(token: &str) -> bool {
|
||||||
|
let parts: Vec<_> = token.split('.').collect();
|
||||||
|
parts.len() == 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Async version of extract_email_token that can handle Google access tokens
|
||||||
|
pub async fn extract_email_token_async(token: String) -> Option<String> {
|
||||||
|
info!(token = %token, "extract_email_token_async called with token");
|
||||||
|
|
||||||
|
if is_jwt(&token) {
|
||||||
|
match decode_access_token(&token) {
|
||||||
|
Ok(data) => {
|
||||||
|
info!(email = %data.claims.sub, "Successfully decoded internal token in extract_email_token_async");
|
||||||
|
return Some(data.claims.sub);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!("Failed to decode as internal JWT in extract_email_token_async, trying Google token validation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a valid internal JWT, try to validate as Google access token
|
||||||
|
extract_email_from_google_token(&token).await
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
use tracing::{info};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
/// Returns the current UTC date/time as an RFC3339 string.
|
||||||
pub fn get_iso_date() -> String {
|
pub fn get_iso_date() -> String {
|
||||||
let now: DateTime<Utc> = Utc::now();
|
info!("get_iso_date called");
|
||||||
now.to_rfc3339()
|
let now: DateTime<Utc> = Utc::now();
|
||||||
|
let date_str = now.to_rfc3339();
|
||||||
|
info!(date_str = %date_str, "get_iso_date returning RFC3339 date string");
|
||||||
|
date_str
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
|
use tracing::{info, error};
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
/// Extracts the table and id from a Thing, returning (&str, &str).
|
||||||
pub fn get_id(thing: &Thing) -> Result<(&str, &str)> {
|
pub fn get_id(thing: &Thing) -> Result<(&str, &str)> {
|
||||||
let table = thing.tb.as_str();
|
info!(?thing, "get_id called with argument");
|
||||||
let id = match &thing.id {
|
let table = thing.tb.as_str();
|
||||||
surrealdb::sql::Id::String(s) => s.as_str(),
|
let id = match &thing.id {
|
||||||
_ => bail!("Unsupported ID type"),
|
surrealdb::sql::Id::String(s) => {
|
||||||
};
|
info!(id = %s, "ID extracted as string in get_id");
|
||||||
Ok((table, id))
|
s.as_str()
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
error!(?other, "Unsupported ID type in get_id");
|
||||||
|
bail!("Unsupported ID type");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!(table = %table, id = %id, "get_id returning table and id");
|
||||||
|
Ok((table, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts the raw id string from a Thing.
|
||||||
pub fn extract_id(thing: &Thing) -> String {
|
pub fn extract_id(thing: &Thing) -> String {
|
||||||
thing.id.to_raw()
|
info!(?thing, "extract_id called with argument");
|
||||||
|
let raw_id = thing.id.to_raw();
|
||||||
|
info!(raw_id = %raw_id, "extract_id returning raw id string");
|
||||||
|
raw_id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod logger;
|
||||||
pub mod bind_filter;
|
pub mod bind_filter;
|
||||||
pub mod extract_email;
|
pub mod extract_email;
|
||||||
pub mod generate_date;
|
pub mod generate_date;
|
||||||
@@ -9,9 +10,11 @@ pub mod query_list;
|
|||||||
pub mod response_format;
|
pub mod response_format;
|
||||||
pub mod serde_helpers;
|
pub mod serde_helpers;
|
||||||
pub mod validator;
|
pub mod validator;
|
||||||
|
pub mod csrf_token;
|
||||||
|
|
||||||
|
pub use logger::init_logger;
|
||||||
pub use bind_filter::*;
|
pub use bind_filter::*;
|
||||||
pub use extract_email::*;
|
pub use extract_email::{extract_email, extract_email_async, extract_email_token, extract_email_token_async};
|
||||||
pub use generate_date::*;
|
pub use generate_date::*;
|
||||||
pub use generate_otp::*;
|
pub use generate_otp::*;
|
||||||
pub use get_id::*;
|
pub use get_id::*;
|
||||||
@@ -26,3 +29,4 @@ pub use serde_helpers::{
|
|||||||
string_or_empty_string, thing_or_string,
|
string_or_empty_string, thing_or_string,
|
||||||
};
|
};
|
||||||
pub use validator::*;
|
pub use validator::*;
|
||||||
|
pub use csrf_token::*;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use dotenvy::dotenv;
|
||||||
|
use tracing_subscriber::{EnvFilter, fmt};
|
||||||
|
|
||||||
|
/// Initializes the logger using tracing and tracing-subscriber.
|
||||||
|
/// Loads environment variables from `.env` and sets log level from `RUST_LOG`.
|
||||||
|
pub fn init_logger() {
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
|
||||||
|
// Set up the tracing subscriber with EnvFilter from RUST_LOG
|
||||||
|
let filter = EnvFilter::try_from_default_env()
|
||||||
|
.or_else(|_| EnvFilter::try_new("warn"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fmt()
|
||||||
|
.with_env_filter(filter)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
@@ -43,3 +43,14 @@ pub fn common_response(status: StatusCode, message: &str) -> Response {
|
|||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn success_created_response<T: Serialize>(params: ResponseSuccessDto<T>) -> Response {
|
||||||
|
(
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(json!({
|
||||||
|
"data": params.data,
|
||||||
|
"version": "0.1.0",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ SKIP_COMPREHENSIVE=false
|
|||||||
SKIP_CRUD=false
|
SKIP_CRUD=false
|
||||||
GENERATE_REPORT=false
|
GENERATE_REPORT=false
|
||||||
VERBOSE=false
|
VERBOSE=false
|
||||||
|
SKIP_CLEAR=false
|
||||||
|
SKIP_SEED=false
|
||||||
|
|
||||||
while getopts "sbcrgvh" opt; do
|
while getopts "sbcrgvhkd" opt; do
|
||||||
case ${opt} in
|
case ${opt} in
|
||||||
s ) START_SERVER=true ;;
|
s ) START_SERVER=true ;;
|
||||||
b ) SKIP_BASIC=true ;;
|
b ) SKIP_BASIC=true ;;
|
||||||
@@ -30,7 +32,7 @@ while getopts "sbcrgvh" opt; do
|
|||||||
r ) SKIP_CRUD=true ;;
|
r ) SKIP_CRUD=true ;;
|
||||||
g ) GENERATE_REPORT=true ;;
|
g ) GENERATE_REPORT=true ;;
|
||||||
v ) VERBOSE=true ;;
|
v ) VERBOSE=true ;;
|
||||||
h )
|
h )
|
||||||
echo "IMPHNEN API Test Suite"
|
echo "IMPHNEN API Test Suite"
|
||||||
echo "Usage: $0 [OPTIONS]"
|
echo "Usage: $0 [OPTIONS]"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -42,6 +44,8 @@ while getopts "sbcrgvh" opt; do
|
|||||||
echo " -g Generate JSON test report"
|
echo " -g Generate JSON test report"
|
||||||
echo " -v Verbose output (show all INFO logs)"
|
echo " -v Verbose output (show all INFO logs)"
|
||||||
echo " -h Show this help message"
|
echo " -h Show this help message"
|
||||||
|
echo " -d Skip database clear"
|
||||||
|
echo " -k Skip database seeding"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 # Run all tests"
|
echo " $0 # Run all tests"
|
||||||
@@ -49,11 +53,18 @@ while getopts "sbcrgvh" opt; do
|
|||||||
echo " $0 -g # Run tests and generate report"
|
echo " $0 -g # Run tests and generate report"
|
||||||
echo " $0 -sv # Start server with verbose output"
|
echo " $0 -sv # Start server with verbose output"
|
||||||
echo " $0 -bc # Run only public endpoint tests"
|
echo " $0 -bc # Run only public endpoint tests"
|
||||||
|
echo " $0 -d # Skip database clear"
|
||||||
|
echo " $0 -k # Skip database seeding"
|
||||||
|
echo " $0 -dk # Skip database clear and seeding"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
d ) SKIP_CLEAR=true ;;
|
||||||
|
k ) SKIP_SEED=true ;;
|
||||||
\? ) echo "Invalid option: -$OPTARG" >&2; echo "Use -h for help" >&2; exit 1 ;;
|
\? ) echo "Invalid option: -$OPTARG" >&2; echo "Use -h for help" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
# Shift past the options
|
||||||
|
shift "$((OPTIND-1))"
|
||||||
|
|
||||||
if ! command -v curl &> /dev/null; then
|
if ! command -v curl &> /dev/null; then
|
||||||
echo "Error: 'curl' tidak ditemukan. Mohon install terlebih dahulu." >&2
|
echo "Error: 'curl' tidak ditemukan. Mohon install terlebih dahulu." >&2
|
||||||
@@ -72,6 +83,7 @@ FALED_TESTS_SUMMARY=()
|
|||||||
PASS_COUNT=0
|
PASS_COUNT=0
|
||||||
FAIL_COUNT=0
|
FAIL_COUNT=0
|
||||||
TEST_TESTIMONIAL_ID=""
|
TEST_TESTIMONIAL_ID=""
|
||||||
|
TEST_EVENT_ID=""
|
||||||
|
|
||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
YELLOW='\033[0;33m'
|
YELLOW='\033[0;33m'
|
||||||
@@ -162,8 +174,12 @@ test_server_connection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear_database() {
|
clear_database() {
|
||||||
|
if [ "$SKIP_CLEAR" = true ]; then
|
||||||
|
write_test_log "INFO" "Melewatkan pembersihan database."
|
||||||
|
return
|
||||||
|
fi
|
||||||
write_test_log "INFO" "Membersihkan database via WebSocket..."
|
write_test_log "INFO" "Membersihkan database via WebSocket..."
|
||||||
if ! cargo run --bin clear_db_test --release; then
|
if ! RUST_LOG=debug cargo run --bin clear_db_test --release; then
|
||||||
write_test_log "ERROR" "Gagal membersihkan database."
|
write_test_log "ERROR" "Gagal membersihkan database."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -433,7 +449,7 @@ test_comprehensive_with_user() {
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
"mentor@example.com")
|
"mentor@example.com")
|
||||||
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true
|
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true
|
||||||
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
|
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
|
||||||
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
|
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
|
||||||
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
||||||
@@ -448,7 +464,7 @@ test_comprehensive_with_user() {
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
"user@example.com")
|
"user@example.com")
|
||||||
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true
|
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true
|
||||||
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
|
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
|
||||||
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
|
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
|
||||||
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
||||||
@@ -471,7 +487,7 @@ test_comprehensive_with_user() {
|
|||||||
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true
|
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 403 "" true
|
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -522,7 +538,7 @@ test_authentication_endpoints() {
|
|||||||
|
|
||||||
local forgot_password_data
|
local forgot_password_data
|
||||||
forgot_password_data=$(jq -n --arg email "$TEST_EMAIL" '{email: $email}')
|
forgot_password_data=$(jq -n --arg email "$TEST_EMAIL" '{email: $email}')
|
||||||
test_api_endpoint "Forgot Password Test" "POST" "/v1/auth/forgot" 400 "$forgot_password_data"
|
test_api_endpoint "Forgot Password Test" "POST" "/v1/auth/forgot" 200 "$forgot_password_data"
|
||||||
|
|
||||||
local new_password_data
|
local new_password_data
|
||||||
new_password_data=$(jq -n --arg token "some_reset_token" --arg pass "newpassword123!A" '{token: $token, password: $pass}')
|
new_password_data=$(jq -n --arg token "some_reset_token" --arg pass "newpassword123!A" '{token: $token, password: $pass}')
|
||||||
@@ -602,9 +618,9 @@ test_crud_operations() {
|
|||||||
|
|
||||||
local testimonial_data
|
local testimonial_data
|
||||||
testimonial_data=$(jq -n --arg content "Test testimonial via Bash $(date +%s)" '{role: "Student", content: $content}')
|
testimonial_data=$(jq -n --arg content "Test testimonial via Bash $(date +%s)" '{role: "Student", content: $content}')
|
||||||
local testimonial_response=$(test_api_endpoint "Create Testimonial" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true)
|
local testimonial_response
|
||||||
|
testimonial_response=$(test_api_endpoint "Create Testimonial" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true)
|
||||||
TEST_TESTIMONIAL_ID=$(echo "$testimonial_response" | jq -r '.data.id // empty')
|
TEST_TESTIMONIAL_ID=$(echo "$testimonial_response" | jq -r '.data.id // empty')
|
||||||
write_test_log "INFO" "TEST_TESTIMONIAL_ID: $TEST_TESTIMONIAL_ID"
|
|
||||||
write_test_log "INFO" "Captured Testimonial ID: $TEST_TESTIMONIAL_ID"
|
write_test_log "INFO" "Captured Testimonial ID: $TEST_TESTIMONIAL_ID"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
|
|
||||||
@@ -627,7 +643,10 @@ test_crud_operations() {
|
|||||||
end_date: "2025-12-01T16:00:00Z",
|
end_date: "2025-12-01T16:00:00Z",
|
||||||
location: null
|
location: null
|
||||||
}')
|
}')
|
||||||
test_api_endpoint "Create Event" "POST" "/v1/cms/landing/events/create" 201 "$event_data" true
|
local event_response
|
||||||
|
event_response=$(test_api_endpoint "Create Event" "POST" "/v1/cms/landing/events/create" 201 "$event_data" true)
|
||||||
|
TEST_EVENT_ID=$(echo "$event_response" | jq -r '.data.id // empty')
|
||||||
|
write_test_log "INFO" "Captured Event ID: $TEST_EVENT_ID"
|
||||||
}
|
}
|
||||||
|
|
||||||
test_roles_and_permissions() {
|
test_roles_and_permissions() {
|
||||||
@@ -687,8 +706,12 @@ test_events_endpoints() {
|
|||||||
test_api_endpoint "Get Events with Pagination" "GET" "/v1/cms/landing/events?page=1&per_page=5" 200
|
test_api_endpoint "Get Events with Pagination" "GET" "/v1/cms/landing/events?page=1&per_page=5" 200
|
||||||
test_api_endpoint "Get Events with Search" "GET" "/v1/cms/landing/events?search=tech" 200
|
test_api_endpoint "Get Events with Search" "GET" "/v1/cms/landing/events?search=tech" 200
|
||||||
|
|
||||||
local test_event_id="e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o"
|
# Ensure TEST_EVENT_ID is not empty before testing
|
||||||
test_api_endpoint "Get Event By ID" "GET" "/v1/cms/landing/events/detail/$test_event_id" 200
|
if [ -n "$TEST_EVENT_ID" ]; then
|
||||||
|
test_api_endpoint "Get Event By ID" "GET" "/v1/cms/landing/events/detail/$TEST_EVENT_ID" 200
|
||||||
|
else
|
||||||
|
write_test_log "WARN" "✗ Get Event By ID - Dilewati: TEST_EVENT_ID tidak tersedia"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
test_testimonials_endpoints() {
|
test_testimonials_endpoints() {
|
||||||
@@ -776,7 +799,7 @@ if [ "$START_SERVER" = true ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf "${YELLOW}Memulai server backend...${NC}\n"
|
printf "${YELLOW}Memulai server backend...${NC}\n"
|
||||||
cargo run --bin api &
|
RUST_LOG=debug cargo run --bin api &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
printf "${YELLOW}Menunggu server siap...${NC}\n"
|
printf "${YELLOW}Menunggu server siap...${NC}\n"
|
||||||
@@ -803,11 +826,15 @@ fi
|
|||||||
clear_database
|
clear_database
|
||||||
|
|
||||||
printf "\n${CYAN}=== Menjalankan Seeders ===${NC}\n"
|
printf "\n${CYAN}=== Menjalankan Seeders ===${NC}\n"
|
||||||
if ! cargo run --bin seeder; then
|
if [ "$SKIP_SEED" = true ]; then
|
||||||
write_test_log "ERROR" "Gagal menjalankan seeder roles permissions."
|
write_test_log "INFO" "Melewatkan seeding database."
|
||||||
exit 1
|
else
|
||||||
|
if ! RUST_LOG=debug cargo run --bin seeder; then
|
||||||
|
write_test_log "ERROR" "Gagal menjalankan seeder roles permissions."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
write_test_log "SUCCESS" "Seeders selesai."
|
||||||
fi
|
fi
|
||||||
write_test_log "SUCCESS" "Seeders selesai."
|
|
||||||
|
|
||||||
|
|
||||||
printf "\n${CYAN}=== Menampilkan User yang Tersedia ===${NC}\n"
|
printf "\n${CYAN}=== Menampilkan User yang Tersedia ===${NC}\n"
|
||||||
|
|||||||
+1
-1
@@ -24,6 +24,6 @@ serde.workspace = true
|
|||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
async-trait.workspace = true
|
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
@@ -5,7 +5,7 @@ mod auth_login_tests {
|
|||||||
use crate::mock_test::setup_all_test_environment;
|
use crate::mock_test::setup_all_test_environment;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use imphnen_iam::{
|
use imphnen_iam::{
|
||||||
v1::auth::{AuthLoginRequestDto, AuthService},
|
v1::auth::{AuthLoginRequestDto, AuthService, AuthServiceTrait}, // Import AuthServiceTrait
|
||||||
AppState, UsersRepository, UsersSchema,
|
AppState, UsersRepository, UsersSchema,
|
||||||
};
|
};
|
||||||
use serde_json::Value; // Import the new setup function
|
use serde_json::Value; // Import the new setup function
|
||||||
@@ -47,13 +47,29 @@ mod auth_login_tests {
|
|||||||
id: crate::make_thing("app_users", &uuid::Uuid::new_v4().to_string()),
|
id: crate::make_thing("app_users", &uuid::Uuid::new_v4().to_string()),
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
fullname: "Test User".to_string(),
|
fullname: "Test User".to_string(),
|
||||||
|
legal_name: None,
|
||||||
password: hash_password(password).unwrap(),
|
password: hash_password(password).unwrap(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
phone_number: "081234567890".to_string(),
|
phone_number: "081234567890".to_string(),
|
||||||
|
phone_for_verification: None,
|
||||||
is_active,
|
is_active,
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
role: crate::make_thing("app_roles", &role.id),
|
role: crate::make_thing("app_roles", &role.id),
|
||||||
mentor_id: None,
|
mentor_id: None,
|
||||||
created_at: imphnen_utils::get_iso_date(),
|
created_at: imphnen_utils::get_iso_date(),
|
||||||
@@ -72,7 +88,7 @@ mod auth_login_tests {
|
|||||||
async fn test_successful_login_with_valid_credentials() {
|
async fn test_successful_login_with_valid_credentials() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_login_success");
|
let email = generate_unique_email("test_login_success");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||||
|
|
||||||
@@ -81,7 +97,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::OK);
|
assert_eq!(parts.status, StatusCode::OK);
|
||||||
@@ -105,10 +121,10 @@ mod auth_login_tests {
|
|||||||
|
|
||||||
let login_dto = AuthLoginRequestDto {
|
let login_dto = AuthLoginRequestDto {
|
||||||
email: "invalid-email".to_string(),
|
email: "invalid-email".to_string(),
|
||||||
password: "TestPass123!".to_string(),
|
password: "password".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||||
@@ -124,10 +140,10 @@ mod auth_login_tests {
|
|||||||
|
|
||||||
let login_dto = AuthLoginRequestDto {
|
let login_dto = AuthLoginRequestDto {
|
||||||
email: "".to_string(),
|
email: "".to_string(),
|
||||||
password: "TestPass123!".to_string(),
|
password: "password".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||||
@@ -147,7 +163,7 @@ mod auth_login_tests {
|
|||||||
password: "".to_string(),
|
password: "".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||||
@@ -160,7 +176,7 @@ mod auth_login_tests {
|
|||||||
async fn test_login_with_wrong_password() {
|
async fn test_login_with_wrong_password() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_wrong_pass");
|
let email = generate_unique_email("test_wrong_pass");
|
||||||
let correct_password = "TestPass123!";
|
let correct_password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, correct_password, "User", true).await;
|
create_test_user_with_role(&state, &email, correct_password, "User", true).await;
|
||||||
|
|
||||||
@@ -169,7 +185,7 @@ mod auth_login_tests {
|
|||||||
password: "WrongPassword123!".to_string(),
|
password: "WrongPassword123!".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||||
@@ -186,10 +202,10 @@ mod auth_login_tests {
|
|||||||
|
|
||||||
let login_dto = AuthLoginRequestDto {
|
let login_dto = AuthLoginRequestDto {
|
||||||
email: generate_unique_email("nonexistent"),
|
email: generate_unique_email("nonexistent"),
|
||||||
password: "TestPass123!".to_string(),
|
password: "password".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::UNAUTHORIZED);
|
assert_eq!(parts.status, StatusCode::UNAUTHORIZED);
|
||||||
@@ -206,7 +222,7 @@ mod auth_login_tests {
|
|||||||
async fn test_login_with_inactive_user() {
|
async fn test_login_with_inactive_user() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_inactive");
|
let email = generate_unique_email("test_inactive");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "User", false).await;
|
create_test_user_with_role(&state, &email, password, "User", false).await;
|
||||||
|
|
||||||
@@ -215,7 +231,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||||
@@ -233,7 +249,7 @@ mod auth_login_tests {
|
|||||||
async fn test_successful_mentor_login() {
|
async fn test_successful_mentor_login() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_mentor_login");
|
let email = generate_unique_email("test_mentor_login");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "Mentor", true).await;
|
create_test_user_with_role(&state, &email, password, "Mentor", true).await;
|
||||||
|
|
||||||
@@ -242,7 +258,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_mentor_login(login_dto, &state).await;
|
let response = AuthService::mutation_mentor_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::OK);
|
assert_eq!(parts.status, StatusCode::OK);
|
||||||
@@ -258,7 +274,7 @@ mod auth_login_tests {
|
|||||||
async fn test_mentor_login_with_non_mentor_user() {
|
async fn test_mentor_login_with_non_mentor_user() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_user_not_mentor");
|
let email = generate_unique_email("test_user_not_mentor");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||||
|
|
||||||
@@ -267,7 +283,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_mentor_login(login_dto, &state).await;
|
let response = AuthService::mutation_mentor_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::FORBIDDEN);
|
assert_eq!(parts.status, StatusCode::FORBIDDEN);
|
||||||
@@ -285,7 +301,7 @@ mod auth_login_tests {
|
|||||||
async fn test_mentor_login_with_inactive_mentor() {
|
async fn test_mentor_login_with_inactive_mentor() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_inactive_mentor");
|
let email = generate_unique_email("test_inactive_mentor");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "Mentor", false).await;
|
create_test_user_with_role(&state, &email, password, "Mentor", false).await;
|
||||||
|
|
||||||
@@ -294,7 +310,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_mentor_login(login_dto, &state).await;
|
let response = AuthService::mutation_mentor_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||||
@@ -312,7 +328,7 @@ mod auth_login_tests {
|
|||||||
async fn test_login_creates_user_cache() {
|
async fn test_login_creates_user_cache() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_cache");
|
let email = generate_unique_email("test_cache");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||||
|
|
||||||
@@ -321,7 +337,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, _) = response.into_parts();
|
let (parts, _) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::OK);
|
assert_eq!(parts.status, StatusCode::OK);
|
||||||
@@ -337,7 +353,7 @@ mod auth_login_tests {
|
|||||||
async fn test_login_with_special_characters_in_email() {
|
async fn test_login_with_special_characters_in_email() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test+special");
|
let email = generate_unique_email("test+special");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||||
|
|
||||||
@@ -346,7 +362,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, _) = response.into_parts();
|
let (parts, _) = response.into_parts();
|
||||||
|
|
||||||
assert_eq!(parts.status, StatusCode::OK);
|
assert_eq!(parts.status, StatusCode::OK);
|
||||||
@@ -356,7 +372,7 @@ mod auth_login_tests {
|
|||||||
async fn test_login_with_case_sensitive_email() {
|
async fn test_login_with_case_sensitive_email() {
|
||||||
let state = setup_test_environment().await;
|
let state = setup_test_environment().await;
|
||||||
let email = generate_unique_email("test_case");
|
let email = generate_unique_email("test_case");
|
||||||
let password = "TestPass123!";
|
let password = "password";
|
||||||
|
|
||||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||||
|
|
||||||
@@ -365,7 +381,7 @@ mod auth_login_tests {
|
|||||||
password: password.to_string(),
|
password: password.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
let response = AuthService::mutation_login(login_dto, &state).await; // Corrected call
|
||||||
let (parts, body) = response.into_parts();
|
let (parts, body) = response.into_parts();
|
||||||
|
|
||||||
// Email should be case-sensitive
|
// Email should be case-sensitive
|
||||||
|
|||||||
@@ -21,13 +21,29 @@ mod auth_repository_test {
|
|||||||
id: make_thing("app_users", &Uuid::new_v4().to_string()),
|
id: make_thing("app_users", &Uuid::new_v4().to_string()),
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
fullname: "Test User".to_string(),
|
fullname: "Test User".to_string(),
|
||||||
|
legal_name: None,
|
||||||
password: "password".to_string(),
|
password: "password".to_string(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
phone_number: "081234567890".to_string(),
|
phone_number: "081234567890".to_string(),
|
||||||
|
phone_for_verification: None,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
role: make_thing("app_roles", &get_role_id(state).await),
|
role: make_thing("app_roles", &get_role_id(state).await),
|
||||||
mentor_id: None,
|
mentor_id: None,
|
||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
@@ -71,12 +87,28 @@ mod auth_repository_test {
|
|||||||
let mock_user = UsersDetailQueryDto {
|
let mock_user = UsersDetailQueryDto {
|
||||||
id: make_thing(&ResourceEnum::UsersCache.to_string(), &email),
|
id: make_thing(&ResourceEnum::UsersCache.to_string(), &email),
|
||||||
fullname: "Test User".into(),
|
fullname: "Test User".into(),
|
||||||
|
legal_name: None,
|
||||||
email: email.clone(),
|
email: email.clone(),
|
||||||
avatar: None,
|
avatar: None,
|
||||||
phone_number: "08123456789".into(),
|
phone_number: "08123456789".into(),
|
||||||
|
phone_for_verification: None,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
role: RolesDetailQueryDto {
|
role: RolesDetailQueryDto {
|
||||||
id: make_thing("app_roles", &Uuid::new_v4().to_string()),
|
id: make_thing("app_roles", &Uuid::new_v4().to_string()),
|
||||||
name: "Dummy Role".into(),
|
name: "Dummy Role".into(),
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{self, Request, StatusCode},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use http_body_util::BodyExt; // for `collect` and `to_bytes`
|
||||||
|
use tower::ServiceExt; // for `call`, `oneshot`, and `ready`
|
||||||
|
use mockall::predicate::*;
|
||||||
|
use mockall::mock;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use imphnen_iam::v1::auth::{AuthLoginResponsetDto, TokenDto};
|
||||||
|
use imphnen_iam::v1::auth::google::google_oauth_controller::GoogleOauthController;
|
||||||
|
use imphnen_iam::v1::auth::google::google_oauth_service::{AuthRequest, GoogleOauthService, GoogleOauthServiceImpl};
|
||||||
|
use imphnen_iam::v1::users::users_dto::{UsersDetailItemDto, UsersCreateRequestDto}; // Corrected: removed UserDto alias, used UsersCreateRequestDto
|
||||||
|
use imphnen_entities::error_dto::ErrorResponse;
|
||||||
|
use imphnen_libs::jsonwebtoken::generate_jwt;
|
||||||
|
use imphnen_libs::enviroment::{ENV, Env}; // Import ENV and Env
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub GoogleOauthServiceMock {}
|
||||||
|
impl GoogleOauthService for GoogleOauthServiceMock {
|
||||||
|
fn new() -> Self;
|
||||||
|
fn with_services(auth_service: crate::v1::auth::auth_service::AuthService, users_service: crate::v1::users::users_service::UsersService, env: &'static Env) -> Self; // Updated signature
|
||||||
|
fn google_oauth_client(&self) -> oauth2::basic::BasicClient;
|
||||||
|
fn generate_auth_url(&self) -> (url::Url, oauth2::CsrfToken);
|
||||||
|
async fn google_oauth_callback(&self, auth_request: AuthRequest, app_state: &crate::AppState) -> anyhow::Result<(crate::v1::users::users_dto::UsersDetailItemDto, crate::v1::auth::TokenDto), imphnen_entities::error_dto::error::Error>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub AuthServiceMock {}
|
||||||
|
impl crate::v1::auth::auth_service::AuthServiceTrait for AuthServiceMock {
|
||||||
|
async fn mutation_login(payload: crate::v1::auth::AuthLoginRequestDto, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
async fn mutation_mentor_login(payload: crate::v1::auth::AuthLoginRequestDto, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
async fn mutation_register(payload: crate::v1::auth::AuthRegisterRequestDto, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
async fn mutation_resend_otp(payload: crate::v1::auth::AuthResendOtpRequestDto, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
async fn mutation_refresh_token(payload: crate::v1::auth::AuthRefreshTokenRequestDto) -> axum::response::Response;
|
||||||
|
async fn mutation_forgot_password(payload: crate::v1::auth::AuthResendOtpRequestDto, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
async fn mutation_verify_email(payload: crate::v1::auth::AuthVerifyEmailRequestDto, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
async fn mutation_new_password(payload: crate::v1::auth::AuthNewPasswordRequestDto, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub UsersServiceMock {}
|
||||||
|
impl crate::v1::users::users_service::UsersServiceTrait for UsersServiceMock {
|
||||||
|
async fn get_user_list(state: &crate::AppState, meta: crate::MetaRequestDto) -> axum::response::Response;
|
||||||
|
async fn get_user_by_id(state: &crate::AppState, id: String) -> axum::response::Response;
|
||||||
|
async fn get_user_me(headers: http::HeaderMap, state: &crate::AppState) -> axum::response::Response;
|
||||||
|
async fn create_user(state: &crate::AppState, new_user: crate::v1::users::UsersCreateRequestDto) -> axum::response::Response;
|
||||||
|
async fn update_user(state: &crate::AppState, id: String, user: crate::v1::users::UsersUpdateRequestDto) -> axum::response::Response;
|
||||||
|
async fn update_user_me(state: &crate::AppState, headers: http::HeaderMap, user: crate::v1::users::UsersUpdateRequestDto) -> axum::response::Response;
|
||||||
|
async fn set_user_active_status(state: &crate::AppState, id: String, payload: crate::v1::users::UsersActiveInactiveRequestDto) -> axum::response::Response;
|
||||||
|
async fn update_user_password(state: &crate::AppState, email: String, payload: crate::v1::users::UsersSetNewPasswordRequestDto) -> axum::response::Response;
|
||||||
|
async fn get_user_by_mentor_id(state: &crate::AppState, mentor_id: String) -> axum::response::Response;
|
||||||
|
async fn delete_user(state: &crate::AppState, id: String) -> axum::response::Response;
|
||||||
|
async fn get_user_by_email(&self, email: &str) -> anyhow::Result<Option<UsersDetailItemDto>>; // Updated return type
|
||||||
|
async fn create_user_by_dto(&self, new_user: UsersCreateRequestDto) -> anyhow::Result<UsersDetailItemDto>; // Updated return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_app_with_mocked_google_oauth_service(
|
||||||
|
mock_google_oauth_service: MockGoogleOauthServiceMock,
|
||||||
|
) -> Router {
|
||||||
|
let google_oauth_controller =
|
||||||
|
GoogleOauthController::with_service(mock_google_oauth_service);
|
||||||
|
google_oauth_controller.get_routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn google_login_redirects_to_google_auth_url() {
|
||||||
|
let app = setup_app_with_mocked_google_oauth_service(
|
||||||
|
GoogleOauthServiceImpl::with_services(
|
||||||
|
AuthServiceMock::new(),
|
||||||
|
UsersServiceMock::new(),
|
||||||
|
&ENV, // Pass ENV
|
||||||
|
)
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.uri("/google/login")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::FOUND);
|
||||||
|
|
||||||
|
let location_header = response.headers().get(http::header::LOCATION).unwrap();
|
||||||
|
let location_str = location_header.to_str().unwrap();
|
||||||
|
|
||||||
|
assert!(location_str.starts_with("https://accounts.google.com/o/oauth2/v2/auth"));
|
||||||
|
assert!(location_str.contains("client_id="));
|
||||||
|
assert!(location_str.contains("redirect_uri="));
|
||||||
|
assert!(location_str.contains("response_type=code"));
|
||||||
|
assert!(location_str.contains("scope="));
|
||||||
|
assert!(location_str.contains("state="));
|
||||||
|
assert!(location_str.contains("access_type=offline"));
|
||||||
|
assert!(location_str.contains("prompt=consent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn google_callback_new_user_creates_user_and_returns_login_response() {
|
||||||
|
let mut mock_google_oauth_service = MockGoogleOauthServiceMock::new();
|
||||||
|
let mut mock_users_service = UsersServiceMock::new(); // Changed from MockUsersServiceMock to UsersServiceMock
|
||||||
|
|
||||||
|
let user_email = "new.user@example.com".to_string();
|
||||||
|
let expected_access_token = generate_jwt(&user_email, "test_user_id", vec![]).unwrap();
|
||||||
|
let expected_refresh_token = generate_jwt(&user_email, "test_user_id", vec![]).unwrap();
|
||||||
|
|
||||||
|
let expected_response_dto = AuthLoginResponsetDto {
|
||||||
|
token: TokenDto {
|
||||||
|
access_token: expected_access_token.clone(),
|
||||||
|
refresh_token: expected_refresh_token.clone(),
|
||||||
|
},
|
||||||
|
user: UsersDetailItemDto {
|
||||||
|
id: "test_user_id".to_string(),
|
||||||
|
email: user_email.clone(),
|
||||||
|
fullname: "Test User".to_string(), // Updated field
|
||||||
|
phone_number: "1234567890".to_string(), // Updated field
|
||||||
|
is_active: true,
|
||||||
|
gender: None, // Added field
|
||||||
|
birthdate: None, // Added field
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
role: imphnen_iam::v1::roles::roles_dto::RolesDetailItemDto {
|
||||||
|
id: "default_role_id".to_string(),
|
||||||
|
name: "User".to_string(),
|
||||||
|
permissions: vec![],
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mock_google_oauth_service.expect_google_oauth_callback()
|
||||||
|
.with(eq(AuthRequest { code: "some_code".to_string(), state: "some_state".to_string() }))
|
||||||
|
.returning(move |_| Ok(expected_response_dto.clone()));
|
||||||
|
|
||||||
|
mock_users_service.expect_get_user_by_email()
|
||||||
|
.with(eq(user_email.clone()))
|
||||||
|
.returning(|_| Ok(None)); // Simulate no existing user
|
||||||
|
|
||||||
|
mock_users_service.expect_create_user_by_dto()
|
||||||
|
.returning(|create_user_dto| {
|
||||||
|
Ok(UsersDetailItemDto { // Changed to UsersDetailItemDto
|
||||||
|
id: "test_user_id".to_string(),
|
||||||
|
email: create_user_dto.email,
|
||||||
|
fullname: create_user_dto.fullname, // Updated field
|
||||||
|
phone_number: create_user_dto.phone_number, // Updated field
|
||||||
|
is_active: create_user_dto.is_active,
|
||||||
|
gender: None, // Added field
|
||||||
|
birthdate: None, // Added field
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
role: imphnen_iam::v1::roles::roles_dto::RolesDetailItemDto {
|
||||||
|
id: "default_role_id".to_string(),
|
||||||
|
name: "User".to_string(),
|
||||||
|
permissions: vec![],
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = setup_app_with_mocked_google_oauth_service(
|
||||||
|
GoogleOauthServiceImpl::with_services(
|
||||||
|
AuthServiceMock::new(),
|
||||||
|
mock_users_service,
|
||||||
|
&ENV, // Pass ENV here
|
||||||
|
)
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.uri("/google/callback?code=some_code&state=some_state")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let json_body: AuthLoginResponsetDto = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(json_body.token.access_token, expected_access_token);
|
||||||
|
assert_eq!(json_body.token.refresh_token, expected_refresh_token);
|
||||||
|
assert_eq!(json_body.user.email, user_email);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn google_callback_existing_user_returns_login_response() {
|
||||||
|
let mut mock_google_oauth_service = MockGoogleOauthServiceMock::new();
|
||||||
|
let mut mock_users_service = UsersServiceMock::new(); // Changed from MockUsersServiceMock to UsersServiceMock
|
||||||
|
|
||||||
|
let user_email = "existing.user@example.com".to_string();
|
||||||
|
let expected_access_token = generate_jwt(&user_email, "existing_user_id", vec![]).unwrap();
|
||||||
|
let expected_refresh_token = generate_jwt(&user_email, "existing_user_id", vec![]).unwrap();
|
||||||
|
|
||||||
|
let existing_user_dto = UsersDetailItemDto { // Changed from UserDto to UsersDetailItemDto
|
||||||
|
id: "existing_user_id".to_string(),
|
||||||
|
email: user_email.clone(),
|
||||||
|
fullname: "Existing User".to_string(), // Updated field
|
||||||
|
phone_number: "0987654321".to_string(), // Updated field
|
||||||
|
is_active: true,
|
||||||
|
gender: None, // Added field
|
||||||
|
birthdate: None, // Added field
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
role: imphnen_iam::v1::roles::roles_dto::RolesDetailItemDto {
|
||||||
|
id: "default_role_id".to_string(),
|
||||||
|
name: "User".to_string(),
|
||||||
|
permissions: vec![],
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_response_dto = AuthLoginResponsetDto {
|
||||||
|
token: TokenDto {
|
||||||
|
access_token: expected_access_token.clone(),
|
||||||
|
refresh_token: expected_refresh_token.clone(),
|
||||||
|
},
|
||||||
|
user: existing_user_dto.clone(), // Cloned
|
||||||
|
};
|
||||||
|
|
||||||
|
mock_google_oauth_service.expect_google_oauth_callback()
|
||||||
|
.with(eq(AuthRequest { code: "some_code".to_string(), state: "some_state".to_string() }))
|
||||||
|
.returning(move |_| Ok(expected_response_dto.clone()));
|
||||||
|
|
||||||
|
mock_users_service.expect_get_user_by_email()
|
||||||
|
.with(eq(user_email.clone()))
|
||||||
|
.returning(move |_| {
|
||||||
|
Ok(Some(UsersDetailItemDto { // Changed to UsersDetailItemDto
|
||||||
|
id: "existing_user_id".to_string(),
|
||||||
|
email: user_email.clone(),
|
||||||
|
fullname: "Existing User".to_string(), // Updated field
|
||||||
|
phone_number: "0987654321".to_string(), // Updated field
|
||||||
|
is_active: true,
|
||||||
|
gender: None, // Added field
|
||||||
|
birthdate: None, // Added field
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
role: imphnen_iam::v1::roles::roles_dto::RolesDetailItemDto {
|
||||||
|
id: "default_role_id".to_string(),
|
||||||
|
name: "User".to_string(),
|
||||||
|
permissions: vec![],
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}); // Simulate existing user
|
||||||
|
|
||||||
|
// Ensure create_user_by_dto is NOT called for existing user
|
||||||
|
mock_users_service.expect_create_user_by_dto().times(0);
|
||||||
|
|
||||||
|
let app = setup_app_with_mocked_google_oauth_service(
|
||||||
|
GoogleOauthServiceImpl::with_services(
|
||||||
|
AuthServiceMock::new(),
|
||||||
|
mock_users_service,
|
||||||
|
&ENV, // Pass ENV here
|
||||||
|
)
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.uri("/google/callback?code=some_code&state=some_state")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = app.oneshot(request).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let json_body: AuthLoginResponsetDto = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(json_body.token.access_token, expected_access_token);
|
||||||
|
assert_eq!(json_body.token.refresh_token, expected_refresh_token);
|
||||||
|
assert_eq!(json_body.user.email, user_email);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,13 +21,29 @@ pub fn create_test_user(
|
|||||||
id: make_thing("app_users", &Uuid::new_v4().to_string()),
|
id: make_thing("app_users", &Uuid::new_v4().to_string()),
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
fullname: format!("Randomize {} {}", fullname, rand::random::<u32>()),
|
fullname: format!("Randomize {} {}", fullname, rand::random::<u32>()),
|
||||||
|
legal_name: None,
|
||||||
password: hash_password("secret").unwrap(),
|
password: hash_password("secret").unwrap(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
phone_number: "081234567890".to_string(),
|
phone_number: "081234567890".to_string(),
|
||||||
|
phone_for_verification: None,
|
||||||
is_active,
|
is_active,
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
role: make_thing("app_roles", role_id),
|
role: make_thing("app_roles", role_id),
|
||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
updated_at: get_iso_date(),
|
updated_at: get_iso_date(),
|
||||||
|
|||||||
+18
-12
@@ -1,7 +1,7 @@
|
|||||||
|
// Restore only the necessary imports to fix unresolved function errors
|
||||||
use crate::{get_iso_date, hash_password};
|
use crate::{get_iso_date, hash_password};
|
||||||
use imphnen_entities::AppState;
|
use imphnen_entities::AppState;
|
||||||
use imphnen_iam::{PermissionsEnum, UsersSchema};
|
use imphnen_iam::{PermissionsEnum, UsersSchema};
|
||||||
use imphnen_libs::enviroment::load_env;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use surrealdb::engine::{any, local};
|
use surrealdb::engine::{any, local};
|
||||||
@@ -29,23 +29,15 @@ struct RoleSeedData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_mock_app_state() -> AppState {
|
pub async fn create_mock_app_state() -> AppState {
|
||||||
load_env();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let db_ws = any::connect("ws://127.00.1:8000/rpc").await.unwrap();
|
let db_ws = any::connect("ws://127.00.1:8000/rpc").await.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let db_mem = Surreal::new::<local::Mem>(()).await.unwrap();
|
let db_mem = Surreal::new::<local::Mem>(()).await.unwrap();
|
||||||
|
|
||||||
|
|
||||||
let unique_id = Uuid::new_v4().to_string();
|
let unique_id = Uuid::new_v4().to_string();
|
||||||
let ns = format!("test_ns_{unique_id}");
|
let ns = format!("test_ns_{unique_id}");
|
||||||
let db = format!("test_db_{unique_id}");
|
let db = format!("test_db_{unique_id}");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db_ws
|
db_ws
|
||||||
.signin(Root {
|
.signin(Root {
|
||||||
username: "root",
|
username: "root",
|
||||||
@@ -54,11 +46,8 @@ pub async fn create_mock_app_state() -> AppState {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db_ws.use_ns(&ns).use_db(&db).await.unwrap();
|
db_ws.use_ns(&ns).use_db(&db).await.unwrap();
|
||||||
|
|
||||||
|
|
||||||
AppState {
|
AppState {
|
||||||
surrealdb_ws: db_ws,
|
surrealdb_ws: db_ws,
|
||||||
surrealdb_mem: db_mem,
|
surrealdb_mem: db_mem,
|
||||||
@@ -160,15 +149,31 @@ pub async fn seed_users_for_test(
|
|||||||
let user = UsersSchema {
|
let user = UsersSchema {
|
||||||
id: Thing::from(("app_users", id)),
|
id: Thing::from(("app_users", id)),
|
||||||
fullname: fullname.into(),
|
fullname: fullname.into(),
|
||||||
|
legal_name: None,
|
||||||
email: email.into(),
|
email: email.into(),
|
||||||
password: hash_password("password").unwrap(),
|
password: hash_password("password").unwrap(),
|
||||||
avatar: None,
|
avatar: None,
|
||||||
phone_number: "081234567890".into(),
|
phone_number: "081234567890".into(),
|
||||||
|
phone_for_verification: None,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
mentor_id: None,
|
mentor_id: None,
|
||||||
gender: None,
|
gender: None,
|
||||||
birthdate: None,
|
birthdate: None,
|
||||||
|
domicile: None,
|
||||||
|
bio: None,
|
||||||
|
last_education: None,
|
||||||
|
linkedin_url: None,
|
||||||
|
github_url: None,
|
||||||
|
cv_url: None,
|
||||||
|
portfolio_url: None,
|
||||||
|
website_url: None,
|
||||||
|
twitter_url: None,
|
||||||
|
location: None,
|
||||||
|
skills: None,
|
||||||
|
experience: None,
|
||||||
|
education: None,
|
||||||
|
career_status: None,
|
||||||
role: Thing::from(("app_roles", role_id)),
|
role: Thing::from(("app_roles", role_id)),
|
||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
updated_at: get_iso_date(),
|
updated_at: get_iso_date(),
|
||||||
@@ -183,6 +188,7 @@ pub async fn seed_users_for_test(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn setup_all_test_environment() -> AppState {
|
pub async fn setup_all_test_environment() -> AppState {
|
||||||
|
debug!("Setting up all test environment in setup_all_test_environment()");
|
||||||
let app_state = create_mock_app_state().await;
|
let app_state = create_mock_app_state().await;
|
||||||
app_state
|
app_state
|
||||||
.surrealdb_mem
|
.surrealdb_mem
|
||||||
|
|||||||
Reference in New Issue
Block a user