+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)")
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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,
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user