Add comprehensive tests for mentor repository and authentication
- Implemented tests for creating, retrieving, updating, and deleting mentors in `mentor_repository_test.rs`. - Added tests for user authentication, including successful login, invalid email formats, and inactive users in `auth_login_tests.rs`. - Created a mock test environment setup in `mock_test.rs` to facilitate database operations during tests. - Updated module structure to include new test files for mentors and authentication. - Ensured cleanup of the database after tests to maintain isolation and prevent side effects.
This commit is contained in:
+24
-8
@@ -1,8 +1,24 @@
|
||||
PORT=
|
||||
SURREALDB_URL=
|
||||
SURREALDB_USERNAME=
|
||||
SURREALDB_PASSWORD=
|
||||
SURREALDB_NAMESPACE=
|
||||
SURREALDB_DBNAME=
|
||||
ACCESS_TOKEN_SECRET=
|
||||
REFRESH_TOKEN_SECRET=
|
||||
RUST_ENV=development
|
||||
PORT=4099
|
||||
SURREALDB_URL=ws://localhost:8000/rpc
|
||||
SURREALDB_USERNAME=root
|
||||
SURREALDB_PASSWORD=root
|
||||
SURREALDB_NAMESPACE=test
|
||||
SURREALDB_DBNAME=test
|
||||
ACCESS_TOKEN_SECRET=your-access-token-secret-key-here
|
||||
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
||||
SMTP_EMAIL=your-email@example.com
|
||||
SMTP_PASSWORD=your-smtp-password
|
||||
SMTP_NAME="Your App Name"
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
REDISDB_URL=localhost
|
||||
FE_URL=http://localhost
|
||||
MINIO_ENDPOINT=http://localhost:9000
|
||||
MINIO_BUCKET_NAME=default_bucket
|
||||
MINIO_ACCESS_KEY=your-minio-access-key
|
||||
MINIO_SECRET_KEY=your-minio-secret-key
|
||||
MAIL_USER=your-email@example.com
|
||||
MAIL_PASSWORD=your-smtp-password
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=465
|
||||
MAIL_SECURE=true
|
||||
|
||||
Generated
+727
-578
File diff suppressed because it is too large
Load Diff
+32
-5
@@ -20,28 +20,55 @@ axum = { version = "0.8.4", features = ["multipart"] }
|
||||
log = "0.4.25"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.138"
|
||||
tokio = { version = "1.45.0" }
|
||||
tokio = { version = "1.45.0", features = ["full"] }
|
||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||
jsonwebtoken = "9.3.1"
|
||||
chrono = "0.4.41"
|
||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
||||
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] }
|
||||
surrealdb = { version = "2.3.2", features = ["kv-mem"] }
|
||||
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
||||
thiserror = "2.0.12"
|
||||
anyhow = "1.0.98"
|
||||
rand = { version = "0.9.1", features = ["std", "alloc"] }
|
||||
rand_distr = "0.5.1"
|
||||
tower-http = { version = "0.6.4", features = ["cors"] }
|
||||
validator = { version = "0.12", features = ["derive"] }
|
||||
tower-http = { version = "0.6.4", features = ["cors", "trace"] }
|
||||
http-body-util = "0.1.1"
|
||||
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.11.1"
|
||||
axum-test = "17.2.0"
|
||||
fancy-regex = "0.14.0"
|
||||
fancy-regex = "0.15.0"
|
||||
futures = "0.3.31"
|
||||
tower = "0.5.2"
|
||||
env_logger = "0.11.8"
|
||||
dotenvy = "0.15.7"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
once_cell = "1.21.3"
|
||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] }
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
strum_macros = "0.27.1"
|
||||
hyper = "1.6.0"
|
||||
hyper-util = "0.1.0"
|
||||
async-trait = "0.1.75"
|
||||
tokio-tungstenite = "0.23"
|
||||
url = "2.5"
|
||||
futures-util = "0.3"
|
||||
|
||||
tests = { path = "./tests" }
|
||||
imphnen-iam = { path = "./imphnen-iam" }
|
||||
imphnen-cms = { path = "./imphnen-cms" }
|
||||
imphnen-libs = { path = "./imphnen-libs" }
|
||||
imphnen-utils = { path = "./imphnen-utils" }
|
||||
imphnen-gacha = { path = "./imphnen-gacha" }
|
||||
imphnen-gateway = { path = "./imphnen-gateway" }
|
||||
imphnen-backend = { path = "./imphnen-backend" }
|
||||
imphnen-entities = { path = "./imphnen-entities" }
|
||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.85-alpine AS builder
|
||||
FROM rust:1.86-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache \
|
||||
curl \
|
||||
|
||||
@@ -4,12 +4,13 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-gateway = { version = "0.1.0", path = "../imphnen-gateway" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" }
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-gateway.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -26,3 +27,9 @@ anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
env_logger.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace=true
|
||||
tokio-tungstenite.workspace = true
|
||||
url.workspace = true
|
||||
futures-util.workspace = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use env_logger;
|
||||
use imphnen_gateway::gateway_service;
|
||||
use imphnen_libs::axum_init;
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
|
||||
use url::Url;
|
||||
use futures_util::{StreamExt, SinkExt};
|
||||
use serde_json::json;
|
||||
|
||||
// Menentukan kredensial dan detail koneksi secara langsung sebagai string statis
|
||||
static SURREALDB_URL_WS: &str = "ws://localhost:8000/rpc";
|
||||
static SURREALDB_USERNAME: &str = "root";
|
||||
static SURREALDB_PASSWORD: &str = "root";
|
||||
static SURREALDB_NAMESPACE: &str = "test";
|
||||
static SURREALDB_DBNAME: &str = "test";
|
||||
|
||||
// Daftar tabel sebagai variabel static yang tidak dapat diubah
|
||||
static TABLES_TO_CLEAR: &[&str] = &[
|
||||
"app_events", "users", "roles", "permissions", "gacha_rolls",
|
||||
"mentor_users", "gacha_claims", "gacha_credits", "gacha_items",
|
||||
"mentor_profiles", "roles_permissions", "testimonials",
|
||||
];
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Tidak perlu memuat env lagi, karena kita menggunakan nilai hardcoded
|
||||
// imphnen_libs::enviroment::load_env(); // Baris ini tidak lagi dibutuhkan
|
||||
// let env = Env::new(); // Baris ini tidak lagi dibutuhkan
|
||||
|
||||
println!("DEBUG: URL WS: {}", SURREALDB_URL_WS);
|
||||
println!("DEBUG: Username: {}", SURREALDB_USERNAME);
|
||||
println!("DEBUG: Namespace: {}", SURREALDB_NAMESPACE);
|
||||
println!("DEBUG: Database: {}", SURREALDB_DBNAME);
|
||||
|
||||
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
|
||||
|
||||
let (ws_stream, _) = connect_async(url).await?;
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Authenticate (signin)
|
||||
let signin_query = json!({
|
||||
"method": "signin",
|
||||
"params": [{
|
||||
"user": SURREALDB_USERNAME, // Menggunakan SURREALDB_USERNAME statis
|
||||
"pass": SURREALDB_PASSWORD, // Menggunakan SURREALDB_PASSWORD statis
|
||||
}],
|
||||
"id": 1
|
||||
}).to_string();
|
||||
println!("DEBUG: Sending signin query: {}", signin_query);
|
||||
write.send(Message::Text(signin_query)).await?;
|
||||
|
||||
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
|
||||
let signin_response_msg = signin_response?;
|
||||
let signin_response_str = signin_response_msg.to_text()?;
|
||||
println!("DEBUG: Signin response: {}", signin_response_str);
|
||||
if signin_response_str.contains("\"error\":") {
|
||||
return Err(format!("Signin failed: {}", signin_response_str).into());
|
||||
}
|
||||
|
||||
// Use namespace and database
|
||||
let use_query = json!({
|
||||
"method": "use",
|
||||
"params": [SURREALDB_NAMESPACE, SURREALDB_DBNAME], // Menggunakan NS & DB statis
|
||||
"id": 2
|
||||
}).to_string();
|
||||
println!("DEBUG: Sending use query: {}", use_query);
|
||||
write.send(Message::Text(use_query)).await?;
|
||||
|
||||
let use_response = read.next().await.ok_or("Failed to read use response")?;
|
||||
let use_response_msg = use_response?;
|
||||
let use_response_str = use_response_msg.to_text()?;
|
||||
println!("DEBUG: Use response: {}", use_response_str);
|
||||
if use_response_str.contains("\"error\":") {
|
||||
return Err(format!("USE command failed: {}", use_response_str).into());
|
||||
}
|
||||
|
||||
println!("INFO: Attempting to clear database tables via WebSocket...");
|
||||
|
||||
let mut all_clear = true;
|
||||
for (i, table) in TABLES_TO_CLEAR.iter().enumerate() {
|
||||
let remove_query = format!("REMOVE TABLE {};", table);
|
||||
let query_json = json!({
|
||||
"method": "query",
|
||||
"params": [remove_query],
|
||||
"id": i + 3
|
||||
}).to_string();
|
||||
|
||||
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
|
||||
write.send(Message::Text(query_json)).await?;
|
||||
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
|
||||
|
||||
|
||||
|
||||
match response_result {
|
||||
Ok(msg) => {
|
||||
let response_str = msg.to_text()?;
|
||||
if response_str.contains("\"error\":") {
|
||||
println!("WARN: Failed to REMOVE TABLE {}: {}. Attempting DELETE type::{}.", table, response_str, table);
|
||||
let delete_all_query = format!("DELETE FROM {};", table);
|
||||
let delete_all_json = json!({
|
||||
"method": "query",
|
||||
"params": [delete_all_query],
|
||||
"id": i + 300
|
||||
}).to_string();
|
||||
|
||||
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
|
||||
write.send(Message::Text(delete_all_json)).await?;
|
||||
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
|
||||
|
||||
match delete_response_result {
|
||||
Ok(delete_msg) => {
|
||||
let delete_response_str = delete_msg.to_text()?;
|
||||
if delete_response_str.contains("\"error\":") {
|
||||
println!("ERROR: Failed to DELETE type::{} : {}", table, delete_response_str);
|
||||
all_clear = false;
|
||||
} else {
|
||||
println!("INFO: Successfully DELETED type:: table: {}", table);
|
||||
|
||||
}
|
||||
},
|
||||
Err(delete_e) => {
|
||||
println!("ERROR: Error receiving response for DELETE type:: table {}: {}", table, delete_e);
|
||||
all_clear = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("INFO: Successfully REMOVED TABLE: {}", table);
|
||||
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("ERROR: Error receiving response for REMOVE TABLE {}: {}", table, e);
|
||||
all_clear = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if table is empty after deletion attempt
|
||||
let select_query = format!("SELECT * FROM {} LIMIT 1;", table);
|
||||
let select_json = json!({
|
||||
"method": "query",
|
||||
"params": [select_query],
|
||||
"id": i + 1000
|
||||
}).to_string();
|
||||
write.send(Message::Text(select_json)).await?;
|
||||
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
|
||||
match select_response_result {
|
||||
Ok(select_msg) => {
|
||||
let select_response_str = select_msg.to_text()?;
|
||||
if select_response_str.contains("does not exist") {
|
||||
println!("CHECK: Table '{}' does not exist after clear attempt (success).", table);
|
||||
} else if select_response_str.contains("\"result\":[]") || select_response_str.contains("\"result\":[[]]") {
|
||||
println!("CHECK: Table '{}' is empty after clear attempt.", table);
|
||||
} else {
|
||||
println!("WARNING: Table '{}' is NOT empty after clear attempt! Response: {}", table, select_response_str);
|
||||
all_clear = false;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("ERROR: Error receiving response for SELECT check on table {}: {}", table, e);
|
||||
all_clear = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("INFO: Database clearing complete.");
|
||||
|
||||
if !all_clear {
|
||||
eprintln!("ERROR: One or more tables could not be cleared. Check logs for details.");
|
||||
return Err("Database clearing failed for one or more tables.".into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
@@ -20,7 +20,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let events = vec![
|
||||
(
|
||||
"e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o",
|
||||
"Tech Conference 2025",
|
||||
"Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.",
|
||||
"https://techconf2025.example.com",
|
||||
@@ -31,7 +30,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"2025-06-17T18:00:00Z",
|
||||
),
|
||||
(
|
||||
"f2b3c4d5-6e7f-8g9h-0i1j-2k3l4m5n6o7p",
|
||||
"Online Web Development Workshop",
|
||||
"Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.",
|
||||
"https://webdev-workshop.example.com",
|
||||
@@ -42,7 +40,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"2025-07-10T17:00:00Z",
|
||||
),
|
||||
(
|
||||
"g3c4d5e6-7f8g-9h0i-1j2k-3l4m5n6o7p8q",
|
||||
"Startup Pitch Competition",
|
||||
"Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.",
|
||||
"https://startup-pitch.example.com",
|
||||
@@ -53,7 +50,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"2025-08-05T16:00:00Z",
|
||||
),
|
||||
(
|
||||
"h4d5e6f7-8g9h-0i1j-2k3l-4m5n6o7p8q9r",
|
||||
"Digital Marketing Masterclass",
|
||||
"Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.",
|
||||
"https://digital-marketing.example.com",
|
||||
@@ -65,9 +61,20 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
),
|
||||
];
|
||||
|
||||
for (id, name, description, detail_link, price, location, is_online, start_date, end_date) in events {
|
||||
for (
|
||||
name,
|
||||
description,
|
||||
detail_link,
|
||||
price,
|
||||
location,
|
||||
is_online,
|
||||
start_date,
|
||||
end_date,
|
||||
) in events
|
||||
{
|
||||
let uuid = Uuid::new_v4().to_string(); // Generate new UUID
|
||||
let event = EventsSchema {
|
||||
id: Thing::from(("app_events", id)),
|
||||
id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
detail_link: detail_link.into(),
|
||||
@@ -81,11 +88,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
|
||||
db.create::<Option<EventsSchema>>(("app_events", id))
|
||||
db.create::<Option<EventsSchema>>(("app_events", uuid.as_str())) // Use generated UUID
|
||||
.content(event)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted event: {} ({})", name, if is_online { "Online" } else { "In-person" });
|
||||
println!(
|
||||
"✅ Inserted event: {} ({})",
|
||||
name,
|
||||
if is_online { "Online" } else { "In-person" }
|
||||
);
|
||||
}
|
||||
|
||||
println!("✅ All Events seeded");
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.await?;
|
||||
|
||||
db.query("DELETE type::thing('app_gacha_items', $id)")
|
||||
.bind(("id", "gacha_item_test_id"))
|
||||
.await?;
|
||||
db.query("DELETE type::thing('app_gacha_rolls', $id)")
|
||||
.bind(("id", "gacha_roll_test_id"))
|
||||
.await?;
|
||||
|
||||
let gacha_item_id = "gacha_item_test_id";
|
||||
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", gacha_item_id))
|
||||
.bind(("name", "Test Gacha Item"))
|
||||
.bind(("image_url", "https://example.com/gacha_item.png"))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("Gacha Item seeded successfully!");
|
||||
|
||||
let gacha_roll_id = "gacha_roll_test_id";
|
||||
db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", gacha_roll_id))
|
||||
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
|
||||
.bind(("quantity", 10))
|
||||
.bind(("weight", 1.0))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("Gacha Roll seeded successfully!");
|
||||
|
||||
println!("✅ Gacha items and rolls seeded.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.await?;
|
||||
|
||||
db.query("DELETE type::thing('app_mentors', $id)")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.await?;
|
||||
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.await?;
|
||||
|
||||
use surrealdb::sql::Thing;
|
||||
db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.bind(("fullname", "Mentor User"))
|
||||
.bind(("email", "mentor@example.com"))
|
||||
.bind(("password", hash_password("password").unwrap()))
|
||||
.bind(("avatar", Option::<String>::None))
|
||||
.bind(("phone_number", "081234567890"))
|
||||
.bind(("is_active", true))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("mentor_id", Option::<Thing>::None))
|
||||
.bind(("gender", "male"))
|
||||
.bind(("birthdate", "1990-05-15"))
|
||||
.bind(("role", Thing::from(("app_roles", "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a"))))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
|
||||
db.query("CREATE type::thing('app_mentors', $id) SET user_id = $user_id, legal_name = $legal_name, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, linkedin_url = $linkedin_url, github_url = $github_url, cv_url = $cv_url, industries = $industries, expertise = $expertise, languages = $languages, current_company = $current_company, current_role = $current_role, years_of_experience = $years_of_experience, topics_of_interest = $topics_of_interest, preferred_mentee_level = $preferred_mentee_level, preferred_mentoring_formats = $preferred_mentoring_formats, availability_commitment = $availability_commitment, mentoring_rate = $mentoring_rate, status = $status, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at, email = $email")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))))
|
||||
.bind(("legal_name", "Mentor User"))
|
||||
.bind(("identity_document_url", "https://example.com/ktp.jpg"))
|
||||
.bind(("phone_for_verification", "081234567890"))
|
||||
.bind(("bio", "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant."))
|
||||
.bind(("linkedin_url", "https://linkedin.com/in/mentor"))
|
||||
.bind(("github_url", "https://github.com/mentor"))
|
||||
.bind(("cv_url", Option::<String>::None))
|
||||
.bind(("industries", vec!["Software", "Education"]))
|
||||
.bind(("expertise", vec!["Rust", "Microservices"]))
|
||||
.bind(("languages", vec!["Indonesian", "English"]))
|
||||
.bind(("current_company", "PT Contoh"))
|
||||
.bind(("current_role", "Senior Backend Engineer"))
|
||||
.bind(("years_of_experience", 5))
|
||||
.bind(("topics_of_interest", vec!["Rust Programming", "Backend Development"]))
|
||||
.bind(("preferred_mentee_level", vec!["beginner", "intermediate"]))
|
||||
.bind(("preferred_mentoring_formats", vec!["online", "offline"]))
|
||||
.bind(("availability_commitment", "2 jam per minggu untuk mentoring online dan offline"))
|
||||
.bind(("mentoring_rate", json!({
|
||||
"amount": 100000,
|
||||
"currency": "IDR",
|
||||
"per_duration": "hour"
|
||||
})))
|
||||
.bind(("status", "verified"))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.bind(("email", "mentor@example.com"))
|
||||
.await?;
|
||||
println!("Mentor created successfully!");
|
||||
println!("Updating user with mentor_id...");
|
||||
|
||||
db.query("UPDATE type::thing('app_users', $id) SET mentor_id = $mentor_id")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.bind((
|
||||
"mentor_id",
|
||||
Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
|
||||
))
|
||||
.await?;
|
||||
println!("User updated with mentor_id successfully!");
|
||||
|
||||
println!("✅ Inserted mentor user: mentor@example.com");
|
||||
println!("✅ Mentor user seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -48,6 +48,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::RegisterMentors,
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::UpdateOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
] {
|
||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||
.bind(("id", permission.id()))
|
||||
@@ -61,9 +69,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
println!("✅ Inserted: {}", permission.to_string());
|
||||
println!("✅ Inserted: {permission}");
|
||||
}
|
||||
|
||||
println!("✅ All Permissions seeded");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
@@ -49,9 +49,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
None,
|
||||
Some("2025-02-22T15:38:39.868306+00"),
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
"Mentor",
|
||||
None,
|
||||
Some("2025-07-06T10:00:00.000000+00"),
|
||||
),
|
||||
];
|
||||
|
||||
for (id, name, _created_at, _updated_at) in roles {
|
||||
db.query("DELETE type::thing('app_roles', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
|
||||
.bind(("id", id))
|
||||
.bind((
|
||||
@@ -65,7 +74,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
println!("✅ Inserted role: {}", name);
|
||||
println!("✅ Inserted role: {name}");
|
||||
}
|
||||
println!("✅ All Roles seeded");
|
||||
Ok(())
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum};
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::engine::any;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
@@ -16,43 +17,118 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.await?;
|
||||
let permission_refs_admin: Vec<_> = [
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::CreateUsers,
|
||||
PermissionsEnum::DeleteUsers,
|
||||
PermissionsEnum::UpdateUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::CreateRoles,
|
||||
PermissionsEnum::DeleteRoles,
|
||||
PermissionsEnum::UpdateRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::CreatePermissions,
|
||||
PermissionsEnum::DeletePermissions,
|
||||
PermissionsEnum::UpdatePermissions,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::CreateGachaItems,
|
||||
PermissionsEnum::DeleteGachaItems,
|
||||
PermissionsEnum::UpdateGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
]
|
||||
.iter()
|
||||
.map(|perm| make_thing("app_permissions", perm.id()))
|
||||
.collect();
|
||||
let admin_role_id = "f6b03f25-e416-4893-ac88-caaa690afb07";
|
||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||
.bind(("role_id", admin_role_id))
|
||||
.bind(("permissions", permission_refs_admin))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("✅ All permissions successfully added to Admin role");
|
||||
|
||||
let roles_permissions = vec![
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::CreateUsers,
|
||||
PermissionsEnum::DeleteUsers,
|
||||
PermissionsEnum::UpdateUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::CreateRoles,
|
||||
PermissionsEnum::DeleteRoles,
|
||||
PermissionsEnum::UpdateRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::CreatePermissions,
|
||||
PermissionsEnum::DeletePermissions,
|
||||
PermissionsEnum::UpdatePermissions,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::CreateGachaItems,
|
||||
PermissionsEnum::DeleteGachaItems,
|
||||
PermissionsEnum::UpdateGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::RegisterMentors,
|
||||
PermissionsEnum::UpdateMentors,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
],
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
vec![
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::UpdateOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
],
|
||||
),
|
||||
(
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
vec![
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::RegisterMentors,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
],
|
||||
),
|
||||
(
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
],
|
||||
),
|
||||
(
|
||||
"60f1aeb7-dad2-4e06-bcb5-be1ba510c906",
|
||||
vec![PermissionsEnum::ActivateUsers],
|
||||
),
|
||||
("6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0", vec![]),
|
||||
];
|
||||
|
||||
for (role_id, permissions) in roles_permissions {
|
||||
let permission_refs: Vec<_> = permissions
|
||||
.iter()
|
||||
.map(|perm| make_thing("app_permissions", perm.id()))
|
||||
.collect();
|
||||
|
||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||
.bind(("role_id", role_id))
|
||||
.bind(("permissions", permission_refs))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("✅ Permissions updated for role: {role_id}");
|
||||
}
|
||||
|
||||
println!("✅ All roles permissions updated!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use imphnen_iam::UsersSchema;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||
use std::error::Error;
|
||||
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -40,6 +41,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
];
|
||||
|
||||
for (id, email, fullname, role_id) in users {
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
|
||||
let user = UsersSchema {
|
||||
id: Thing::from(("app_users", id)),
|
||||
fullname: fullname.into(),
|
||||
@@ -49,6 +54,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
phone_number: "081234567890".into(),
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
mentor_id: None,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
role: Thing::from(("app_roles", role_id)),
|
||||
@@ -60,7 +66,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
.content(user)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted user: {} ({})", fullname, email);
|
||||
println!("✅ Inserted user: {fullname} ({email})");
|
||||
}
|
||||
|
||||
println!("✅ All Users seeded");
|
||||
|
||||
@@ -20,7 +20,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
run_seed("seed_roles_permissions")?;
|
||||
run_seed("seed_users")?;
|
||||
run_seed("seed_events")?;
|
||||
|
||||
run_seed("seed_gacha_rolls")?;
|
||||
run_seed("seed_mentor_user")?;
|
||||
println!("\n✅ All seeding completed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ use imphnen_libs::axum_init;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||
})
|
||||
|
||||
@@ -4,10 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -23,3 +23,8 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
VALID_URL_REGEX = "^https?://"
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
lazy_static! {
|
||||
pub static ref VALID_URL_REGEX: regex::Regex =
|
||||
regex::Regex::new(r"^https?://").unwrap();
|
||||
}
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
// Lazy static regex for URL validation
|
||||
lazy_static! {
|
||||
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsCreateRequestDto {
|
||||
#[validate(length(min = 1, message = "Name is required"))]
|
||||
@@ -19,13 +17,10 @@ pub struct EventsCreateRequestDto {
|
||||
#[validate(length(min = 1, message = "Description is required"))]
|
||||
pub description: String,
|
||||
|
||||
#[validate(regex(
|
||||
path = "VALID_URL_REGEX",
|
||||
message = "Detail link must be a valid URL"
|
||||
))]
|
||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
||||
pub detail_link: String,
|
||||
|
||||
#[validate(range(min = 0, message = "Price cannot be negative"))]
|
||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
||||
pub price: f64,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
@@ -42,16 +37,18 @@ pub struct EventsCreateRequestDto {
|
||||
pub struct EventsUpdateRequestDto {
|
||||
#[validate(length(min = 1, message = "Name is required"))]
|
||||
pub name: String,
|
||||
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
|
||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub description: String,
|
||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct EventsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -12,17 +14,25 @@ impl<'a> EventsRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_event_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> {
|
||||
let query = ListQueryBuilder::new(&ResourceEnum::Events.to_string())
|
||||
let now = Instant::now();
|
||||
let query = ListQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
let res: Vec<EventsQueryDto> =
|
||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_event_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = ResponseListSuccessDto {
|
||||
data: res,
|
||||
meta: None,
|
||||
@@ -30,8 +40,9 @@ impl<'a> EventsRepository<'a> {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// Get event by ID
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||
.with_id(&id)
|
||||
@@ -39,6 +50,12 @@ impl<'a> EventsRepository<'a> {
|
||||
let sql = builder.build();
|
||||
let result: Option<EventsQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_event_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match result {
|
||||
Some(event) => {
|
||||
@@ -51,13 +68,20 @@ impl<'a> EventsRepository<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Create new event
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<EventsSchema> = db
|
||||
.create(ResourceEnum::Events.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success create event".into()),
|
||||
@@ -65,17 +89,16 @@ impl<'a> EventsRepository<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Update existing event
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
// Cek apakah event ada
|
||||
let existing = self.query_event_by_id(data.id.id.to_raw()).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Event already deleted");
|
||||
}
|
||||
|
||||
// Merge field tertentu jika diperlukan
|
||||
let merged = EventsSchema {
|
||||
created_at: existing.created_at,
|
||||
updated_at: get_iso_date(),
|
||||
@@ -84,6 +107,12 @@ impl<'a> EventsRepository<'a> {
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success update event".into()),
|
||||
@@ -91,8 +120,9 @@ impl<'a> EventsRepository<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Soft delete event (mark is_deleted = true)
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_event(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let event = self.query_event_by_id(id).await?;
|
||||
if event.is_deleted {
|
||||
@@ -104,6 +134,12 @@ impl<'a> EventsRepository<'a> {
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success delete event".into()),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
|
||||
use super::events_dto::{EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto};
|
||||
use super::events_dto::{
|
||||
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsSchema {
|
||||
|
||||
@@ -1,86 +1,101 @@
|
||||
use super::{
|
||||
events_dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto, EventsUpdateRequestDto},
|
||||
events_repository::EventsRepository,
|
||||
events_schema::EventsSchema,
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_repository::EventsRepository,
|
||||
events_schema::EventsSchema,
|
||||
};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||
use imphnen_utils::{common_response, success_list_response, success_response, validate_request};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
};
|
||||
|
||||
pub struct EventsService;
|
||||
|
||||
impl EventsService {
|
||||
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<EventsListItemDto> = data.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(EventsQueryDto::from)
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<EventsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(EventsQueryDto::from)
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_by_id(id).await {
|
||||
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: EventsDetailItemDto {
|
||||
id: event.id.id.to_raw(),
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
start_date: event.start_date,
|
||||
end_date: event.end_date,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.updated_at,
|
||||
location: event.location,
|
||||
},
|
||||
}),
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_by_id(id).await {
|
||||
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: EventsDetailItemDto {
|
||||
id: event.id.id.to_raw(),
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
start_date: event.start_date,
|
||||
end_date: event.end_date,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.updated_at,
|
||||
location: event.location,
|
||||
},
|
||||
}),
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_event(state: &AppState, payload: EventsCreateRequestDto) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::create(payload);
|
||||
match repo.query_create_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
pub async fn create_event(
|
||||
state: &AppState,
|
||||
payload: EventsCreateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::create(payload);
|
||||
match repo.query_create_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_event(state: &AppState, id: String, payload: EventsUpdateRequestDto) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::update(payload, id);
|
||||
match repo.query_update_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
pub async fn update_event(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: EventsUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::update(payload, id);
|
||||
match repo.query_update_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_event(state: &AppState, id: String) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_delete_event(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn delete_event(state: &AppState, id: String) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_delete_event(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ pub async fn post_create_testimonial(
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Json(payload): Json<TestimonialsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
println!("Authenticated User Now: {:?}", authenticated_user);
|
||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct TestimonialsUpdateRequestDto {
|
||||
pub struct TestimonialsListItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String, // Assuming we'll fetch user's full name
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
@@ -45,7 +45,7 @@ pub struct TestimonialsListItemDto {
|
||||
pub struct TestimonialsDetailItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String, // Assuming we'll fetch user's full name
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
@@ -55,7 +55,7 @@ pub struct TestimonialsDetailItemDto {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsQueryDto {
|
||||
pub id: Thing,
|
||||
pub user: UsersSchema, // Change from Thing to UsersSchema
|
||||
pub user: UsersSchema,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
@@ -68,7 +68,7 @@ impl TestimonialsQueryDto {
|
||||
TestimonialsListItemDto {
|
||||
id: self.id.id.to_raw(),
|
||||
user_id: self.user.id.id.to_raw(),
|
||||
user_fullname: self.user.fullname, // Extract fullname from UsersSchema
|
||||
user_fullname: self.user.fullname,
|
||||
role: self.role,
|
||||
content: self.content,
|
||||
created_at: self.created_at,
|
||||
|
||||
@@ -4,6 +4,9 @@ use super::{
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||
use serde_json;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct TestimonialsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -14,17 +17,25 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_testimonial_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
|
||||
let query = ListQueryBuilder::new(&ResourceEnum::Testimonials.to_string())
|
||||
.with_select_fields(vec!["*", "user.* as user"]) // Select user details
|
||||
let now = Instant::now();
|
||||
let query = ListQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||
.with_select_fields(vec!["*", "user.* as user"])
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
let res: Vec<TestimonialsQueryDto> =
|
||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = ResponseListSuccessDto {
|
||||
data: res,
|
||||
meta: None,
|
||||
@@ -32,17 +43,26 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_testimonial_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<TestimonialsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||
.with_id(&id)
|
||||
.with_select_fields(vec!["*", "user.* as user"]); // Select user details
|
||||
.with_condition("is_deleted = false")
|
||||
.with_select_fields(vec!["*", "user.* as user"]);
|
||||
let sql = builder.build();
|
||||
let result: Option<TestimonialsQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match result {
|
||||
Some(testimonial) => {
|
||||
@@ -55,15 +75,23 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<TestimonialsSchema> = db
|
||||
.create(ResourceEnum::Testimonials.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success create testimonial".into()),
|
||||
@@ -71,10 +99,12 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
|
||||
@@ -85,13 +115,19 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
let merged = TestimonialsSchema {
|
||||
created_at: existing.created_at,
|
||||
updated_at: get_iso_date(),
|
||||
user: existing.user.id, // Preserve user ID
|
||||
user: existing.user.id,
|
||||
..data
|
||||
};
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
let record: Option<TestimonialsSchema> =
|
||||
db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success update testimonial".into()),
|
||||
@@ -99,7 +135,9 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let testimonial = self.query_testimonial_by_id(id).await?;
|
||||
if testimonial.is_deleted {
|
||||
@@ -111,6 +149,12 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success delete testimonial".into()),
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::testimonials_dto::{
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing, // Link to app_users table
|
||||
pub user: Thing,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
@@ -28,7 +28,7 @@ impl Default for TestimonialsSchema {
|
||||
),
|
||||
user: make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(), // Placeholder, will be replaced by actual user ID
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
role: String::new(),
|
||||
content: String::new(),
|
||||
|
||||
@@ -4,9 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -22,3 +23,8 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
http-body-util.workspace = true
|
||||
|
||||
@@ -1,14 +1,2 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
pub mod v1;
|
||||
pub use v1::*;
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
use super::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsService,
|
||||
};
|
||||
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
||||
use ::axum::{
|
||||
extract::{Extension, Json, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use imphnen_entities::*;
|
||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
use imphnen_utils::extract_email;
|
||||
use serde_json::json;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/register",
|
||||
request_body = MentorUserRegisterRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 409, description = "Conflict - user already has mentor profile"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors"
|
||||
)]
|
||||
pub async fn post_register_mentor(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Json(dto): Json<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
MentorsService::register_mentor(&app_state, dto).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors",
|
||||
params(
|
||||
("page" = Option<u64>, Query, description = "Page number"),
|
||||
("per_page" = Option<u64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search query"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_list(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadListMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::get_mentor_list(&app_state, meta).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get mentor by ID", body = MentorDetailResponseDto),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_by_id(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadDetailMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::get_mentor_by_id(&app_state, &id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::UpdateMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::update_mentor(&app_state, &id, dto).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/mentors/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Mentor deleted successfully"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::DeleteMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::delete_mentor(&app_state, &id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/verify/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorVerifyRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor verified successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn put_verify_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<MentorVerifyRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::VerifyMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::verify_mentor(&app_state, &id, dto).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me",
|
||||
responses(
|
||||
(status = 200, description = "Current user's mentor profile", body = MentorDetailResponseDto),
|
||||
(status = 401, description = "Unauthorized - invalid token"),
|
||||
(status = 403, description = "Mentor profile not found for current user"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_me(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadOwnMentorProfile],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return (
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "Unauthorized",
|
||||
"message": "Token tidak valid"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_me(&app_state, &email).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update/me",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 401, description = "Unauthorized - invalid token"),
|
||||
(status = 404, description = "Mentor profile not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_mentor_me(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::UpdateOwnMentorProfile],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 400, description = "Bad request - Mentor ID is required for update"),
|
||||
),
|
||||
tag = "Mentors - Admin"
|
||||
)]
|
||||
pub async fn put_update_mentor_no_id() -> Response {
|
||||
imphnen_utils::common_response(
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
"Mentor ID is required for update",
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/status",
|
||||
responses(
|
||||
(status = 200, description = "Mentor application status", body = String),
|
||||
(status = 401, description = "Unauthorized - invalid token"),
|
||||
(status = 403, description = "No mentor application found for current user"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_status(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadOwnMentorStatus],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return (
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "Unauthorized",
|
||||
"message": "Token tidak valid"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_status(&app_state, &email).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
use crate::v1::mentors::MentorSchema;
|
||||
use imphnen_utils::extract_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorListResponseDto {
|
||||
pub id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorDetailWithUserDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
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 expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorDetailResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
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 expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorRegisterResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorUpdateRequestDto {
|
||||
#[validate(length(
|
||||
min = 3,
|
||||
message = "Legal name must be at least 3 characters"
|
||||
))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub legal_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[validate(url(message = "Invalid identity document URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub identity_document_url: Option<String>,
|
||||
#[validate(length(
|
||||
min = 10,
|
||||
max = 15,
|
||||
message = "Phone must be 10-15 characters"
|
||||
))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
#[validate(length(min = 50, message = "Bio must be at least 50 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[validate(url(message = "Invalid LinkedIn URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[validate(url(message = "Invalid GitHub URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[validate(url(message = "Invalid CV URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 industry required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub industries: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 expertise required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expertise: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 language required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub languages: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "Current company required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_company: Option<String>,
|
||||
#[validate(length(min = 1, message = "Current role required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_role: Option<String>,
|
||||
#[validate(range(min = 2, message = "At least 2 years of experience required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub years_of_experience: Option<i32>,
|
||||
#[validate(length(min = 1, message = "At least 1 topic required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub topics_of_interest: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentee level required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentee_level: Option<Vec<String>>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentoring format required"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentoring_formats: Option<Vec<String>>,
|
||||
#[validate(length(
|
||||
min = 5,
|
||||
message = "Availability commitment must be at least 5 characters"
|
||||
))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub availability_commitment: Option<String>,
|
||||
#[validate(range(min = 1, message = "Amount must be at least 1"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentoring_rate_amount: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorUserRegisterRequestDto {
|
||||
#[validate(
|
||||
length(min = 1, message = "Email cannot be empty"),
|
||||
email(message = "Email not valid")
|
||||
)]
|
||||
pub email: String,
|
||||
#[validate(length(
|
||||
min = 8,
|
||||
message = "Password must have at least 8 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "imphnen_iam::auth_dto::validate_password_complexity",
|
||||
message = "Password must include uppercase, lowercase, number, and special character"
|
||||
))]
|
||||
pub password: String,
|
||||
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||
pub fullname: String,
|
||||
#[validate(length(min = 1, message = "Phone number is required"))]
|
||||
pub phone_number: String,
|
||||
#[validate(nested)]
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
#[validate(nested)]
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
#[validate(nested)]
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorRegisterFromTokenRequestDto {
|
||||
#[validate(nested)]
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
#[validate(nested)]
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
#[validate(nested)]
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct IdentityAndVerification {
|
||||
#[validate(length(
|
||||
min = 3,
|
||||
message = "Legal name must be at least 3 characters"
|
||||
))]
|
||||
pub legal_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[validate(url(message = "Invalid identity document URL"))]
|
||||
pub identity_document_url: String,
|
||||
#[validate(length(
|
||||
min = 10,
|
||||
max = 15,
|
||||
message = "Phone must be 10-15 characters"
|
||||
))]
|
||||
pub phone_for_verification: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct ProfessionalProfile {
|
||||
#[validate(length(min = 50, message = "Bio must be at least 50 characters"))]
|
||||
pub bio: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[validate(url(message = "Invalid LinkedIn URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[validate(url(message = "Invalid GitHub URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[validate(url(message = "Invalid CV URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 industry required"))]
|
||||
pub industries: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 expertise required"))]
|
||||
pub expertise: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 language required"))]
|
||||
pub languages: Vec<String>,
|
||||
#[validate(length(min = 1, message = "Current company required"))]
|
||||
pub current_company: String,
|
||||
#[validate(length(min = 1, message = "Current role required"))]
|
||||
pub current_role: String,
|
||||
#[validate(range(min = 2, message = "At least 2 years of experience required"))]
|
||||
pub years_of_experience: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentoringLogistics {
|
||||
#[validate(length(min = 1, message = "At least 1 topic required"))]
|
||||
pub topics_of_interest: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentee level required"))]
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 mentoring format required"))]
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
#[validate(length(
|
||||
min = 5,
|
||||
message = "Availability commitment must be at least 5 characters"
|
||||
))]
|
||||
pub availability_commitment: String,
|
||||
#[validate(range(min = 1, message = "Amount must be at least 1"))]
|
||||
pub mentoring_rate_amount: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate, Default)]
|
||||
pub struct MentoringRate {
|
||||
#[validate(range(min = 1, message = "Amount must be at least 1"))]
|
||||
pub amount: u64,
|
||||
#[validate(length(min = 1, message = "Currency is required"))]
|
||||
pub currency: String,
|
||||
#[validate(length(min = 1, message = "Per duration is required"))]
|
||||
pub per_duration: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorInsertDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Option<Thing>,
|
||||
pub email: Option<String>,
|
||||
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 expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<MentorSchema> for MentorInsertDto {
|
||||
fn from(schema: MentorSchema) -> Self {
|
||||
MentorInsertDto {
|
||||
id: schema.id,
|
||||
user_id: schema.user_id,
|
||||
email: schema.email,
|
||||
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,
|
||||
expertise: schema.expertise,
|
||||
languages: schema.languages,
|
||||
current_company: schema.current_company,
|
||||
current_role: schema.current_role,
|
||||
years_of_experience: schema.years_of_experience,
|
||||
topics_of_interest: schema.topics_of_interest,
|
||||
preferred_mentee_level: schema.preferred_mentee_level,
|
||||
preferred_mentoring_formats: schema.preferred_mentoring_formats,
|
||||
availability_commitment: schema.availability_commitment,
|
||||
mentoring_rate: schema.mentoring_rate,
|
||||
status: schema.status,
|
||||
is_deleted: schema.is_deleted,
|
||||
created_at: schema.created_at,
|
||||
updated_at: schema.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct MentorVerifyRequestDto {
|
||||
#[validate(length(min = 1, message = "Status is required"))]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorDetailQueryDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
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 expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
fullname: dto.fullname,
|
||||
email: dto.email,
|
||||
status: dto.status,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
user_id: extract_id(&dto.user_id),
|
||||
fullname: dto.fullname,
|
||||
email: dto.email,
|
||||
legal_name: dto.legal_name,
|
||||
identity_document_url: dto.identity_document_url,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
current_company: dto.current_company,
|
||||
current_role: dto.current_role,
|
||||
years_of_experience: dto.years_of_experience,
|
||||
topics_of_interest: dto.topics_of_interest,
|
||||
preferred_mentee_level: dto.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto.preferred_mentoring_formats,
|
||||
availability_commitment: dto.availability_commitment,
|
||||
mentoring_rate: dto.mentoring_rate,
|
||||
status: dto.status,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<MentorSchema> for MentorRegisterResponseDto {
|
||||
fn from(schema: MentorSchema) -> Self {
|
||||
Self {
|
||||
id: schema.id.to_string(),
|
||||
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
|
||||
email: schema.email,
|
||||
status: schema.status,
|
||||
created_at: schema.created_at,
|
||||
updated_at: schema.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MentorDetailWithUserDto> for MentorDetailQueryDto {
|
||||
fn from(dto: MentorDetailWithUserDto) -> Self {
|
||||
MentorDetailQueryDto {
|
||||
id: dto.id,
|
||||
user_id: dto.user_id,
|
||||
fullname: dto.fullname,
|
||||
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,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
current_company: dto.current_company,
|
||||
current_role: dto.current_role,
|
||||
years_of_experience: dto.years_of_experience,
|
||||
topics_of_interest: dto.topics_of_interest,
|
||||
preferred_mentee_level: dto.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto.preferred_mentoring_formats,
|
||||
availability_commitment: dto.availability_commitment,
|
||||
mentoring_rate: dto.mentoring_rate,
|
||||
status: dto.status,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::{get_id, make_thing};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use crate::v1::mentors::{MentorDetailWithUserDto, MentorInsertDto, MentorSchema};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
|
||||
use serde_json::{Map, Value};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct MentorsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> MentorsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_mentor_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<MentorDetailWithUserDto>>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
|
||||
.search_field("legal_name")
|
||||
.select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
let result = builder.build().await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = result.data.into_iter().collect();
|
||||
Ok(ResponseListSuccessDto {
|
||||
data,
|
||||
meta: result.meta,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, email, include_deleted), err)]
|
||||
pub async fn query_mentor_by_email(
|
||||
&self,
|
||||
email: String,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorDetailWithUserDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
||||
.with_where("email", Some(email.clone()))
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
|
||||
if !include_deleted {
|
||||
builder = builder.with_condition("is_deleted = false");
|
||||
}
|
||||
|
||||
let sql = builder.build();
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_by_email' took: {elapsed:.2?}");
|
||||
}
|
||||
let Some(mentor) = mentor_opt else {
|
||||
bail!("Mentor not found");
|
||||
};
|
||||
Ok(mentor)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, include_deleted), err)]
|
||||
pub async fn query_mentor_by_id(
|
||||
&self,
|
||||
id: &Thing,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorDetailWithUserDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
||||
.with_id(get_id(id)?.1)
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
|
||||
if !include_deleted {
|
||||
builder = builder.with_condition("is_deleted = false");
|
||||
}
|
||||
|
||||
let sql = builder.build();
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
let Some(mentor) = mentor_opt else {
|
||||
bail!("Mentor not found in database");
|
||||
};
|
||||
if mentor.is_deleted && !include_deleted {
|
||||
bail!("Mentor has been deleted");
|
||||
}
|
||||
Ok(mentor)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let dto: MentorInsertDto = data.into();
|
||||
let record: Option<MentorSchema> = db
|
||||
.create(ResourceEnum::Mentors.to_string())
|
||||
.content(dto.clone())
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(mentor) => {
|
||||
let id_str = mentor.id.id.to_raw();
|
||||
let _user = format!("{:?}", mentor.user_id);
|
||||
Ok(id_str)
|
||||
}
|
||||
None => {
|
||||
bail!("Failed to create mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let id_ref = &data.id;
|
||||
let record_key = get_id(id_ref)?;
|
||||
let _existing = self.query_mentor_by_id(id_ref, false).await?;
|
||||
|
||||
let mut merged_data_json: Map<String, Value> =
|
||||
serde_json::to_value(data.clone())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to serialize MentorSchema: {}", e))?
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
merged_data_json.remove("id");
|
||||
merged_data_json.remove("user_id");
|
||||
merged_data_json.remove("created_at");
|
||||
|
||||
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
let record: Option<MentorSchema> =
|
||||
db.update(record_key).merge(merged_data_json).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success update mentor".into()),
|
||||
None => {
|
||||
bail!("Failed to update mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_mentor(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let thing = make_thing(ResourceEnum::Mentors.to_string().as_str(), &id);
|
||||
let record_key = get_id(&thing)?;
|
||||
|
||||
let mentor_to_delete_res = self.query_mentor_by_id(&thing, true).await;
|
||||
|
||||
let _mentor_to_delete = match mentor_to_delete_res {
|
||||
Ok(mentor) => {
|
||||
if mentor.is_deleted {
|
||||
bail!("Mentor is already soft deleted");
|
||||
}
|
||||
mentor
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("Mentor has been deleted") {
|
||||
bail!("Mentor is already soft deleted");
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut patch = Map::new();
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success soft delete mentor".into()),
|
||||
None => {
|
||||
bail!("Failed to soft delete mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
use super::{
|
||||
IdentityAndVerification, MentorDetailQueryDto, MentorUpdateRequestDto,
|
||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorSchema {
|
||||
pub id: Thing,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<Thing>,
|
||||
pub email: Option<String>,
|
||||
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 expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for MentorSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
ResourceEnum::Mentors.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user_id: Some(make_thing(
|
||||
ResourceEnum::Users.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
)),
|
||||
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(),
|
||||
expertise: Vec::new(),
|
||||
languages: Vec::new(),
|
||||
current_company: String::new(),
|
||||
current_role: String::new(),
|
||||
years_of_experience: 0,
|
||||
topics_of_interest: Vec::new(),
|
||||
preferred_mentee_level: Vec::new(),
|
||||
preferred_mentoring_formats: Vec::new(),
|
||||
availability_commitment: String::new(),
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: 0,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MentorSchema {
|
||||
pub fn create(
|
||||
identity_and_verification: IdentityAndVerification,
|
||||
professional_profile: ProfessionalProfile,
|
||||
mentoring_logistics: MentoringLogistics,
|
||||
user_id_raw: String,
|
||||
email_str: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Mentors.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)),
|
||||
email: Some(email_str),
|
||||
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,
|
||||
expertise: professional_profile.expertise,
|
||||
languages: professional_profile.languages,
|
||||
current_company: professional_profile.current_company,
|
||||
current_role: professional_profile.current_role,
|
||||
years_of_experience: professional_profile.years_of_experience,
|
||||
topics_of_interest: mentoring_logistics.topics_of_interest,
|
||||
preferred_mentee_level: mentoring_logistics.preferred_mentee_level,
|
||||
preferred_mentoring_formats: mentoring_logistics.preferred_mentoring_formats,
|
||||
availability_commitment: mentoring_logistics.availability_commitment,
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: mentoring_logistics.mentoring_rate_amount,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user_id: Some(dto.user_id),
|
||||
email: dto.email,
|
||||
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,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
current_company: dto.current_company,
|
||||
current_role: dto.current_role,
|
||||
years_of_experience: dto.years_of_experience,
|
||||
topics_of_interest: dto.topics_of_interest,
|
||||
preferred_mentee_level: dto.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto.preferred_mentoring_formats,
|
||||
availability_commitment: dto.availability_commitment,
|
||||
mentoring_rate: dto.mentoring_rate,
|
||||
status: dto.status,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self {
|
||||
// Update fields only if they are Some(value), otherwise preserve current value
|
||||
if let Some(val) = dto.legal_name {
|
||||
self.legal_name = val;
|
||||
}
|
||||
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 {
|
||||
self.industries = val;
|
||||
}
|
||||
if let Some(val) = dto.expertise {
|
||||
self.expertise = val;
|
||||
}
|
||||
if let Some(val) = dto.languages {
|
||||
self.languages = val;
|
||||
}
|
||||
if let Some(val) = dto.current_company {
|
||||
self.current_company = val;
|
||||
}
|
||||
if let Some(val) = dto.current_role {
|
||||
self.current_role = val;
|
||||
}
|
||||
if let Some(val) = dto.years_of_experience {
|
||||
self.years_of_experience = val;
|
||||
}
|
||||
if let Some(val) = dto.topics_of_interest {
|
||||
self.topics_of_interest = val;
|
||||
}
|
||||
if let Some(val) = dto.preferred_mentee_level {
|
||||
self.preferred_mentee_level = val;
|
||||
}
|
||||
if let Some(val) = dto.preferred_mentoring_formats {
|
||||
self.preferred_mentoring_formats = val;
|
||||
}
|
||||
if let Some(val) = dto.availability_commitment {
|
||||
self.availability_commitment = val;
|
||||
}
|
||||
if let Some(val) = dto.mentoring_rate_amount {
|
||||
self.mentoring_rate.amount = val;
|
||||
}
|
||||
|
||||
self.updated_at = get_iso_date();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn update_status(mut self, status: String) -> Self {
|
||||
self.status = status;
|
||||
self.updated_at = get_iso_date();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
use crate::v1::mentors::{
|
||||
MentorDetailQueryDto, MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorRegisterResponseDto, MentorSchema, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsRepository,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
use imphnen_entities::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_iam::{
|
||||
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
use tracing::error;
|
||||
|
||||
pub struct MentorsService;
|
||||
|
||||
impl MentorsService {
|
||||
pub async fn register_mentor(
|
||||
state: &AppState,
|
||||
dto: MentorUserRegisterRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let role_repo = RolesRepository::new(state);
|
||||
let auth_repo = AuthRepository::new(state);
|
||||
|
||||
let user_email = dto.email.clone();
|
||||
let mut _user_to_update: Option<UsersSchema> = None;
|
||||
|
||||
let existing_user_result =
|
||||
user_repo.query_user_by_email(user_email.clone()).await;
|
||||
|
||||
let user_id;
|
||||
let final_user_email = user_email.clone();
|
||||
|
||||
if let Ok(user_detail_query_dto) = existing_user_result {
|
||||
if mentor_repo
|
||||
.query_mentor_by_email(user_email.clone(), false)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return common_response(
|
||||
StatusCode::CONFLICT,
|
||||
"Mentor profile already exists for this user",
|
||||
);
|
||||
}
|
||||
|
||||
let mut user_schema = UsersSchema::from(user_detail_query_dto.clone());
|
||||
|
||||
user_schema.fullname = dto.fullname.clone();
|
||||
user_schema.phone_number = dto.phone_number.clone();
|
||||
user_schema.updated_at = imphnen_utils::get_iso_date();
|
||||
|
||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to hash password during update for {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
);
|
||||
}
|
||||
};
|
||||
user_schema.password = hashed_password;
|
||||
|
||||
let mentor_role = match role_repo
|
||||
.query_role_by_name(RolesEnum::Mentor.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(role) => role,
|
||||
Err(_e) => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
|
||||
}
|
||||
};
|
||||
user_schema.role =
|
||||
imphnen_utils::make_thing(&ResourceEnum::Roles.to_string(), &mentor_role.id);
|
||||
user_schema.is_active = false;
|
||||
|
||||
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
|
||||
error!(
|
||||
"Failed to update existing user {} to mentor role: {}",
|
||||
final_user_email, _err
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
user_id = user_schema.id.clone();
|
||||
} else {
|
||||
let mentor_role = match role_repo
|
||||
.query_role_by_name(RolesEnum::Mentor.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(role) => role,
|
||||
Err(_e) => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
|
||||
}
|
||||
};
|
||||
|
||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to hash password for new user {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let new_user_schema = UsersSchema {
|
||||
id: imphnen_utils::make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
email: dto.email.clone(),
|
||||
fullname: dto.fullname.clone(),
|
||||
password: hashed_password,
|
||||
phone_number: dto.phone_number.clone(),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
role: imphnen_utils::make_thing(
|
||||
&ResourceEnum::Roles.to_string(),
|
||||
&mentor_role.id,
|
||||
),
|
||||
is_active: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
user_id = new_user_schema.id.clone();
|
||||
|
||||
match user_repo.query_create_user(new_user_schema).await {
|
||||
Ok(_) => {}
|
||||
Err(_err) => {
|
||||
error!("Failed to create new user {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
|
||||
|
||||
match auth_repo
|
||||
.query_store_otp(final_user_email.clone(), otp)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let message = format!("your otp code is {otp}");
|
||||
if let Err(_err) =
|
||||
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
|
||||
{
|
||||
error!("Failed to send OTP email to {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
error!("Failed to store OTP for {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mentor_schema = MentorSchema::create(
|
||||
dto.identity_and_verification,
|
||||
dto.professional_profile,
|
||||
dto.mentoring_logistics,
|
||||
user_id.to_raw(),
|
||||
final_user_email.clone(),
|
||||
);
|
||||
|
||||
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
||||
Ok(mentor_profile_id) => {
|
||||
let user_after_mentor_creation_dto = user_repo
|
||||
.query_user_by_email(final_user_email.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut user_after_mentor_creation_schema =
|
||||
UsersSchema::from(user_after_mentor_creation_dto);
|
||||
user_after_mentor_creation_schema = user_after_mentor_creation_schema
|
||||
.update_mentor_id(Some(mentor_profile_id));
|
||||
if let Err(_e) = user_repo
|
||||
.query_update_user(user_after_mentor_creation_schema)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to update user's mentor_id for {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_e.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let response_dto = MentorRegisterResponseDto::from(mentor_schema);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to create mentor profile for {}: {}",
|
||||
final_user_email, _e
|
||||
);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_mentor_list(meta).await {
|
||||
Ok(result) => {
|
||||
let data: Vec<MentorListResponseDto> = result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(MentorDetailQueryDto::from)
|
||||
.map(MentorListResponseDto::from)
|
||||
.collect();
|
||||
success_list_response(ResponseListSuccessDto {
|
||||
data,
|
||||
meta: result.meta,
|
||||
})
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => {
|
||||
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_mentor(
|
||||
state: &AppState,
|
||||
id: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_mentor(state: &AppState, id: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_delete_mentor(id.to_string()).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_me(state: &AppState, email: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => {
|
||||
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(_e) => common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Mentor profile not found for current user",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_mentor_me(
|
||||
state: &AppState,
|
||||
email: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = MentorsRepository::new(state);
|
||||
let existing_mentor =
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::FORBIDDEN, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor = repo
|
||||
.query_mentor_by_email(email.to_string(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mentor_status(state: &AppState, email: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => common_response(StatusCode::OK, &mentor.status),
|
||||
Err(_e) => common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"No mentor application found for current user",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_mentor(
|
||||
state: &AppState,
|
||||
id: &str,
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
schema = schema.update_status(dto.status);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
|
||||
pub mod mentors_controller;
|
||||
pub mod mentors_dto;
|
||||
pub mod mentors_repository;
|
||||
pub mod mentors_schema;
|
||||
pub mod mentors_service;
|
||||
|
||||
pub use mentors_controller::*;
|
||||
pub use mentors_dto::*;
|
||||
pub use mentors_repository::*;
|
||||
pub use mentors_schema::*;
|
||||
pub use mentors_service::*;
|
||||
|
||||
pub fn mentors_router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(get_mentor_list))
|
||||
.route("/register", post(post_register_mentor))
|
||||
.route("/me", get(get_mentor_me))
|
||||
.route("/update/me", put(put_update_mentor_me))
|
||||
.route("/status", get(get_mentor_status))
|
||||
.route("/detail/{id}", get(get_mentor_by_id))
|
||||
.route("/update/{id}", put(put_update_mentor))
|
||||
.route("/update", put(put_update_mentor_no_id))
|
||||
.route("/delete/{id}", delete(delete_mentor))
|
||||
.route("/verify/{id}", put(put(put_verify_mentor)))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod mentors;
|
||||
|
||||
pub fn dimentorin_router() -> Router {
|
||||
Router::new().nest("/mentors", mentors::mentors_router())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{
|
||||
engine::local::Db,
|
||||
engine::any::Any,
|
||||
Surreal,
|
||||
};
|
||||
use surrealdb::{Surreal, engine::any::Any, engine::local::Db};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
pub mod error {
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Response;
|
||||
use axum::Json;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("database error")]
|
||||
Db,
|
||||
#[error("database error: {0}")]
|
||||
Db(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(self.to_string())).into_response()
|
||||
let (status, error_message) = match self {
|
||||
Error::Db(detail) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Database error: {detail}"),
|
||||
),
|
||||
};
|
||||
(status, Json(error_message)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<surrealdb::Error> for Error {
|
||||
fn from(error: surrealdb::Error) -> Self {
|
||||
eprintln!("{error}");
|
||||
Self::Db
|
||||
Self::Db(error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam ={ version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -23,4 +23,6 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
rand_distr.workspace = true
|
||||
rand_distr.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -1,7 +1,3 @@
|
||||
use imphnen_entities::*;
|
||||
use imphnen_libs::*;
|
||||
use imphnen_utils::*;
|
||||
|
||||
pub mod v1;
|
||||
|
||||
pub use imphnen_entities::*;
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::{GachaClaimQueryDto, GachaClaimSchema};
|
||||
use crate::{AppState, ResourceEnum};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::DetailQueryBuilder;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaClaimRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -12,10 +14,12 @@ impl<'a> GachaClaimRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_claim_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<GachaClaimQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string())
|
||||
.with_id(id.clone())
|
||||
@@ -25,21 +29,35 @@ impl<'a> GachaClaimRepository<'a> {
|
||||
let sql = builder.build();
|
||||
let result: Option<GachaClaimQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_gacha_claim_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(claim) if !claim.is_deleted => Ok(claim),
|
||||
_ => bail!("Gacha Claim not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_gacha_claim(
|
||||
&self,
|
||||
data: GachaClaimSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaClaimSchema> = db
|
||||
.create(ResourceEnum::GachaClaims.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_gacha_claim' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Claim".into()),
|
||||
None => bail!("Failed to create Gacha Claim"),
|
||||
|
||||
@@ -2,7 +2,9 @@ use super::{GachaCreditRequestDto, GachaCreditSchema};
|
||||
use crate::{AppState, ResourceEnum};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::make_thing;
|
||||
use std::time::Instant;
|
||||
use surrealdb::Uuid;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaCreditRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -13,28 +15,54 @@ impl<'a> GachaCreditRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, user_id), err)]
|
||||
pub async fn query_by_user_id(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<GachaCreditSchema>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let sql = format!(
|
||||
"SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1",
|
||||
ResourceEnum::GachaCredits.to_string(),
|
||||
ResourceEnum::Users.to_string()
|
||||
ResourceEnum::GachaCredits,
|
||||
ResourceEnum::Users
|
||||
);
|
||||
let result: Vec<GachaCreditSchema> =
|
||||
db.query(sql).bind(("user_id", user_id)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_by_user_id' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(result.into_iter().next())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, user_id), err)]
|
||||
pub async fn query_consume_credit(&self, user_id: String) -> Result<()> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let credit_opt = self.query_by_user_id(user_id).await?;
|
||||
let Some(mut credit) = credit_opt else {
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!(
|
||||
"Query 'query_consume_credit' took: {elapsed:.2?} (no credit to consume)"
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if credit.available_rolls <= 0 {
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!(
|
||||
"Query 'query_consume_credit' took: {elapsed:.2?} (no rolls remaining)"
|
||||
);
|
||||
}
|
||||
bail!("No extra roll credits remaining");
|
||||
}
|
||||
credit.available_rolls -= 1;
|
||||
@@ -45,13 +73,21 @@ impl<'a> GachaCreditRepository<'a> {
|
||||
))
|
||||
.merge(credit)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_consume_credit' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), err)]
|
||||
pub async fn query_add_credit(
|
||||
&self,
|
||||
payload: GachaCreditRequestDto,
|
||||
) -> Result<()> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
|
||||
credit.available_rolls += payload.amount;
|
||||
@@ -63,7 +99,7 @@ impl<'a> GachaCreditRepository<'a> {
|
||||
.merge(credit)
|
||||
.await?;
|
||||
} else {
|
||||
let data = GachaCreditSchema::from(&GachaCreditSchema {
|
||||
let data = GachaCreditSchema {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaCredits.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
@@ -71,12 +107,18 @@ impl<'a> GachaCreditRepository<'a> {
|
||||
user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id),
|
||||
available_rolls: payload.amount,
|
||||
..Default::default()
|
||||
});
|
||||
};
|
||||
let _: Option<GachaCreditSchema> = db
|
||||
.create(&ResourceEnum::GachaCredits.to_string())
|
||||
.create(ResourceEnum::GachaCredits.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
}
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_add_credit' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::QueryListBuilder;
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde_json::{Map, Value};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaItemRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -15,10 +19,12 @@ impl<'a> GachaItemRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_gacha_item_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
|
||||
let now = Instant::now();
|
||||
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
||||
QueryListBuilder::new(
|
||||
&self.state.surrealdb_ws,
|
||||
@@ -30,6 +36,12 @@ impl<'a> GachaItemRepository<'a> {
|
||||
.select_fields(vec!["*"])
|
||||
.build()
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_gacha_item_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = raw_result
|
||||
.data
|
||||
.into_iter()
|
||||
@@ -41,36 +53,54 @@ impl<'a> GachaItemRepository<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let result: Option<GachaItemSchema> = db
|
||||
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_gacha_item_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(item) if !item.is_deleted => Ok(item),
|
||||
_ => bail!("Gacha Item not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_gacha_item(
|
||||
&self,
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaItemSchema> = db
|
||||
.create(ResourceEnum::GachaItems.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_gacha_item' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Item".into()),
|
||||
None => bail!("Failed to create Gacha Item"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_gacha_item(
|
||||
&self,
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(&data.id)?;
|
||||
let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?;
|
||||
@@ -83,13 +113,21 @@ impl<'a> GachaItemRepository<'a> {
|
||||
};
|
||||
let record: Option<GachaItemSchema> =
|
||||
db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_gacha_item' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success update Gacha Item".into()),
|
||||
None => bail!("Failed to update Gacha Item"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
|
||||
let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?;
|
||||
@@ -97,13 +135,20 @@ impl<'a> GachaItemRepository<'a> {
|
||||
bail!("Gacha Item already deleted");
|
||||
}
|
||||
let record_key = get_id(&item.id)?;
|
||||
let record: Option<GachaItemSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
let mut patch = Map::new();
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
let record: Option<GachaItemSchema> = db.update(record_key).merge(patch).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_gacha_item' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success delete Gacha Item".into()),
|
||||
None => bail!("Failed to delete Gacha Item"),
|
||||
Some(_) => Ok("Success soft delete Gacha Item".into()),
|
||||
None => bail!("Failed to soft delete Gacha Item"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,12 @@ impl GachaItemService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = GachaItemRepository::new(state);
|
||||
let schema = GachaItemSchema::from(payload);
|
||||
let schema = GachaItemSchema {
|
||||
id: make_thing(&ResourceEnum::GachaItems.to_string(), &payload.name), // Fixed: Use payload.name or some other identifier
|
||||
name: payload.name,
|
||||
image_url: payload.image_url,
|
||||
..Default::default()
|
||||
};
|
||||
match repo.query_create_gacha_item(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
|
||||
@@ -91,3 +91,32 @@ pub async fn post_execute_gacha_roll(
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/gacha/rolls/delete/{id}",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
params(("id" = String, Path, description = "Gacha Roll ID")),
|
||||
responses(
|
||||
(status = 200, description = "Delete Gacha Roll (soft delete)", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn delete_gacha_roll(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::DeleteGachaRolls],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaRollService::soft_delete_gacha_roll(&state, id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl GachaRollItemDto {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
item: GachaItemDto::from(dto.item.clone()),
|
||||
weight: dto.weight.clone(),
|
||||
weight: dto.weight,
|
||||
quantity: dto.quantity,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at.clone(),
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use super::GachaRollQueryDto;
|
||||
use super::GachaRollSchema;
|
||||
use crate::{AppState, DetailQueryBuilder, ResourceEnum};
|
||||
use crate::{AppState, DetailQueryBuilder, ResourceEnum, get_id, make_thing};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::ListQueryBuilder;
|
||||
|
||||
use rand::prelude::*;
|
||||
use rand::rng;
|
||||
|
||||
use imphnen_utils::get_iso_date;
|
||||
use rand_distr::weighted::WeightedIndex;
|
||||
use serde_json::{Map, Value};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaRollRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -16,46 +20,70 @@ impl<'a> GachaRollRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_roll_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<GachaRollQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string())
|
||||
.with_id(id.clone())
|
||||
.with_condition("is_deleted = false")
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("item");
|
||||
let sql = builder.build();
|
||||
let result: Option<GachaRollQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_gacha_roll_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(roll) if !roll.is_deleted => Ok(roll),
|
||||
_ => bail!("Gacha Roll not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_gacha_roll(
|
||||
&self,
|
||||
data: GachaRollSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaRollSchema> = db
|
||||
.create(ResourceEnum::GachaRolls.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_gacha_roll' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Roll".into()),
|
||||
None => bail!("Failed to create Gacha Roll"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn query_all_active_rolls(&self) -> Result<Vec<GachaRollQueryDto>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = ListQueryBuilder::new(ResourceEnum::GachaRolls.to_string())
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch(Some(vec!["item"]));
|
||||
let sql = builder.build();
|
||||
let table_name = ResourceEnum::GachaRolls.to_string();
|
||||
let sql =
|
||||
format!("SELECT * FROM {table_name} WHERE is_deleted = false FETCH item");
|
||||
let result: Vec<GachaRollQueryDto> = db.query(sql).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_all_active_rolls' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -72,8 +100,36 @@ impl<'a> GachaRollRepository<'a> {
|
||||
return None;
|
||||
}
|
||||
let dist = WeightedIndex::new(&weights).ok()?;
|
||||
let mut rng = rng();
|
||||
let mut rng = rand::rngs::ThreadRng::default();
|
||||
let index = dist.sample(&mut rng);
|
||||
Some(filtered[index].clone())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_soft_delete_gacha_roll(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let roll_id_thing = make_thing(&ResourceEnum::GachaRolls.to_string(), &id);
|
||||
let roll = self.query_gacha_roll_by_id(id.clone()).await?;
|
||||
if roll.is_deleted {
|
||||
bail!("Gacha Roll already deleted");
|
||||
}
|
||||
let record_key = get_id(&roll_id_thing)?;
|
||||
|
||||
let mut patch = Map::new();
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
let record: Option<GachaRollSchema> = db.update(record_key).merge(patch).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_soft_delete_gacha_roll' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success soft delete Gacha Roll".into()),
|
||||
None => bail!("Failed to soft delete Gacha Roll"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,12 @@ impl GachaRollService {
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn soft_delete_gacha_roll(state: &AppState, id: String) -> Response {
|
||||
let repo = GachaRollRepository::new(state);
|
||||
match repo.query_soft_delete_gacha_roll(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-gacha = { version = "0.1.0", path = "../imphnen-gacha" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-middleware = { version = "0.1.0", path = "../imphnen-middleware" }
|
||||
imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" }
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-gacha.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-middleware.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
+79
-23
@@ -1,18 +1,44 @@
|
||||
use imphnen_cms::{
|
||||
events_controller,
|
||||
events_dto::{EventsDetailItemDto, EventsListItemDto},
|
||||
testimonials_controller,
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
};
|
||||
use imphnen_dimentorin::v1::mentors::{
|
||||
mentors_controller,
|
||||
mentors_dto::{
|
||||
IdentityAndVerification, MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorRegisterFromTokenRequestDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
},
|
||||
};
|
||||
use imphnen_gacha::{
|
||||
GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto,
|
||||
GachaRollItemDto, GachaRollRequestDto, gacha_claims, gacha_items, gacha_rolls,
|
||||
};
|
||||
use imphnen_iam::{
|
||||
auth, permissions, roles, users, AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, UsersListItemDto, UsersUpdateRequestDto
|
||||
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
|
||||
AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto,
|
||||
MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto,
|
||||
PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto,
|
||||
RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto,
|
||||
UsersListItemDto, UsersUpdateRequestDto, auth, permissions, roles, users,
|
||||
};
|
||||
use utoipa::{
|
||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||
Modify, OpenApi,
|
||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||
};
|
||||
use imphnen_gacha::{gacha_claims, gacha_items, gacha_rolls, GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto, GachaRollItemDto, GachaRollRequestDto};
|
||||
use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsListItemDto}, testimonials_controller, testimonials_dto::{TestimonialsCreateRequestDto, TestimonialsDetailItemDto, TestimonialsListItemDto, TestimonialsUpdateRequestDto}};
|
||||
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
auth::auth_controller::post_login,
|
||||
auth::auth_controller::post_login_mentor,
|
||||
auth::auth_controller::post_register,
|
||||
auth::auth_controller::post_verify_email,
|
||||
auth::auth_controller::post_resend_otp,
|
||||
@@ -47,16 +73,25 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis
|
||||
gacha_rolls::get_detail_gacha_roll,
|
||||
gacha_rolls::post_create_gacha_roll,
|
||||
gacha_rolls::post_execute_gacha_roll,
|
||||
events_controller::get_event_list,
|
||||
events_controller::get_event_by_id,
|
||||
events_controller::post_create_event,
|
||||
events_controller::patch_update_event,
|
||||
events_controller::delete_event,
|
||||
testimonials_controller::get_testimonial_list,
|
||||
testimonials_controller::get_testimonial_by_id,
|
||||
testimonials_controller::post_create_testimonial,
|
||||
testimonials_controller::patch_update_testimonial,
|
||||
testimonials_controller::delete_testimonial,
|
||||
events_controller::get_event_list,
|
||||
events_controller::get_event_by_id,
|
||||
events_controller::post_create_event,
|
||||
events_controller::patch_update_event,
|
||||
events_controller::delete_event,
|
||||
testimonials_controller::get_testimonial_list,
|
||||
testimonials_controller::get_testimonial_by_id,
|
||||
testimonials_controller::post_create_testimonial,
|
||||
testimonials_controller::patch_update_testimonial,
|
||||
testimonials_controller::delete_testimonial,
|
||||
mentors_controller::get_mentor_list,
|
||||
mentors_controller::get_mentor_by_id,
|
||||
mentors_controller::post_register_mentor,
|
||||
mentors_controller::get_mentor_me,
|
||||
mentors_controller::put_update_mentor_me,
|
||||
mentors_controller::get_mentor_status,
|
||||
mentors_controller::put_update_mentor,
|
||||
mentors_controller::put_verify_mentor,
|
||||
mentors_controller::delete_mentor,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@@ -71,7 +106,7 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis
|
||||
AuthRefreshTokenRequestDto,
|
||||
ResponseSuccessDto<TokenDto>,
|
||||
RolesListItemDto,
|
||||
RolesRequestCreateDto,
|
||||
RolesRequestCreateDto,
|
||||
RolesRequestUpdateDto,
|
||||
PermissionsRequestDto,
|
||||
PermissionsItemDto,
|
||||
@@ -96,13 +131,26 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis
|
||||
ResponseSuccessDto<UsersDetailItemDto>,
|
||||
ResponseListSuccessDto<Vec<PermissionsItemDto>>,
|
||||
ResponseSuccessDto<PermissionsItemDto>,
|
||||
ResponseListSuccessDto<Vec<EventsListItemDto>>,
|
||||
ResponseSuccessDto<EventsDetailItemDto>,
|
||||
ResponseListSuccessDto<Vec<TestimonialsListItemDto>>,
|
||||
ResponseSuccessDto<TestimonialsDetailItemDto>,
|
||||
TestimonialsCreateRequestDto,
|
||||
TestimonialsUpdateRequestDto,
|
||||
MessageResponseDto,
|
||||
ResponseListSuccessDto<Vec<EventsListItemDto>>,
|
||||
ResponseSuccessDto<EventsDetailItemDto>,
|
||||
ResponseListSuccessDto<Vec<TestimonialsListItemDto>>,
|
||||
ResponseSuccessDto<TestimonialsDetailItemDto>,
|
||||
TestimonialsCreateRequestDto,
|
||||
TestimonialsUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto,
|
||||
MentorRegisterFromTokenRequestDto,
|
||||
MentorRegisterResponseDto,
|
||||
MentorListResponseDto,
|
||||
MentorDetailResponseDto,
|
||||
MentorUpdateRequestDto,
|
||||
MentorVerifyRequestDto,
|
||||
IdentityAndVerification,
|
||||
ProfessionalProfile,
|
||||
MentoringLogistics,
|
||||
MentoringRate,
|
||||
ResponseListSuccessDto<Vec<MentorListResponseDto>>,
|
||||
ResponseSuccessDto<MentorDetailResponseDto>,
|
||||
ResponseSuccessDto<MentorRegisterResponseDto>,
|
||||
)
|
||||
),
|
||||
info(
|
||||
@@ -121,6 +169,14 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis
|
||||
modifiers(&SecurityAddon),
|
||||
tags(
|
||||
(name = "Authentication", description = "List of Authentication Endpoints"),
|
||||
(name = "Users", description = "User Management Endpoints"),
|
||||
(name = "Roles", description = "Role Management Endpoints"),
|
||||
(name = "Permissions", description = "Permission Management Endpoints"),
|
||||
(name = "Events", description = "Event Management Endpoints"),
|
||||
(name = "Testimonials", description = "Testimonial Management Endpoints"),
|
||||
(name = "Mentors", description = "Mentor Management Endpoints"),
|
||||
(name = "Mentors - Admin", description = "Mentor Admin Management Endpoints (Admin Access Required)"),
|
||||
(name = "Gacha", description = "Gacha System Endpoints"),
|
||||
)
|
||||
)]
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use imphnen_cms::{
|
||||
events_protected_routes, events_public_routes, testimonials_protected_routes,
|
||||
testimonials_public_routes,
|
||||
};
|
||||
use imphnen_dimentorin::dimentorin_router;
|
||||
use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient};
|
||||
use imphnen_gacha::gacha_router;
|
||||
use imphnen_iam::{iam_protected_routes, iam_public_routes};
|
||||
@@ -32,6 +33,7 @@ pub async fn gateway_service(
|
||||
.merge(iam_protected_routes())
|
||||
.merge(events_protected_routes())
|
||||
.merge(testimonials_protected_routes())
|
||||
.merge(dimentorin_router())
|
||||
.merge(gacha_router())
|
||||
.layer(from_fn(auth_middleware));
|
||||
|
||||
|
||||
+13
-3
@@ -4,9 +4,9 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -22,3 +22,13 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
http-body-util.workspace = true
|
||||
|
||||
@@ -1,34 +1,6 @@
|
||||
use ::surrealdb::Uuid;
|
||||
use imphnen_entities::*;
|
||||
use imphnen_libs::*;
|
||||
use imphnen_utils::*;
|
||||
|
||||
pub mod v1;
|
||||
|
||||
pub use imphnen_entities::*;
|
||||
pub use imphnen_libs::*;
|
||||
pub use imphnen_utils::*;
|
||||
pub use v1::*;
|
||||
|
||||
pub fn create_test_user(
|
||||
email: &str,
|
||||
fullname: &str,
|
||||
is_active: bool,
|
||||
role_id: &str,
|
||||
) -> UsersSchema {
|
||||
UsersSchema {
|
||||
id: make_thing("app_users", &Uuid::new_v4().to_string()),
|
||||
email: email.to_string(),
|
||||
fullname: format!("Randomize {} {}", fullname, rand::random::<u32>()),
|
||||
password: hash_password("secret").unwrap(),
|
||||
is_deleted: false,
|
||||
avatar: None,
|
||||
phone_number: "081234567890".to_string(),
|
||||
is_active,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
role: make_thing("app_roles", role_id),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use super::{
|
||||
AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto,
|
||||
AuthResendOtpRequestDto, AuthService, AuthVerifyEmailRequestDto,
|
||||
};
|
||||
use crate::{v1::AuthLoginResponsetDto, AppState};
|
||||
use crate::{AppState, v1::AuthLoginResponsetDto};
|
||||
use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto};
|
||||
use axum::{response::IntoResponse, Extension, Json};
|
||||
use axum::{Extension, Json, response::IntoResponse};
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
@@ -23,6 +23,24 @@ pub async fn post_login(
|
||||
AuthService::mutation_login(payload, &state).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/auth/login-mentor",
|
||||
request_body = AuthLoginRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor login successful", body = ResponseSuccessDto<AuthLoginResponsetDto>),
|
||||
(status = 401, description = "Mentor login failed", body = MessageResponseDto),
|
||||
(status = 403, description = "Forbidden - Not a mentor", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Authentication"
|
||||
)]
|
||||
pub async fn post_login_mentor(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<AuthLoginRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
AuthService::mutation_mentor_login(payload, &state).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/auth/register",
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
fn validate_password_complexity(password: &str) -> Result<(), ValidationError> {
|
||||
pub fn validate_password_complexity(password: &str) -> Result<(), ValidationError> {
|
||||
let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
|
||||
let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
|
||||
let has_digit = password.chars().any(|c| c.is_ascii_digit());
|
||||
@@ -117,3 +117,8 @@ pub struct AuthSetNewPasswordRequestDto {
|
||||
))]
|
||||
pub password: String,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UserCacheSchema {
|
||||
pub email: String,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use super::AuthOtpSchema;
|
||||
use crate::{AppState, ResourceEnum, UsersDetailQueryDto, make_thing};
|
||||
use super::UserCacheSchema;
|
||||
use crate::{
|
||||
AppState, PermissionsQueryDto, ResourceEnum, RolesDetailQueryDto,
|
||||
UsersDetailQueryDto,
|
||||
};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use chrono::{Duration, Utc};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
pub struct AuthRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
@@ -18,20 +23,26 @@ impl<'a> AuthRepository<'a> {
|
||||
}
|
||||
let table = ResourceEnum::UsersCache.to_string();
|
||||
let user_id = user.email.clone();
|
||||
let id = make_thing(&table, &user_id);
|
||||
let _ = self
|
||||
let permissions: Vec<String> =
|
||||
user.role.permissions.into_iter().map(|p| p.name).collect();
|
||||
let user_cache = UserCacheSchema {
|
||||
email: user_id.clone(),
|
||||
permissions,
|
||||
};
|
||||
|
||||
let _record: Option<UserCacheSchema> = self
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.delete::<Option<UsersDetailQueryDto>>((table.clone(), user_id.clone()))
|
||||
.delete::<Option<UserCacheSchema>>((table.clone(), user_id.clone()))
|
||||
.await?;
|
||||
let mut user_to_store = user.clone();
|
||||
user_to_store.id = id.clone();
|
||||
let record: Option<UsersDetailQueryDto> = self
|
||||
|
||||
let record: Option<UserCacheSchema> = self
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.create((table, user_id))
|
||||
.content(user_to_store)
|
||||
.content(user_cache)
|
||||
.await?;
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success store user data".to_string()),
|
||||
None => bail!("Failed store user data"),
|
||||
@@ -42,13 +53,54 @@ impl<'a> AuthRepository<'a> {
|
||||
&self,
|
||||
email: String,
|
||||
) -> Result<UsersDetailQueryDto> {
|
||||
let user: Option<UsersDetailQueryDto> = self
|
||||
let user_cache: Option<UserCacheSchema> = self
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.select((ResourceEnum::UsersCache.to_string(), email))
|
||||
.select((ResourceEnum::UsersCache.to_string(), email.clone()))
|
||||
.await?;
|
||||
match user {
|
||||
Some(u) => Ok(u),
|
||||
|
||||
match user_cache {
|
||||
Some(cache) => {
|
||||
let permissions_query_dto: Vec<PermissionsQueryDto> = cache
|
||||
.permissions
|
||||
.into_iter()
|
||||
.map(|name| PermissionsQueryDto {
|
||||
id: Thing::from((
|
||||
"app_permissions".to_string(),
|
||||
surrealdb::sql::Id::rand(),
|
||||
)),
|
||||
name,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let role_detail_query_dto = RolesDetailQueryDto {
|
||||
id: Thing::from(("app_roles".to_string(), surrealdb::sql::Id::rand())),
|
||||
name: "CachedRole".to_string(),
|
||||
permissions: permissions_query_dto,
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
};
|
||||
|
||||
Ok(UsersDetailQueryDto {
|
||||
id: Thing::from(("app_users".to_string(), email.clone())),
|
||||
fullname: "Cached User".to_string(),
|
||||
email: cache.email,
|
||||
avatar: None,
|
||||
phone_number: String::new(),
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
password: String::new(),
|
||||
role: role_detail_query_dto,
|
||||
created_at: String::new(),
|
||||
updated_at: String::new(),
|
||||
mentor_id: None,
|
||||
})
|
||||
}
|
||||
None => bail!("No stored user data found"),
|
||||
}
|
||||
}
|
||||
@@ -59,7 +111,6 @@ impl<'a> AuthRepository<'a> {
|
||||
.surrealdb_mem
|
||||
.delete((ResourceEnum::UsersCache.to_string(), email))
|
||||
.await?;
|
||||
dbg!(record.clone());
|
||||
match record {
|
||||
Some(_) => Ok("Success delete stored user".to_string()),
|
||||
None => bail!("Failed delete stored user"),
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use surrealdb::Uuid;
|
||||
use tracing::error;
|
||||
|
||||
pub struct AuthService;
|
||||
|
||||
@@ -49,7 +50,11 @@ impl AuthService {
|
||||
|
||||
let access_token = match encode_access_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate access token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
@@ -59,7 +64,11 @@ impl AuthService {
|
||||
|
||||
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate refresh token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate refresh token",
|
||||
@@ -77,13 +86,116 @@ impl AuthService {
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(_err) = auth_repo.query_store_user(user).await {
|
||||
return common_response(StatusCode::BAD_REQUEST, "User already login");
|
||||
if let Err(err_store) = auth_repo.query_store_user(user.clone()).await {
|
||||
error!(
|
||||
"Failed to store user cache for {}: {}",
|
||||
user.email, err_store
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"User already login or failed to cache",
|
||||
);
|
||||
}
|
||||
|
||||
success_response(response)
|
||||
}
|
||||
Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()),
|
||||
Err(err_find) => {
|
||||
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mutation_mentor_login(
|
||||
payload: AuthLoginRequestDto,
|
||||
state: &AppState,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let auth_repo = AuthRepository::new(state);
|
||||
|
||||
match user_repo.query_user_by_email(payload.email.clone()).await {
|
||||
Ok(user) => {
|
||||
let is_password_correct =
|
||||
verify_password(&payload.password, &user.password).unwrap_or(false);
|
||||
|
||||
if !is_password_correct {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Email or password not correct",
|
||||
);
|
||||
}
|
||||
|
||||
if !user.is_active {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Account not active, please verify your email",
|
||||
);
|
||||
}
|
||||
|
||||
let user_detail = UsersDetailItemDto::from(&user);
|
||||
|
||||
if user_detail.role.name != RolesEnum::Mentor.to_string() {
|
||||
return common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"User does not have mentor privileges",
|
||||
);
|
||||
}
|
||||
|
||||
let access_token = match encode_access_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate access token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate refresh token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate refresh token",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let response = ResponseSuccessDto {
|
||||
data: AuthLoginResponsetDto {
|
||||
user: UsersDetailItemDto::from(&user),
|
||||
token: TokenDto {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(err_store) = auth_repo.query_store_user(user.clone()).await {
|
||||
error!(
|
||||
"Failed to store user cache for {}: {}",
|
||||
user.email, err_store
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"User already login or failed to cache",
|
||||
);
|
||||
}
|
||||
success_response(response)
|
||||
}
|
||||
Err(err_find) => {
|
||||
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +214,10 @@ impl AuthService {
|
||||
.await
|
||||
{
|
||||
Ok(role) => role,
|
||||
Err(_) => return common_response(StatusCode::BAD_REQUEST, "Role Not Found"),
|
||||
Err(_e) => {
|
||||
error!("Failed to retrieve User role during registration: {}", _e);
|
||||
return common_response(StatusCode::BAD_REQUEST, "Role Not Found");
|
||||
}
|
||||
};
|
||||
if user_repo
|
||||
.query_user_by_email(payload.email.clone())
|
||||
@@ -113,7 +228,11 @@ impl AuthService {
|
||||
}
|
||||
let hashed_password = match hash_password(&payload.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to hash password during registration for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
@@ -121,27 +240,34 @@ impl AuthService {
|
||||
}
|
||||
};
|
||||
let new_user = AuthRegisterRequestDto {
|
||||
email: payload.email,
|
||||
email: payload.email.clone(),
|
||||
password: hashed_password,
|
||||
fullname: payload.fullname,
|
||||
phone_number: payload.phone_number,
|
||||
};
|
||||
let otp = generate_otp::OtpManager::generate_otp();
|
||||
match auth_repo
|
||||
.query_store_otp(new_user.email.clone(), otp.clone())
|
||||
.await
|
||||
{
|
||||
match auth_repo.query_store_otp(new_user.email.clone(), otp).await {
|
||||
Ok(_) => {
|
||||
let message = format!("your otp code is {}", otp);
|
||||
if let Err(err) = send_email(&new_user.email, "OTP Verification", &message) {
|
||||
let message = format!("your otp code is {otp}");
|
||||
if let Err(err_send) =
|
||||
send_email(&new_user.email, "OTP Verification", &message)
|
||||
{
|
||||
error!(
|
||||
"Failed to send OTP email to {}: {}",
|
||||
new_user.email, err_send
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&err.to_string(),
|
||||
&err_send.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string());
|
||||
Err(err_store) => {
|
||||
error!("Failed to store OTP for {}: {}", new_user.email, err_store);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&err_store.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &role.id);
|
||||
@@ -159,13 +285,15 @@ impl AuthService {
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
role: role_thing,
|
||||
is_active: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(err) => {
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
|
||||
Err(err_create) => {
|
||||
error!("Failed to create user {}: {}", new_user.email, err_create);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err_create.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,13 +316,22 @@ impl AuthService {
|
||||
let auth_repo = AuthRepository::new(state);
|
||||
let _ = auth_repo.query_get_stored_otp(payload.email.clone()).await;
|
||||
let otp = generate_otp::OtpManager::generate_otp();
|
||||
let message = format!("Your OTP code is {}", otp);
|
||||
let message = format!("Your OTP code is {otp}");
|
||||
match auth_repo.query_store_otp(payload.email.clone(), otp).await {
|
||||
Ok(_) => match send_email(&payload.email, "OTP Verification", &message) {
|
||||
Ok(_) => common_response(StatusCode::OK, "OTP resent successfully"),
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_send) => {
|
||||
error!(
|
||||
"Failed to send OTP email to {}: {}",
|
||||
payload.email, err_send
|
||||
);
|
||||
common_response(StatusCode::BAD_REQUEST, &err_send.to_string())
|
||||
}
|
||||
},
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_store) => {
|
||||
error!("Failed to store OTP for {}: {}", payload.email, err_store);
|
||||
common_response(StatusCode::BAD_REQUEST, &err_store.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,13 +343,14 @@ impl AuthService {
|
||||
}
|
||||
let email = match decode_refresh_token(&payload.refresh_token) {
|
||||
Ok(token) => token.claims.sub,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token");
|
||||
}
|
||||
};
|
||||
let access_token = match encode_access_token(email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!("Failed to generate access token for {}: {}", email, _e);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
@@ -221,7 +359,8 @@ impl AuthService {
|
||||
};
|
||||
let refresh_token = match encode_refresh_token(email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!("Failed to generate refresh token for {}: {}", email, _e);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate refresh token",
|
||||
@@ -248,16 +387,27 @@ impl AuthService {
|
||||
let user_result = user_repo.query_user_by_email(payload.email.clone()).await;
|
||||
let user = match user_result {
|
||||
Ok(user) => user,
|
||||
Err(err) if err.to_string().contains("User not found") => {
|
||||
Err(err_find) if err_find.to_string().contains("User not found") => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||
}
|
||||
Err(err) => {
|
||||
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string());
|
||||
Err(err_other) => {
|
||||
error!(
|
||||
"Error finding user for forgot password {}: {}",
|
||||
payload.email, err_other
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&err_other.to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let token = match encode_reset_password_token(user.email) {
|
||||
let token = match encode_reset_password_token(user.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate reset password token for {}: {}",
|
||||
user.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
@@ -267,12 +417,17 @@ impl AuthService {
|
||||
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: {}/auth/reset-password?token={}",
|
||||
fe_url, token
|
||||
"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) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_send) => {
|
||||
error!(
|
||||
"Failed to send reset password email to {}: {}",
|
||||
payload.email, err_send
|
||||
);
|
||||
common_response(StatusCode::BAD_REQUEST, &err_send.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,33 +443,38 @@ impl AuthService {
|
||||
let email = payload.email.clone();
|
||||
let user = match user_repo.query_user_by_email(email.clone()).await {
|
||||
Ok(user) if !user.is_deleted => user,
|
||||
_ => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||
_ => {
|
||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||
}
|
||||
};
|
||||
let patch = UsersSchema {
|
||||
id: user.id.clone(),
|
||||
is_active: true,
|
||||
..UsersSchema::from(user)
|
||||
..UsersSchema::from(user.clone())
|
||||
};
|
||||
match auth_repo.query_get_stored_otp(email.clone()).await {
|
||||
Ok(stored_otp) => match stored_otp == payload.otp {
|
||||
true => match user_repo.query_update_user(patch).await {
|
||||
Ok(_) => match auth_repo.query_delete_stored_otp(email).await {
|
||||
Ok(_) => match auth_repo.query_delete_stored_otp(email.clone()).await {
|
||||
Ok(_) => common_response(StatusCode::OK, "Email verified successfully"),
|
||||
Err(e) => {
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
Err(e_del) => {
|
||||
error!("Failed to delete OTP for {}: {}", email, e_del);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e_del.to_string())
|
||||
}
|
||||
},
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_update) => {
|
||||
common_response(StatusCode::BAD_REQUEST, &err_update.to_string())
|
||||
}
|
||||
},
|
||||
false => match auth_repo.query_delete_stored_otp(email).await {
|
||||
false => match auth_repo.query_delete_stored_otp(email.clone()).await {
|
||||
Ok(_) => common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP"),
|
||||
Err(e) => common_response(
|
||||
Err(e_del_mismatch) => common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&format!("Failed to delete OTP: {}", e),
|
||||
&format!("Failed to delete OTP: {e_del_mismatch}"),
|
||||
),
|
||||
},
|
||||
},
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_get) => common_response(StatusCode::BAD_REQUEST, &err_get.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,14 +487,15 @@ impl AuthService {
|
||||
}
|
||||
let repo = UsersRepository::new(state);
|
||||
let email = match extract_email_token(payload.token.clone()) {
|
||||
Some(email) => email,
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Invalid or missing token");
|
||||
}
|
||||
};
|
||||
let password = match hash_password(&payload.password) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!("Failed to hash new password for {}: {}", email, _e);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
@@ -343,7 +504,9 @@ impl AuthService {
|
||||
};
|
||||
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"),
|
||||
_ => {
|
||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||
}
|
||||
};
|
||||
let patch = UsersSchema {
|
||||
id: user.id.clone(),
|
||||
@@ -352,7 +515,7 @@ impl AuthService {
|
||||
};
|
||||
match repo.query_update_user(patch).await {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub fn auth_router() -> Router {
|
||||
Router::new()
|
||||
.route("/forgot", post(auth_controller::post_forgot_password))
|
||||
.route("/login", post(auth_controller::post_login))
|
||||
.route("/login-mentor", post(auth_controller::post_login_mentor))
|
||||
.route("/new-password", post(auth_controller::post_new_password))
|
||||
.route("/refresh", post(auth_controller::post_refresh_token))
|
||||
.route("/register", post(auth_controller::post_register))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumIter)]
|
||||
pub enum PermissionsEnum {
|
||||
ReadListUsers,
|
||||
ReadDetailUsers,
|
||||
@@ -28,6 +30,16 @@ pub enum PermissionsEnum {
|
||||
ReadDetailGachaRolls,
|
||||
CreateGachaRolls,
|
||||
ExecuteGachaRolls,
|
||||
DeleteGachaRolls,
|
||||
ReadListMentors,
|
||||
ReadDetailMentors,
|
||||
RegisterMentors,
|
||||
ReadOwnMentorProfile,
|
||||
UpdateOwnMentorProfile,
|
||||
ReadOwnMentorStatus,
|
||||
UpdateMentors,
|
||||
VerifyMentors,
|
||||
DeleteMentors,
|
||||
}
|
||||
|
||||
impl fmt::Display for PermissionsEnum {
|
||||
@@ -59,8 +71,18 @@ impl fmt::Display for PermissionsEnum {
|
||||
PermissionsEnum::ReadDetailGachaRolls => "Read Detail Gacha Rolls",
|
||||
PermissionsEnum::CreateGachaRolls => "Create Gacha Rolls",
|
||||
PermissionsEnum::ExecuteGachaRolls => "Execute Gacha Rolls",
|
||||
PermissionsEnum::DeleteGachaRolls => "Delete Gacha Rolls",
|
||||
PermissionsEnum::ReadListMentors => "Read List Mentors",
|
||||
PermissionsEnum::ReadDetailMentors => "Read Detail Mentors",
|
||||
PermissionsEnum::RegisterMentors => "Register Mentors",
|
||||
PermissionsEnum::ReadOwnMentorProfile => "Read Own Mentor Profile",
|
||||
PermissionsEnum::UpdateOwnMentorProfile => "Update Own Mentor Profile",
|
||||
PermissionsEnum::ReadOwnMentorStatus => "Read Own Mentor Status",
|
||||
PermissionsEnum::UpdateMentors => "Update Mentors",
|
||||
PermissionsEnum::VerifyMentors => "Verify Mentors",
|
||||
PermissionsEnum::DeleteMentors => "Delete Mentors",
|
||||
};
|
||||
write!(f, "{}", permission_str)
|
||||
write!(f, "{permission_str}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +123,20 @@ impl PermissionsEnum {
|
||||
}
|
||||
PermissionsEnum::CreateGachaRolls => "18e36c63-fcb7-4877-b911-c5aa611e878f",
|
||||
PermissionsEnum::ExecuteGachaRolls => "14c6a1cd-5c63-4643-89b5-b1a5f9920cc0",
|
||||
PermissionsEnum::DeleteGachaRolls => "12345678-ABCD-EFAB-CDEF-0123456789AB",
|
||||
PermissionsEnum::ReadListMentors => "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
|
||||
PermissionsEnum::ReadDetailMentors => "b2c3d4e5-6f78-9012-bcde-f23456789012",
|
||||
PermissionsEnum::RegisterMentors => "c3d4e5f6-7890-1234-cdef-345678901234",
|
||||
PermissionsEnum::ReadOwnMentorProfile => {
|
||||
"d4e5f6a7-8901-2345-def0-456789012345"
|
||||
}
|
||||
PermissionsEnum::UpdateOwnMentorProfile => {
|
||||
"e5f6a7b8-9012-3456-ef01-567890123456"
|
||||
}
|
||||
PermissionsEnum::ReadOwnMentorStatus => "f6a7b8c9-0123-4567-f012-678901234567",
|
||||
PermissionsEnum::UpdateMentors => "a7b8c9d0-1234-5678-0123-789012345678",
|
||||
PermissionsEnum::VerifyMentors => "b8c9d0e1-2345-6789-1234-890123456789",
|
||||
PermissionsEnum::DeleteMentors => "c9d0e1f2-3456-7890-2345-901234567890",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::PermissionsEnum;
|
||||
use crate::{common_response, extract_email, AppState, AuthRepository};
|
||||
use crate::{AppState, AuthRepository, common_response, extract_email};
|
||||
use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Response,
|
||||
@@ -29,14 +29,16 @@ pub async fn permissions_guard(
|
||||
let role = raw_user.role;
|
||||
let role_permissions: Vec<String> =
|
||||
role.permissions.into_iter().map(|perm| perm.name).collect();
|
||||
let has_all_permissions = required_permissions
|
||||
.iter()
|
||||
.all(|required| role_permissions.contains(&required.to_string()));
|
||||
if !has_all_permissions {
|
||||
return Err(common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"You don't have the required permissions",
|
||||
));
|
||||
|
||||
for required in &required_permissions {
|
||||
let required_str = required.to_string();
|
||||
if !role_permissions.contains(&required_str) {
|
||||
eprintln!(" MISSING REQUIRED PERMISSION: {required_str}");
|
||||
return Err(common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"You don't have the required permissions",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, extract_id};
|
||||
use serde_json;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct PermissionsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -14,10 +17,12 @@ impl<'a> PermissionsRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_permission_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<PermissionsItemDto>>> {
|
||||
let now = Instant::now();
|
||||
let raw_result: ResponseListSuccessDto<Vec<PermissionsSchema>> =
|
||||
QueryListBuilder::new(
|
||||
&self.state.surrealdb_ws,
|
||||
@@ -30,6 +35,13 @@ impl<'a> PermissionsRepository<'a> {
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_permission_list' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let transformed_data = raw_result
|
||||
.data
|
||||
.into_iter()
|
||||
@@ -42,25 +54,41 @@ impl<'a> PermissionsRepository<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_permission_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<PermissionsSchema> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let result: Option<PermissionsSchema> = db
|
||||
.select((ResourceEnum::Permissions.to_string(), id.clone()))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_permission_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(permission) if !permission.is_deleted => Ok(permission),
|
||||
_ => bail!("Permission not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn transformed_query_permission_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<PermissionsItemDto> {
|
||||
let now = Instant::now();
|
||||
let raw_result = self.query_permission_by_id(id.clone()).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'transformed_query_permission_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
let transformed_data = PermissionsItemDto {
|
||||
id: extract_id(&raw_result.id),
|
||||
name: raw_result.name,
|
||||
@@ -70,43 +98,60 @@ impl<'a> PermissionsRepository<'a> {
|
||||
Ok(transformed_data)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, name), err)]
|
||||
pub async fn query_permission_by_name(
|
||||
&self,
|
||||
name: String,
|
||||
) -> Result<PermissionsSchema> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Permissions.to_string())
|
||||
.with_where("name")
|
||||
.where_value(name.clone())
|
||||
.with_where("name", Some(name.clone()))
|
||||
.with_select_fields(vec!["*"]);
|
||||
let sql = builder.build();
|
||||
let result: Option<PermissionsSchema> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_permission_by_name' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(permission) => Ok(permission),
|
||||
None => bail!("Permission not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_permission(
|
||||
&self,
|
||||
data: PermissionsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<PermissionsSchema> = db
|
||||
.create(ResourceEnum::Permissions.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_permission' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create permission".into()),
|
||||
None => bail!("Failed to create permission"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_permission(
|
||||
&self,
|
||||
data: PermissionsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(&data.id)?;
|
||||
let existing = self.query_permission_by_id(data.id.id.to_raw()).await?;
|
||||
@@ -119,13 +164,21 @@ impl<'a> PermissionsRepository<'a> {
|
||||
};
|
||||
let record: Option<PermissionsSchema> =
|
||||
db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_permission' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success update permission".into()),
|
||||
None => bail!("Failed to update permission"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_permission(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let permission_id = make_thing(&ResourceEnum::Permissions.to_string(), &id);
|
||||
let permission = self
|
||||
@@ -139,6 +192,12 @@ impl<'a> PermissionsRepository<'a> {
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_permission' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success delete permission".into()),
|
||||
None => bail!("Failed to delete permission"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
common_response, make_thing, success_list_response, success_response,
|
||||
validate_request, AppState, MetaRequestDto, PermissionsRepository,
|
||||
PermissionsSchema, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
AppState, MetaRequestDto, PermissionsRepository, PermissionsSchema, ResourceEnum,
|
||||
ResponseListSuccessDto, ResponseSuccessDto, common_response, make_thing,
|
||||
success_list_response, success_response, validate_request,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
|
||||
@@ -64,3 +64,16 @@ pub struct RolesDetailQueryDto {
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for RolesDetailQueryDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Thing::from(("".to_string(), surrealdb::sql::Id::Number(0))),
|
||||
name: String::new(),
|
||||
permissions: Vec::new(),
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pub enum RolesEnum {
|
||||
Admin,
|
||||
User,
|
||||
Staff,
|
||||
Mentor, // Added Mentor role
|
||||
}
|
||||
|
||||
impl fmt::Display for RolesEnum {
|
||||
@@ -13,7 +14,8 @@ impl fmt::Display for RolesEnum {
|
||||
RolesEnum::Admin => "Admin",
|
||||
RolesEnum::User => "User",
|
||||
RolesEnum::Staff => "Staff",
|
||||
RolesEnum::Mentor => "Mentor", // Added Mentor role
|
||||
};
|
||||
write!(f, "{}", roles_str)
|
||||
write!(f, "{roles_str}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder};
|
||||
use serde_json;
|
||||
use std::time::Instant;
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct RolesRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -19,10 +22,12 @@ impl<'a> RolesRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_role_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<RolesListItemDto>>> {
|
||||
let now = Instant::now();
|
||||
let result: ResponseListSuccessDto<Vec<RolesSchema>> = QueryListBuilder::new(
|
||||
&self.state.surrealdb_ws,
|
||||
&ResourceEnum::Roles.to_string(),
|
||||
@@ -31,6 +36,12 @@ impl<'a> RolesRepository<'a> {
|
||||
.search_field("name")
|
||||
.build()
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_role_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = result
|
||||
.data
|
||||
.into_iter()
|
||||
@@ -42,48 +53,71 @@ impl<'a> RolesRepository<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, name), err)]
|
||||
pub async fn query_role_by_name(
|
||||
&self,
|
||||
name: String,
|
||||
) -> Result<RolesDetailItemDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string())
|
||||
.with_where("name")
|
||||
.where_value(name.clone())
|
||||
.with_where("name", Some(name.clone()))
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("permissions");
|
||||
let sql = builder.build();
|
||||
let result: Option<RolesDetailQueryDto> = builder
|
||||
.apply_bindings(db.query(sql).bind(("name", name)))
|
||||
.await?
|
||||
.take(0)?;
|
||||
let role = match result {
|
||||
Some(r) if !r.is_deleted => r,
|
||||
_ => bail!("Role not found"),
|
||||
};
|
||||
let mut response = builder.apply_bindings(db.query(sql)).await?;
|
||||
|
||||
let result_vec: Vec<RolesDetailQueryDto> = response.take(0).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to take result from response: {:?}", e)
|
||||
})?;
|
||||
|
||||
let role = result_vec
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Role not found"))?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_role_by_name' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(RolesDetailItemDto::from(&role))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_role_by_id(&self, id: String) -> Result<RolesDetailItemDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let thing_id = make_thing(&ResourceEnum::Roles.to_string(), &id);
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string())
|
||||
.with_id(&id)
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("permissions");
|
||||
|
||||
let sql = builder.build();
|
||||
let sql_debug = sql.to_string(); // Move this line here
|
||||
let result: Option<RolesDetailQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_role_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
let role = match result {
|
||||
Some(r) if !r.is_deleted => r,
|
||||
_ => bail!("Role not found"),
|
||||
_ => bail!("Role not found sql: {} id: {}", sql_debug, thing_id),
|
||||
};
|
||||
Ok(RolesDetailItemDto::from(&role))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), err)]
|
||||
pub async fn query_create_role(
|
||||
&self,
|
||||
payload: RolesRequestCreateDto,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let role_id = Uuid::new_v4().to_string();
|
||||
let permission_things: Vec<Thing> = payload
|
||||
@@ -103,14 +137,22 @@ impl<'a> RolesRepository<'a> {
|
||||
.create((&ResourceEnum::Roles.to_string(), role_id))
|
||||
.content(role)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_role' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok("Role with permissions created successfully".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, data), err)]
|
||||
pub async fn query_update_role(
|
||||
&self,
|
||||
id: String,
|
||||
data: RolesRequestUpdateDto,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let existing = self.query_role_by_id(id.clone()).await?;
|
||||
if existing.is_deleted {
|
||||
@@ -119,13 +161,21 @@ impl<'a> RolesRepository<'a> {
|
||||
let merged = RolesSchema::update(data, id.clone(), existing);
|
||||
let record: Option<RolesSchema> =
|
||||
db.update(get_id(&merged.id)?).content(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_role' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success update role".into()),
|
||||
None => bail!("Failed to update role"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_role(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let role_id = make_thing(&ResourceEnum::Roles.to_string(), &id);
|
||||
let role = self.query_role_by_id(role_id.id.to_raw()).await?;
|
||||
@@ -137,6 +187,12 @@ impl<'a> RolesRepository<'a> {
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_role' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success delete role".into()),
|
||||
None => bail!("Failed to delete role"),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::{RolesDetailItemDto, RolesDetailQueryDto};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
lazy_static! {
|
||||
static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap();
|
||||
static ref PASSWORD_REGEX: regex::Regex =
|
||||
regex::Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
@@ -34,7 +34,7 @@ pub struct UsersCreateRequestDto {
|
||||
message = "Password must have at least 8 characters"
|
||||
))]
|
||||
#[validate(regex(
|
||||
path = "PASSWORD_REGEX",
|
||||
path = "*PASSWORD_REGEX",
|
||||
message = "Password must include uppercase, lowercase, number, and special character"
|
||||
))]
|
||||
pub password: String,
|
||||
@@ -62,6 +62,7 @@ pub struct UsersUpdateRequestDto {
|
||||
min = 8,
|
||||
message = "Password must have at least 8 characters"
|
||||
))]
|
||||
pub password: String,
|
||||
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||
pub fullname: String,
|
||||
#[validate(length(
|
||||
@@ -103,7 +104,7 @@ impl UsersDetailItemDto {
|
||||
email: dto.email.clone(),
|
||||
avatar: dto.avatar.clone(),
|
||||
phone_number: dto.phone_number.clone(),
|
||||
is_active: dto.is_active.clone(),
|
||||
is_active: dto.is_active,
|
||||
gender: dto.gender.clone(),
|
||||
birthdate: dto.birthdate.clone(),
|
||||
created_at: dto.created_at.clone(),
|
||||
@@ -169,18 +170,20 @@ pub struct UsersDetailQueryDto {
|
||||
pub role: RolesDetailQueryDto,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub mentor_id: Option<Thing>,
|
||||
}
|
||||
|
||||
impl UsersDetailQueryDto {
|
||||
pub fn from(&self) -> Self {
|
||||
Self {
|
||||
id: self.id.clone(),
|
||||
role: RolesDetailQueryDto::from(self.role.clone()),
|
||||
role: self.role.clone(),
|
||||
fullname: self.fullname.clone(),
|
||||
email: self.email.clone(),
|
||||
avatar: self.avatar.clone(),
|
||||
phone_number: self.phone_number.clone(),
|
||||
is_active: self.is_active,
|
||||
mentor_id: self.mentor_id.clone(),
|
||||
gender: self.gender.clone(),
|
||||
is_deleted: self.is_deleted,
|
||||
password: self.password.clone(),
|
||||
@@ -190,3 +193,24 @@ impl UsersDetailQueryDto {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&UsersDetailItemDto> for UsersDetailQueryDto {
|
||||
fn from(dto: &UsersDetailItemDto) -> Self {
|
||||
Self {
|
||||
id: crate::make_thing(&imphnen_libs::ResourceEnum::Users.to_string(), &dto.id),
|
||||
fullname: dto.fullname.clone(),
|
||||
email: dto.email.clone(),
|
||||
avatar: dto.avatar.clone(),
|
||||
phone_number: dto.phone_number.clone(),
|
||||
is_active: dto.is_active,
|
||||
is_deleted: false,
|
||||
gender: dto.gender.clone(),
|
||||
birthdate: dto.birthdate.clone(),
|
||||
password: String::new(),
|
||||
role: RolesDetailQueryDto::default(),
|
||||
created_at: dto.created_at.clone(),
|
||||
updated_at: dto.updated_at.clone(),
|
||||
mentor_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ use super::{UsersDetailQueryDto, UsersListItemDto, UsersListQueryDto, UsersSchem
|
||||
use crate::{
|
||||
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id, make_thing,
|
||||
};
|
||||
use surrealdb::sql::Thing;
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder};
|
||||
use serde_json;
|
||||
use std::time::Instant;
|
||||
use surrealdb::{Surreal, engine::remote::ws::Client};
|
||||
|
||||
|
||||
|
||||
|
||||
pub struct UsersRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
@@ -30,10 +36,12 @@ impl<'a> UsersRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
|
||||
pub async fn query_user_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<UsersListItemDto>>> {
|
||||
let now = Instant::now();
|
||||
let result: ResponseListSuccessDto<Vec<UsersListQueryDto>> =
|
||||
QueryListBuilder::new(
|
||||
&self.state.surrealdb_ws,
|
||||
@@ -46,6 +54,14 @@ impl<'a> UsersRepository<'a> {
|
||||
.fetch_fields(vec!["role", "role.permissions"])
|
||||
.build()
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_user_list' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let data = result
|
||||
.data
|
||||
.into_iter()
|
||||
@@ -57,70 +73,103 @@ impl<'a> UsersRepository<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
pub async fn query_user_by_email(
|
||||
&self,
|
||||
email: String,
|
||||
) -> Result<UsersDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Users.to_string())
|
||||
.with_where("email")
|
||||
.where_value(email.clone())
|
||||
.with_where("email", Some(email.clone()))
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("role")
|
||||
.with_fetch("role.permissions");
|
||||
let sql = builder.build();
|
||||
let user_opt: Option<UsersDetailQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_user_by_email' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let Some(user) = user_opt else {
|
||||
bail!("User not found");
|
||||
};
|
||||
if user.is_deleted {
|
||||
bail!("User not found");
|
||||
}
|
||||
if user.role.is_deleted {
|
||||
if user.role.updated_at.is_none() || user.role.is_deleted {
|
||||
bail!("User not found");
|
||||
}
|
||||
Ok(UsersDetailQueryDto::from(&user))
|
||||
}
|
||||
|
||||
pub async fn query_user_by_id(&self, id: String) -> Result<UsersDetailQueryDto> {
|
||||
|
||||
|
||||
|
||||
pub async fn query_user_by_id(&self, id: &Thing) -> Result<UsersDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Users.to_string())
|
||||
.with_id(&id)
|
||||
.with_id(&id.id.to_raw())
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("role")
|
||||
.with_fetch("role.permissions");
|
||||
let sql = builder.build();
|
||||
let result: Option<UsersDetailQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_user_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let Some(user) = result else {
|
||||
bail!("User not found");
|
||||
bail!("User not found in database");
|
||||
};
|
||||
if user.is_deleted {
|
||||
bail!("User not found");
|
||||
}
|
||||
if user.role.is_deleted {
|
||||
bail!("User not found");
|
||||
bail!("User's role has been deleted");
|
||||
}
|
||||
Ok(UsersDetailQueryDto::from(&user))
|
||||
}
|
||||
|
||||
|
||||
pub async fn query_create_user(&self, data: UsersSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<UsersSchema> = db
|
||||
.create(ResourceEnum::Users.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_user' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success create user".into()),
|
||||
None => bail!("Failed to create user"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn query_update_user(&self, data: UsersSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(&data.id)?;
|
||||
let existing = self.query_user_by_id(data.id.id.to_raw()).await?;
|
||||
let existing = self.query_user_by_id(&data.id).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("User already deleted");
|
||||
}
|
||||
@@ -136,15 +185,25 @@ impl<'a> UsersRepository<'a> {
|
||||
..data.clone()
|
||||
};
|
||||
let record: Option<UsersSchema> = db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_user' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success update user".into()),
|
||||
None => bail!("Failed to update user"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn query_delete_user(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let user = self.query_user_by_id(id).await?;
|
||||
let user = self.query_user_by_id(&make_thing(&ResourceEnum::Users.to_string(), &id)).await?;
|
||||
if user.is_deleted {
|
||||
bail!("User not found");
|
||||
}
|
||||
@@ -153,9 +212,18 @@ impl<'a> UsersRepository<'a> {
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_user' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success delete user".into()),
|
||||
None => bail!("Failed to delete user"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{UsersCreateRequestDto, UsersDetailQueryDto, UsersUpdateRequestDto};
|
||||
use imphnen_libs::{ResourceEnum, hash_password};
|
||||
use imphnen_utils::extract_id;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
@@ -10,11 +11,16 @@ pub struct UsersSchema {
|
||||
pub fullname: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<String>,
|
||||
pub phone_number: String,
|
||||
pub is_active: bool,
|
||||
pub is_deleted: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentor_id: Option<Thing>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub birthdate: Option<String>,
|
||||
pub role: Thing,
|
||||
pub created_at: String,
|
||||
@@ -35,6 +41,10 @@ impl Default for UsersSchema {
|
||||
phone_number: String::new(),
|
||||
is_active: false,
|
||||
is_deleted: false,
|
||||
mentor_id: Some(make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
)),
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
role: make_thing(
|
||||
@@ -57,12 +67,18 @@ impl UsersSchema {
|
||||
phone_number: dto.phone_number,
|
||||
is_active: dto.is_active,
|
||||
is_deleted: dto.is_deleted,
|
||||
mentor_id: Some(dto.mentor_id.unwrap_or_else(|| {
|
||||
make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
)
|
||||
})),
|
||||
gender: dto.gender,
|
||||
birthdate: dto.birthdate,
|
||||
password: dto.password,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
role: make_thing(&ResourceEnum::Roles.to_string(), &dto.role.id.id.to_raw()),
|
||||
role: make_thing(&ResourceEnum::Roles.to_string(), &extract_id(&dto.role.id)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +111,10 @@ impl UsersSchema {
|
||||
password,
|
||||
phone_number: user.phone_number,
|
||||
is_active: false,
|
||||
mentor_id: Some(make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
)),
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
avatar: None,
|
||||
@@ -112,4 +132,16 @@ impl UsersSchema {
|
||||
..Self::from(dto)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_mentor_id(mut self, mentor_id: Option<String>) -> Self {
|
||||
self.mentor_id = match mentor_id {
|
||||
Some(id) => Some(make_thing(&ResourceEnum::Users.to_string(), &id)),
|
||||
None => Some(make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
)),
|
||||
};
|
||||
self.updated_at = get_iso_date();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ use crate::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, UsersRepository, UsersSchema,
|
||||
};
|
||||
use crate::{
|
||||
ResourceEnum, ResponseSuccessDto, common_response, extract_email, make_thing,
|
||||
success_list_response, success_response, validate_request,
|
||||
ResponseSuccessDto, common_response, extract_email, success_list_response,
|
||||
success_response, validate_request,
|
||||
};
|
||||
use axum::http::HeaderMap;
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{hash_password, verify_password};
|
||||
use imphnen_libs::{ResourceEnum, hash_password, verify_password};
|
||||
use imphnen_utils::make_thing;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
pub struct UsersService;
|
||||
|
||||
@@ -31,8 +34,12 @@ impl UsersService {
|
||||
}
|
||||
|
||||
pub async fn get_user_by_id(state: &AppState, id: String) -> Response {
|
||||
if Uuid::parse_str(&id).is_err() {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||
}
|
||||
let repo = UsersRepository::new(state);
|
||||
match repo.query_user_by_id(id).await {
|
||||
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||
match repo.query_user_by_id(&thing_id).await {
|
||||
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: UsersDetailItemDto::from(&user),
|
||||
}),
|
||||
@@ -84,6 +91,9 @@ impl UsersService {
|
||||
id: String,
|
||||
user: UsersUpdateRequestDto,
|
||||
) -> Response {
|
||||
if Uuid::parse_str(&id).is_err() {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||
}
|
||||
let repo = UsersRepository::new(state);
|
||||
if let Err((status, message)) = validate_request(&user) {
|
||||
return common_response(status, &message);
|
||||
@@ -124,9 +134,12 @@ impl UsersService {
|
||||
id: String,
|
||||
payload: UsersActiveInactiveRequestDto,
|
||||
) -> Response {
|
||||
if Uuid::parse_str(&id).is_err() {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||
}
|
||||
let repo = UsersRepository::new(state);
|
||||
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||
match repo.query_user_by_id(thing_id.id.to_raw()).await {
|
||||
match repo.query_user_by_id(&thing_id).await {
|
||||
Ok(user) if !user.is_deleted => {
|
||||
let patch = UsersSchema {
|
||||
id: user.id.clone(),
|
||||
@@ -186,9 +199,28 @@ impl UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_user(state: &AppState, id: String) -> Response {
|
||||
pub async fn get_user_by_mentor_id(
|
||||
state: &AppState,
|
||||
mentor_id: String,
|
||||
) -> Response {
|
||||
let repo = UsersRepository::new(state);
|
||||
if repo.query_user_by_id(id.clone()).await.is_err() {
|
||||
let thing_id = make_thing(&ResourceEnum::Mentors.to_string(), &mentor_id);
|
||||
match repo.query_user_by_id(&thing_id).await {
|
||||
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: UsersDetailItemDto::from(&user),
|
||||
}),
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_user(state: &AppState, id: String) -> Response {
|
||||
if Uuid::parse_str(&id).is_err() {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Invalid User ID format");
|
||||
}
|
||||
let repo = UsersRepository::new(state);
|
||||
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||
if repo.query_user_by_id(&thing_id).await.is_err() {
|
||||
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||
}
|
||||
match repo.query_delete_user(id).await {
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-entities = { path = "../imphnen-entities" }
|
||||
imphnen-entities.workspace = true
|
||||
log.workspace = true
|
||||
axum.workspace = true
|
||||
tokio.workspace = true
|
||||
@@ -15,4 +15,3 @@ chrono.workspace = true
|
||||
surrealdb.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
dotenvy.workspace = true
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use argon2::{
|
||||
password_hash::{
|
||||
rand_core::OsRng, Error, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||
SaltString,
|
||||
},
|
||||
Argon2,
|
||||
password_hash::{
|
||||
Error, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
|
||||
rand_core::OsRng,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String, Error> {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use crate::{load_env, surrealdb_init_mem, surrealdb_init_ws, Env};
|
||||
use crate::{Env, load_env, surrealdb_init_mem, surrealdb_init_ws};
|
||||
use axum::{Router, serve};
|
||||
use imphnen_entities::{SurrealMemClient, SurrealWsClient};
|
||||
use log::{debug, error, info};
|
||||
use std::{future::Future, net::SocketAddr};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
|
||||
pub async fn axum_init<F, Fut>(router_fn: F)
|
||||
where
|
||||
F: FnOnce(SurrealWsClient, SurrealMemClient) -> Fut,
|
||||
@@ -13,24 +11,19 @@ where
|
||||
{
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
info!("Environment loaded with port: {}", env.port);
|
||||
|
||||
let surrealdb_ws = surrealdb_init_ws().await.expect("Failed surrealdb ws");
|
||||
info!("SurrealDB WS client initialized");
|
||||
|
||||
let surrealdb_mem = surrealdb_init_mem().await.expect("Failed surrealdb mem");
|
||||
info!("SurrealDB MEM client initialized");
|
||||
|
||||
let router = router_fn(surrealdb_ws, surrealdb_mem).await;
|
||||
debug!("Router created successfully");
|
||||
|
||||
let port = env.port;
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||
info!("Listening on http://{}", addr);
|
||||
|
||||
match serve(listener, router).await {
|
||||
Ok(_) => info!("Server stopped gracefully."),
|
||||
Err(err) => error!("Server encountered an error: {}", err),
|
||||
Ok(_) => {}
|
||||
Err(_err) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::env;
|
||||
|
||||
pub fn load_env() {
|
||||
dotenvy::dotenv().ok();
|
||||
if dotenvy::dotenv().is_ok() {}
|
||||
}
|
||||
|
||||
pub struct Env {
|
||||
@@ -13,6 +13,7 @@ pub struct Env {
|
||||
pub surrealdb_password: String,
|
||||
pub surrealdb_namespace: String,
|
||||
pub surrealdb_dbname: String,
|
||||
pub surrealdb_url_ws: String,
|
||||
pub smtp_email: String,
|
||||
pub smtp_password: String,
|
||||
pub smtp_name: String,
|
||||
@@ -31,112 +32,63 @@ impl Env {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
port: env::var("PORT")
|
||||
.unwrap_or_else(|_| {
|
||||
println!("INFO: PORT is not set, using default '3000'.");
|
||||
"3000".to_string()
|
||||
})
|
||||
.unwrap_or_else(|_| "3000".to_string())
|
||||
.parse()
|
||||
.unwrap_or(3000),
|
||||
|
||||
rust_env: env::var("RUST_ENV").unwrap_or_else(|_| {
|
||||
println!("INFO: RUST_ENV is not set, using default 'development'.");
|
||||
"development".to_string()
|
||||
}),
|
||||
rust_env: env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()),
|
||||
|
||||
access_token_secret: env::var("ACCESS_TOKEN_SECRET").unwrap_or_else(|_| {
|
||||
println!("WARNING: ACCESS_TOKEN_SECRET is not set, using fallback!");
|
||||
"default_access_secret".to_string()
|
||||
}),
|
||||
access_token_secret: env::var("ACCESS_TOKEN_SECRET")
|
||||
.unwrap_or_else(|_| "default_access_secret".to_string()),
|
||||
|
||||
refresh_token_secret: env::var("REFRESH_TOKEN_SECRET").unwrap_or_else(|_| {
|
||||
println!("WARNING: REFRESH_TOKEN_SECRET is not set, using fallback!");
|
||||
"default_refresh_secret".to_string()
|
||||
}),
|
||||
refresh_token_secret: env::var("REFRESH_TOKEN_SECRET")
|
||||
.unwrap_or_else(|_| "default_refresh_secret".to_string()),
|
||||
|
||||
surrealdb_url: env::var("SURREALDB_URL").unwrap_or_else(|_| {
|
||||
println!(
|
||||
"WARNING: SURREALDB_URL is not set, using fallback 'http://localhost:8000'!"
|
||||
);
|
||||
"http://localhost:8000".to_string()
|
||||
}),
|
||||
surrealdb_url: env::var("SURREALDB_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8000".to_string()),
|
||||
|
||||
surrealdb_username: env::var("SURREALDB_USERNAME").unwrap_or_else(|_| {
|
||||
println!("WARNING: SURREALDB_USERNAME is not set, using fallback 'root'!");
|
||||
"root".to_string()
|
||||
}),
|
||||
surrealdb_username: env::var("SURREALDB_USERNAME")
|
||||
.unwrap_or_else(|_| "root".to_string()),
|
||||
|
||||
surrealdb_password: env::var("SURREALDB_PASSWORD").unwrap_or_else(|_| {
|
||||
println!("WARNING: SURREALDB_PASSWORD is not set, using fallback!");
|
||||
"password".to_string()
|
||||
}),
|
||||
surrealdb_password: env::var("SURREALDB_PASSWORD")
|
||||
.unwrap_or_else(|_| "password".to_string()),
|
||||
|
||||
surrealdb_namespace: env::var("SURREALDB_NAMESPACE").unwrap_or_else(|_| {
|
||||
println!(
|
||||
"WARNING: SURREALDB_NAMESPACE is not set, using fallback 'namespace'!"
|
||||
);
|
||||
"namespace".to_string()
|
||||
}),
|
||||
surrealdb_namespace: env::var("SURREALDB_NAMESPACE")
|
||||
.unwrap_or_else(|_| "namespace".to_string()),
|
||||
|
||||
surrealdb_dbname: env::var("SURREALDB_DBNAME").unwrap_or_else(|_| {
|
||||
println!("WARNING: SURREALDB_DBNAME is not set, using fallback 'database'!");
|
||||
"database".to_string()
|
||||
}),
|
||||
surrealdb_dbname: env::var("SURREALDB_DBNAME")
|
||||
.unwrap_or_else(|_| "database".to_string()),
|
||||
|
||||
smtp_email: env::var("SMTP_EMAIL").unwrap_or_else(|_| {
|
||||
println!(
|
||||
"WARNING: SMTP_EMAIL is not set, using fallback 'no-reply@example.com'!"
|
||||
);
|
||||
"no-reply@example.com".to_string()
|
||||
}),
|
||||
surrealdb_url_ws: env::var("SURREALDB_URL_WS")
|
||||
.unwrap_or_else(|_| "ws://localhost:8000/rpc".to_string()),
|
||||
|
||||
smtp_password: env::var("SMTP_PASSWORD").unwrap_or_else(|_| {
|
||||
println!("WARNING: SMTP_PASSWORD is not set, using fallback!");
|
||||
"default_smtp_password".to_string()
|
||||
}),
|
||||
smtp_email: env::var("SMTP_EMAIL")
|
||||
.unwrap_or_else(|_| "no-reply@example.com".to_string()),
|
||||
|
||||
smtp_name: env::var("SMTP_NAME").unwrap_or_else(|_| {
|
||||
println!("WARNING: SMTP_NAME is not set, using fallback 'MyApp SMTP'!");
|
||||
"MyApp SMTP".to_string()
|
||||
}),
|
||||
smtp_password: env::var("SMTP_PASSWORD")
|
||||
.unwrap_or_else(|_| "default_smtp_password".to_string()),
|
||||
|
||||
smtp_host: env::var("SMTP_HOST").unwrap_or_else(|_| {
|
||||
println!("WARNING: SMTP_HOST is not set, using fallback 'smtp.gmail.com'!");
|
||||
"smtp.gmail.com".to_string()
|
||||
}),
|
||||
smtp_name: env::var("SMTP_NAME").unwrap_or_else(|_| "MyApp SMTP".to_string()),
|
||||
|
||||
redisdb_url: env::var("REDISDB_URL").unwrap_or_else(|_| {
|
||||
println!("WARNING: REDISDB_URL is not set, using fallback 'localhost'!");
|
||||
"localhost".to_string()
|
||||
}),
|
||||
smtp_host: env::var("SMTP_HOST")
|
||||
.unwrap_or_else(|_| "smtp.gmail.com".to_string()),
|
||||
|
||||
fe_url: env::var("FE_URL").unwrap_or_else(|_| {
|
||||
println!("WARNING: FE_URL is not set, using fallback 'http://localhost'!");
|
||||
"http://localhost".to_string()
|
||||
}),
|
||||
redisdb_url: env::var("REDISDB_URL")
|
||||
.unwrap_or_else(|_| "localhost".to_string()),
|
||||
|
||||
minio_endpoint: env::var("MINIO_ENDPOINT").unwrap_or_else(|_| {
|
||||
println!(
|
||||
"WARNING: MINIO_ENDPOINT is not set, using fallback 'http://localhost:9000'!"
|
||||
);
|
||||
"http://localhost:9000".to_string()
|
||||
}),
|
||||
fe_url: env::var("FE_URL").unwrap_or_else(|_| "http://localhost".to_string()),
|
||||
|
||||
minio_bucket_name: env::var("MINIO_BUCKET_NAME").unwrap_or_else(|_| {
|
||||
println!(
|
||||
"WARNING: MINIO_BUCKET_NAME is not set, using fallback 'default_bucket'!"
|
||||
);
|
||||
"default_bucket".to_string()
|
||||
}),
|
||||
minio_endpoint: env::var("MINIO_ENDPOINT")
|
||||
.unwrap_or_else(|_| "http://localhost:9000".to_string()),
|
||||
|
||||
minio_access_key: env::var("MINIO_ACCESS_KEY").unwrap_or_else(|_| {
|
||||
println!("WARNING: MINIO_ACCESS_KEY is not set, using fallback!");
|
||||
"minio_access".to_string()
|
||||
}),
|
||||
minio_bucket_name: env::var("MINIO_BUCKET_NAME")
|
||||
.unwrap_or_else(|_| "default_bucket".to_string()),
|
||||
|
||||
minio_secret_key: env::var("MINIO_SECRET_KEY").unwrap_or_else(|_| {
|
||||
println!("WARNING: MINIO_SECRET_KEY is not set, using fallback!");
|
||||
"minio_secret".to_string()
|
||||
}),
|
||||
minio_access_key: env::var("MINIO_ACCESS_KEY")
|
||||
.unwrap_or_else(|_| "minio_access".to_string()),
|
||||
|
||||
minio_secret_key: env::var("MINIO_SECRET_KEY")
|
||||
.unwrap_or_else(|_| "minio_secret".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::Env;
|
||||
use axum::http::StatusCode;
|
||||
use chrono::{Duration, TimeDelta, Utc};
|
||||
use jsonwebtoken::{
|
||||
decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation,
|
||||
DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn decode_access_token(
|
||||
let env = Env::new();
|
||||
let secret: String = env.access_token_secret;
|
||||
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
||||
&jwt_token,
|
||||
jwt_token,
|
||||
&DecodingKey::from_secret(secret.as_ref()),
|
||||
&Validation::default(),
|
||||
)
|
||||
@@ -81,7 +81,7 @@ pub fn decode_refresh_token(
|
||||
let env = Env::new();
|
||||
let secret: String = env.refresh_token_secret;
|
||||
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
||||
&jwt_token,
|
||||
jwt_token,
|
||||
&DecodingKey::from_secret(secret.as_ref()),
|
||||
&Validation::default(),
|
||||
)
|
||||
|
||||
@@ -29,13 +29,7 @@ pub fn send_email(
|
||||
.credentials(smtp_credentials)
|
||||
.build();
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => {
|
||||
println!("Email sent successfully to {}", to);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to send email: {}", e);
|
||||
Err(Box::new(e))
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::Env;
|
||||
use crate::SurrealMemClient;
|
||||
use surrealdb::engine::local::Mem;
|
||||
use crate::enviroment::load_env;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::engine::local::Mem;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::{Result, Surreal};
|
||||
use crate::enviroment::load_env;
|
||||
|
||||
pub mod resource;
|
||||
pub use resource::*;
|
||||
@@ -13,7 +13,6 @@ pub async fn surrealdb_init_ws() -> Result<Surreal<any::Any>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
|
||||
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
|
||||
@@ -14,6 +14,7 @@ pub enum ResourceEnum {
|
||||
RolesPermissions,
|
||||
Events,
|
||||
Testimonials,
|
||||
Mentors,
|
||||
}
|
||||
|
||||
impl fmt::Display for ResourceEnum {
|
||||
@@ -31,7 +32,8 @@ impl fmt::Display for ResourceEnum {
|
||||
ResourceEnum::GachaCredits => "app_gacha_credits",
|
||||
ResourceEnum::Events => "app_events",
|
||||
ResourceEnum::Testimonials => "app_testimonials",
|
||||
ResourceEnum::Mentors => "app_mentors",
|
||||
};
|
||||
write!(f, "{}", str)
|
||||
write!(f, "{str}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -4,8 +4,8 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { path = "../imphnen-libs"}
|
||||
imphnen-entities = { path = "../imphnen-entities" }
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
surrealdb.workspace = true
|
||||
axum.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -15,3 +15,7 @@ axum-test.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
validator.workspace = true
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
uuid.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use surrealdb::method::Query;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::method::Query;
|
||||
|
||||
pub fn bind_filter_value(
|
||||
query: Query<'_, any::Any>,
|
||||
val: String,
|
||||
) -> Query<'_, any::Any> {
|
||||
query.bind(("filter", val)) // langsung string aja, udah cukup
|
||||
query.bind(("filter", val))
|
||||
}
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
use crate::decode_access_token;
|
||||
use axum::http::{header::AUTHORIZATION, HeaderMap};
|
||||
use axum::http::{HeaderMap, header::AUTHORIZATION};
|
||||
|
||||
pub fn extract_email(headers: &HeaderMap) -> Option<String> {
|
||||
println!("📥 Received headers: {:?}", headers);
|
||||
|
||||
let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?;
|
||||
println!("🔍 Authorization Header: {}", auth_header);
|
||||
|
||||
let token = auth_header.strip_prefix("Bearer ")?;
|
||||
println!("🧪 Token: {}", token);
|
||||
|
||||
match decode_access_token(token) {
|
||||
Ok(data) => {
|
||||
println!("✅ Token claims: {:?}", data.claims);
|
||||
Some(data.claims.sub)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to decode token: {}", e);
|
||||
None
|
||||
}
|
||||
Ok(data) => Some(data.claims.sub),
|
||||
Err(_e) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use rand::{rng, Rng};
|
||||
use rand::{Rng, rng};
|
||||
|
||||
pub struct OtpManager;
|
||||
|
||||
|
||||
@@ -11,6 +11,5 @@ pub fn get_id(thing: &Thing) -> Result<(&str, &str)> {
|
||||
}
|
||||
|
||||
pub fn extract_id(thing: &Thing) -> String {
|
||||
let id = thing.id.to_raw();
|
||||
id
|
||||
thing.id.to_raw()
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use imphnen_entities::*;
|
||||
use imphnen_libs::*;
|
||||
|
||||
pub mod bind_filter;
|
||||
pub mod extract_email;
|
||||
pub mod generate_date;
|
||||
pub mod generate_otp;
|
||||
pub mod get_id;
|
||||
pub mod make_thing;
|
||||
pub mod mock_test;
|
||||
pub mod query_builder;
|
||||
pub mod query_list;
|
||||
pub mod response_format;
|
||||
pub mod serde_helpers;
|
||||
pub mod validator;
|
||||
|
||||
pub use bind_filter::*;
|
||||
@@ -21,8 +18,11 @@ pub use get_id::*;
|
||||
pub use imphnen_entities::*;
|
||||
pub use imphnen_libs::*;
|
||||
pub use make_thing::*;
|
||||
pub use mock_test::*;
|
||||
pub use query_builder::*;
|
||||
pub use query_list::*;
|
||||
pub use response_format::*;
|
||||
pub use serde_helpers::{
|
||||
option_thing_or_string, serialize_option_thing, serialize_thing,
|
||||
string_or_empty_string, thing_or_string,
|
||||
};
|
||||
pub use validator::*;
|
||||
|
||||
@@ -5,5 +5,5 @@ pub fn make_thing(table: &str, id: &str) -> Thing {
|
||||
}
|
||||
|
||||
pub fn make_thing_str(table: &str, id: &str) -> String {
|
||||
format!("{}:⟨{}⟩", table, id)
|
||||
format!("{table}:⟨{id}⟩")
|
||||
}
|
||||
|
||||
@@ -1,52 +1 @@
|
||||
use crate::AppState;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::{Surreal, engine::local::Mem, opt::auth::Root};
|
||||
use super::Env;
|
||||
|
||||
pub async fn create_mock_app_state() -> AppState {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let db_mem = Surreal::new::<Mem>(()).await.unwrap();
|
||||
let db_ws = any::connect(&env.surrealdb_url).await.unwrap();
|
||||
db_mem.use_ns("test").use_db("test").await.unwrap();
|
||||
db_ws
|
||||
.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db_ws.use_ns("test").use_db("test").await.unwrap();
|
||||
|
||||
AppState {
|
||||
surrealdb_mem: db_mem,
|
||||
surrealdb_ws: db_ws,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cleanup_db() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let _ = app_state
|
||||
.surrealdb_mem
|
||||
.query(
|
||||
r#"
|
||||
REMOVE TABLE app_users;
|
||||
REMOVE TABLE app_roles;
|
||||
REMOVE TABLE app_users_cache;
|
||||
REMOVE TABLE app_otp_cache;
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
let _ = app_state
|
||||
.surrealdb_ws
|
||||
.query(
|
||||
r#"
|
||||
REMOVE TABLE app_users;
|
||||
REMOVE TABLE app_roles;
|
||||
REMOVE TABLE app_users_cache;
|
||||
REMOVE TABLE app_otp_cache;
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use imphnen_libs::MetaRequestDto;
|
||||
use serde_json::{Map, Value};
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::method::Query;
|
||||
use surrealdb::sql::Thing;
|
||||
use surrealdb::engine::any;
|
||||
|
||||
pub struct ListQueryBuilder {
|
||||
resource: String,
|
||||
@@ -57,8 +58,7 @@ impl ListQueryBuilder {
|
||||
if let Some(search) = search {
|
||||
if !search.is_empty() {
|
||||
self.conditions.push(format!(
|
||||
"string::contains(string::lowercase({} ?? ''), string::lowercase($search))",
|
||||
field
|
||||
"string::contains(string::lowercase({field} ?? ''), string::lowercase($search))"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,7 @@ impl ListQueryBuilder {
|
||||
if let (Some(f), Some(v)) = (field, value) {
|
||||
if !v.is_empty() {
|
||||
self.conditions.push(format!(
|
||||
"string::contains(string::join('', [{}]), $filter)",
|
||||
f
|
||||
"string::contains(string::join('', [{f}]), $filter)"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -111,7 +110,7 @@ impl ListQueryBuilder {
|
||||
|
||||
let order_clause = if let Some(field) = self.order_by {
|
||||
let ord = self.order.unwrap_or_else(|| "ASC".into());
|
||||
format!("ORDER BY {} {}", field, ord)
|
||||
format!("ORDER BY {field} {ord}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -145,16 +144,26 @@ impl ListQueryBuilder {
|
||||
fetch_clause
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_count(self) -> String {
|
||||
let where_clause = if !self.conditions.is_empty() {
|
||||
format!("WHERE {}", self.conditions.join(" AND "))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
format!("SELECT count() FROM {} {}", self.resource, where_clause)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DetailQueryBuilder {
|
||||
resource: String,
|
||||
id: Option<String>,
|
||||
thing: Option<String>,
|
||||
where_field: Option<String>,
|
||||
where_value: Option<String>,
|
||||
select_fields: Vec<String>,
|
||||
fetch_fields: Vec<String>,
|
||||
conditions: Vec<String>,
|
||||
bindings: Map<String, Value>,
|
||||
}
|
||||
|
||||
impl DetailQueryBuilder {
|
||||
@@ -163,40 +172,59 @@ impl DetailQueryBuilder {
|
||||
resource: resource.into(),
|
||||
id: None,
|
||||
thing: None,
|
||||
where_field: None,
|
||||
where_value: None,
|
||||
select_fields: vec![],
|
||||
fetch_fields: vec![],
|
||||
conditions: vec![],
|
||||
bindings: Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||
if self.where_field.is_some() || self.thing.is_some() {
|
||||
panic!("Cannot use with_id() after with_where() or with_thing()");
|
||||
if self.thing.is_some() || !self.conditions.is_empty() {
|
||||
panic!(
|
||||
"Cannot use with_id() after with_thing() or with_where()/with_condition()"
|
||||
);
|
||||
}
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_thing(mut self, thing: &Thing) -> Self {
|
||||
if self.id.is_some() || self.where_field.is_some() {
|
||||
panic!("Cannot use with_thing() after with_id() or with_where()");
|
||||
if self.id.is_some() || !self.conditions.is_empty() {
|
||||
panic!(
|
||||
"Cannot use with_thing() after with_id() or with_where()/with_condition()"
|
||||
);
|
||||
}
|
||||
self.thing = Some(thing.to_string()); // app_users:uuid
|
||||
self.resource = thing.tb.clone(); // update resource dari thing
|
||||
self.thing = Some(thing.to_string());
|
||||
self.resource = thing.tb.clone();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_where(mut self, field: impl Into<String>) -> Self {
|
||||
// Modified with_where method
|
||||
pub fn with_where(
|
||||
mut self,
|
||||
field: impl Into<String>,
|
||||
value: Option<impl Into<String>>,
|
||||
) -> Self {
|
||||
if self.id.is_some() || self.thing.is_some() {
|
||||
panic!("Cannot use with_where() after with_id() or with_thing()");
|
||||
}
|
||||
self.where_field = Some(field.into());
|
||||
let field_str = field.into();
|
||||
if let Some(val) = value {
|
||||
// Using a distinct binding key to avoid conflicts
|
||||
self.conditions.push(format!("{field_str} = $value_where"));
|
||||
self
|
||||
.bindings
|
||||
.insert("value_where".to_string(), Value::String(val.into()));
|
||||
} else {
|
||||
// If no value, assume it's a direct condition string (e.g., "is_active = true")
|
||||
self.conditions.push(field_str);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn where_value(mut self, value: impl Into<String>) -> Self {
|
||||
self.where_value = Some(value.into());
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -220,32 +248,41 @@ impl DetailQueryBuilder {
|
||||
let fetch_clause = if self.fetch_fields.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("FETCH {}", self.fetch_fields.join(", "))
|
||||
format!("FETCH {}", self.fetch_fields.join(", ")) // Fixed: Changed self.fetch to self.fetch_fields
|
||||
};
|
||||
|
||||
let from_clause = if let Some(thing) = &self.thing {
|
||||
// Determine the base FROM clause
|
||||
let mut from_clause_base = if let Some(thing) = &self.thing {
|
||||
thing.to_string()
|
||||
} else if let Some(id) = &self.id {
|
||||
format!("{}:⟨{}⟩", self.resource, id)
|
||||
} else if let (Some(field), Some(_)) = (&self.where_field, &self.where_value) {
|
||||
format!("{} WHERE {} = $value", self.resource, field)
|
||||
} else if let Some(id_val) = &self.id {
|
||||
format!("{}:⟨{}⟩", self.resource, id_val)
|
||||
} else {
|
||||
panic!(
|
||||
"You must set one of with_id(), with_thing(), or with_where()+where_value()"
|
||||
);
|
||||
self.resource.clone() // Start with resource name for WHERE queries
|
||||
};
|
||||
|
||||
format!(
|
||||
"SELECT {} FROM {} {}",
|
||||
select_clause, from_clause, fetch_clause
|
||||
)
|
||||
// Add WHERE clause based on accumulated conditions
|
||||
if !self.conditions.is_empty() {
|
||||
// This logic needs to be careful: if `from_clause_base` already contains `WHERE` (e.g. from `id` lookup),
|
||||
// then `conditions` should append with `AND`. But for `DetailQueryBuilder`, only one `WHERE` style is expected.
|
||||
// The panic conditions in `with_id`, `with_thing`, `with_where` should prevent logical conflicts.
|
||||
from_clause_base = format!(
|
||||
"{} WHERE {}",
|
||||
from_clause_base,
|
||||
self.conditions.join(" AND ")
|
||||
);
|
||||
}
|
||||
|
||||
format!("SELECT {select_clause} FROM {from_clause_base} {fetch_clause}")
|
||||
}
|
||||
|
||||
pub fn apply_bindings<'q>(&self, query: Query<'q, any::Any>) -> Query<'q, any::Any> {
|
||||
if let (Some(_), Some(value)) = (&self.where_field, &self.where_value) {
|
||||
query.bind(("value", value.clone()))
|
||||
} else {
|
||||
query
|
||||
// Modified apply_bindings to clone both key and value
|
||||
pub fn apply_bindings<'q>(
|
||||
&self,
|
||||
mut query: Query<'q, any::Any>,
|
||||
) -> Query<'q, any::Any> {
|
||||
for (key, val) in &self.bindings {
|
||||
query = query.bind((key.clone(), val.clone())); // Clone both key and value
|
||||
}
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
+122
-97
@@ -1,117 +1,142 @@
|
||||
use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto};
|
||||
use anyhow::Result;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use surrealdb::Surreal;
|
||||
use imphnen_entities::{
|
||||
CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::Surreal;
|
||||
use tracing;
|
||||
|
||||
pub struct QueryListBuilder<'a> {
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
conditions: Vec<String>,
|
||||
search_field: String,
|
||||
select_fields: Option<Vec<&'a str>>,
|
||||
fetch_fields: Option<Vec<&'a str>>,
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
conditions: Vec<String>,
|
||||
search_field: String,
|
||||
select_fields: Option<Vec<&'a str>>,
|
||||
fetch_fields: Option<Vec<&'a str>>,
|
||||
cast_thing_fields: bool,
|
||||
}
|
||||
|
||||
impl<'a> QueryListBuilder<'a> {
|
||||
pub fn new(
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
table,
|
||||
meta,
|
||||
conditions: vec![],
|
||||
search_field: "name".to_string(),
|
||||
select_fields: None,
|
||||
fetch_fields: None,
|
||||
}
|
||||
}
|
||||
pub fn new(
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
table,
|
||||
meta,
|
||||
conditions: vec![],
|
||||
search_field: "name".to_string(),
|
||||
select_fields: None,
|
||||
fetch_fields: None,
|
||||
cast_thing_fields: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_field(mut self, field: &'a str) -> Self {
|
||||
self.search_field = field.to_string();
|
||||
self
|
||||
}
|
||||
pub fn search_field(mut self, field: &'a str) -> Self {
|
||||
self.search_field = field.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.select_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.select_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.fetch_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.fetch_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build<T>(self) -> Result<ResponseListSuccessDto<Vec<T>>>
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let page = self.meta.page.unwrap_or(1).max(1);
|
||||
let per_page = self.meta.per_page.unwrap_or(10).max(1);
|
||||
let start = (page - 1) * per_page;
|
||||
pub fn with_cast_thing_fields(mut self) -> Self {
|
||||
self.cast_thing_fields = true;
|
||||
self
|
||||
}
|
||||
|
||||
let sql = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
self.select_fields,
|
||||
self.fetch_fields,
|
||||
)
|
||||
.build();
|
||||
pub async fn build<T>(self) -> Result<ResponseListSuccessDto<Vec<T>>>
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let page = self.meta.page.unwrap_or(1).max(1);
|
||||
let per_page = self.meta.per_page.unwrap_or(10).max(1);
|
||||
let start = (page - 1) * per_page;
|
||||
|
||||
let mut query_exec = self.db.query(sql);
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
query_exec = query_exec.bind(("search", search.to_lowercase()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
query_exec = crate::bind_filter_value(query_exec, filter_val.clone());
|
||||
}
|
||||
query_exec = query_exec
|
||||
.bind(("per_page", per_page))
|
||||
.bind(("start", start));
|
||||
// --- Data Query ---
|
||||
let data_query_builder = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
self.select_fields,
|
||||
self.fetch_fields,
|
||||
);
|
||||
let data_sql = data_query_builder.build();
|
||||
|
||||
let raw: Vec<T> = query_exec.await?.take(0)?;
|
||||
// --- Count Query ---
|
||||
let count_query_builder = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
None, // No select fields for count
|
||||
None, // No fetch fields for count
|
||||
);
|
||||
let count_sql = count_query_builder.build_count();
|
||||
|
||||
let mut count_query = self.db.query(format!(
|
||||
"SELECT count() FROM {} {}",
|
||||
self.table,
|
||||
if self.conditions.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
format!("WHERE {}", self.conditions.join(" AND "))
|
||||
}
|
||||
));
|
||||
// Combine both queries into a single query string within a transaction for a single database call
|
||||
let combined_sql = format!(
|
||||
"BEGIN; {}; {}; COMMIT;",
|
||||
data_sql,
|
||||
count_sql
|
||||
);
|
||||
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
count_query = count_query.bind(("search", search.clone()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
count_query = crate::bind_filter_value(count_query, filter_val.clone());
|
||||
}
|
||||
let mut query_exec = self.db.query(combined_sql.clone());
|
||||
|
||||
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
|
||||
let total = count_result.first().map(|c| c.count);
|
||||
// Bind parameters for both data and count queries.
|
||||
// It's assumed that the parameters are named consistently and applied to both.
|
||||
// The ListQueryBuilder already uses $search, $per_page, $start, $filter.
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
query_exec = query_exec.bind(("search", search.to_lowercase()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
query_exec = crate::bind_filter_value(query_exec, filter_val.clone());
|
||||
}
|
||||
query_exec = query_exec
|
||||
.bind(("per_page", per_page))
|
||||
.bind(("start", start));
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: raw,
|
||||
meta: Some(MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total,
|
||||
}),
|
||||
})
|
||||
}
|
||||
let query_debug_str = format!("{:?}", &query_exec);
|
||||
|
||||
let mut response = query_exec.await.map_err(|e| {
|
||||
tracing::error!(
|
||||
query = %combined_sql, // `combined_sql` is cloned, so it can be borrowed here
|
||||
full_query_object = %query_debug_str,
|
||||
"Failed to execute combined query: {:?}", e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
|
||||
// Extract results: first for the data, then for the count
|
||||
let raw: Vec<T> = response.take(0)?; // First result is the data
|
||||
let count_result: Vec<CountResult> = response.take(1)?; // Second result is the count
|
||||
|
||||
let total = count_result.first().map(|c| c.count);
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: raw,
|
||||
meta: Some(MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use serde::de::{self};
|
||||
use serde::ser::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn thing_or_string<'de, D>(deserializer: D) -> Result<Thing, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
match &v {
|
||||
Value::Object(map) => {
|
||||
if let Some(id_val) = map.get("Id") {
|
||||
if let Value::Object(id_map) = id_val {
|
||||
if let Some(Value::String(s)) = id_map.get("String") {
|
||||
return Thing::from_str(s).map_err(|e| {
|
||||
de::Error::custom(format!("Thing::from_str error: {e:?}"))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::from_value(v).map_err(de::Error::custom)
|
||||
}
|
||||
Value::String(s) => {
|
||||
if s.is_empty() {
|
||||
Thing::from_str("unknown:empty")
|
||||
.map_err(|e| de::Error::custom(format!("Thing::from_str error: {e:?}")))
|
||||
} else {
|
||||
Thing::from_str(s)
|
||||
.map_err(|e| de::Error::custom(format!("Thing::from_str error: {e:?}")))
|
||||
}
|
||||
}
|
||||
_ => Err(de::Error::custom(
|
||||
"Expected SurrealDB Thing object, string, or enum Id::String",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn option_thing_or_string<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Thing>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
match &v {
|
||||
Value::Null => Ok(None),
|
||||
Value::Object(map) => {
|
||||
if let Some(id_val) = map.get("Id") {
|
||||
if let Value::Object(id_map) = id_val {
|
||||
if let Some(Value::String(s)) = id_map.get("String") {
|
||||
return Ok(Some(Thing::from_str(s).map_err(|e| {
|
||||
de::Error::custom(format!("Thing::from_str error: {e:?}"))
|
||||
})?));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(serde_json::from_value(v).map_err(de::Error::custom)?))
|
||||
}
|
||||
Value::String(s) => {
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Thing::from_str(s).map_err(|e| {
|
||||
de::Error::custom(format!("Thing::from_str error: {e:?}"))
|
||||
})?))
|
||||
}
|
||||
}
|
||||
_ => Err(de::Error::custom(
|
||||
"Expected SurrealDB Thing object, string, enum Id::String, or null",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string_or_empty_string<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
match v {
|
||||
Value::String(s) => Ok(s),
|
||||
Value::Null => Ok(String::new()),
|
||||
_ => Err(de::Error::custom("Expected a string or null")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_thing<S>(thing: &Thing, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
thing.to_string().serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn serialize_option_thing<S>(
|
||||
thing: &Option<Thing>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match thing {
|
||||
Some(t) => Some(t.to_string()).serialize(serializer),
|
||||
None => None::<String>.serialize(serializer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_datetime<S>(
|
||||
datetime: &chrono::DateTime<chrono::Utc>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&datetime.to_rfc3339())
|
||||
}
|
||||
|
||||
pub fn deserialize_datetime<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
chrono::DateTime::parse_from_rfc3339(&s)
|
||||
.map_err(de::Error::custom)
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc))
|
||||
}
|
||||
@@ -0,0 +1,905 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==============================================================================
|
||||
# IMPHNEN API Comprehensive Test Suite (Bash Version)
|
||||
# ==============================================================================
|
||||
|
||||
BASE_URL="http://127.0.0.1:4099"
|
||||
TEST_EMAIL="admin@example.com"
|
||||
TEST_PASSWORD="password"
|
||||
|
||||
declare -A ALL_USERS=(
|
||||
["admin@example.com"]="Admin"
|
||||
["staff@example.com"]="Staff"
|
||||
["user@example.com"]="User"
|
||||
["mentor@example.com"]="Mentor User"
|
||||
)
|
||||
|
||||
START_SERVER=false
|
||||
SKIP_BASIC=false
|
||||
SKIP_COMPREHENSIVE=false
|
||||
SKIP_CRUD=false
|
||||
GENERATE_REPORT=false
|
||||
VERBOSE=false
|
||||
|
||||
while getopts "sbcrgvh" opt; do
|
||||
case ${opt} in
|
||||
s ) START_SERVER=true ;;
|
||||
b ) SKIP_BASIC=true ;;
|
||||
c ) SKIP_COMPREHENSIVE=true ;;
|
||||
r ) SKIP_CRUD=true ;;
|
||||
g ) GENERATE_REPORT=true ;;
|
||||
v ) VERBOSE=true ;;
|
||||
h )
|
||||
echo "IMPHNEN API Test Suite"
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -s Start server automatically"
|
||||
echo " -b Skip basic tests (auth, error handling)"
|
||||
echo " -c Skip comprehensive tests (users, roles, mentors, etc.)"
|
||||
echo " -r Skip CRUD and advanced tests"
|
||||
echo " -g Generate JSON test report"
|
||||
echo " -v Verbose output (show all INFO logs)"
|
||||
echo " -h Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Run all tests"
|
||||
echo " $0 -s # Start server and run all tests"
|
||||
echo " $0 -g # Run tests and generate report"
|
||||
echo " $0 -sv # Start server with verbose output"
|
||||
echo " $0 -bc # Run only public endpoint tests"
|
||||
exit 0
|
||||
;;
|
||||
\? ) echo "Invalid option: -$OPTARG" >&2; echo "Use -h for help" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v curl &> /dev/null; then
|
||||
echo "Error: 'curl' tidak ditemukan. Mohon install terlebih dahulu." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: 'jq' tidak ditemukan. Mohon install terlebih dahulu." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEST_START_TIME=$(date +%s)
|
||||
AUTH_TOKEN=""
|
||||
SERVER_PID=""
|
||||
TEST_RESULTS=()
|
||||
FALED_TESTS_SUMMARY=()
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
TEST_TESTIMONIAL_ID=""
|
||||
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[0;33m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$SERVER_PID" ]; then
|
||||
printf "\n${YELLOW}Menghentikan proses server...${NC}\n"
|
||||
kill "$SERVER_PID" &>/dev/null
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
write_test_log() {
|
||||
local level=$1
|
||||
local message=$2
|
||||
local color=$NC
|
||||
|
||||
case $level in
|
||||
"SUCCESS") color=$GREEN ;;
|
||||
"ERROR") color=$RED ;;
|
||||
"WARN") color=$YELLOW ;;
|
||||
"INFO") color=$CYAN ;;
|
||||
esac
|
||||
|
||||
if [[ "$VERBOSE" = true || "$level" != "INFO" ]]; then
|
||||
printf "[$(date +'%H:%M:%S')] [${color}%-7s${NC}] %s\n" "$level" "$message" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
test_api_endpoint() {
|
||||
local test_name=$1
|
||||
local method=$2
|
||||
local endpoint=$3
|
||||
local expected_status=$4
|
||||
local body=$5
|
||||
local require_auth=$6
|
||||
|
||||
local headers=(-H "Content-Type: application/json")
|
||||
if [[ "$require_auth" = true && -n "$AUTH_TOKEN" ]]; then
|
||||
headers+=(-H "Authorization: Bearer $AUTH_TOKEN")
|
||||
elif [[ "$require_auth" = true && -z "$AUTH_TOKEN" ]]; then
|
||||
write_test_log "WARN" "✗ $test_name - Dilewati: token autentikasi tidak tersedia"
|
||||
return
|
||||
fi
|
||||
|
||||
local start_req_time=$(date +%s%3N)
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" "${headers[@]}" -d "$body" "$BASE_URL$endpoint")
|
||||
|
||||
http_status=$(echo "$response" | tail -n1)
|
||||
response_body=$(echo "$response" | sed '$d')
|
||||
|
||||
local end_req_time=$(date +%s%3N)
|
||||
local duration=$((end_req_time - start_req_time))
|
||||
|
||||
local status="FAIL"
|
||||
local error_msg=""
|
||||
|
||||
if [ "$http_status" -eq "$expected_status" ]; then
|
||||
status="PASS"
|
||||
((PASS_COUNT++))
|
||||
write_test_log "SUCCESS" "✓ $test_name - Sukses (Status: $http_status, Waktu: ${duration}ms)"
|
||||
else
|
||||
status="FAIL"
|
||||
((FAIL_COUNT++))
|
||||
write_test_log "ERROR" " Request Body: $body"
|
||||
write_test_log "ERROR" " Response Body: $response_body"
|
||||
error_msg="Status yang diharapkan $expected_status, tetapi mendapat $http_status."
|
||||
write_test_log "ERROR" "✗ $test_name - Gagal: $error_msg"
|
||||
FAILED_TESTS_SUMMARY+=("✗ $test_name - $error_msg")
|
||||
fi
|
||||
|
||||
result_json=$(jq -n --arg name "$test_name" --arg ep "$endpoint" --arg meth "$method" \
|
||||
--arg stat "$status" --arg code "$http_status" --arg dur "$duration" \
|
||||
--arg err "$error_msg" \
|
||||
'{TestName: $name, Endpoint: $ep, Method: $meth, Status: $stat, StatusCode: $code, ResponseTimeMs: $dur, Error: $err}')
|
||||
TEST_RESULTS+=("$result_json")
|
||||
# Return response_body for further processing if needed by the caller
|
||||
}
|
||||
|
||||
test_server_connection() {
|
||||
curl -s --head "$BASE_URL/v1/cms/landing/events" > /dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
clear_database() {
|
||||
write_test_log "INFO" "Membersihkan database via WebSocket..."
|
||||
if ! cargo run --bin clear_db_test --release; then
|
||||
write_test_log "ERROR" "Gagal membersihkan database."
|
||||
exit 1
|
||||
fi
|
||||
write_test_log "SUCCESS" "Pembersihan database selesai."
|
||||
}
|
||||
|
||||
get_auth_token() {
|
||||
write_test_log "INFO" "Mengautentikasi test user..."
|
||||
local login_data
|
||||
login_data=$(jq -n --arg email "$TEST_EMAIL" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}')
|
||||
|
||||
local headers=(-H "Content-Type: application/json")
|
||||
local start_req_time=$(date +%s%3N)
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" -X "POST" "${headers[@]}" -d "$login_data" "$BASE_URL/v1/auth/login")
|
||||
|
||||
local http_status=$(echo "$response" | tail -n1)
|
||||
local response_body=$(echo "$response" | sed '$d')
|
||||
local end_req_time=$(date +%s%3N)
|
||||
local duration=$((end_req_time - start_req_time))
|
||||
|
||||
if [ "$http_status" -eq 200 ]; then
|
||||
if echo "$response_body" | jq . > /dev/null 2>&1; then
|
||||
AUTH_TOKEN=$(echo "$response_body" | jq -r '.data.token.access_token // empty')
|
||||
if [[ -n "$AUTH_TOKEN" && "$AUTH_TOKEN" != "null" ]]; then
|
||||
write_test_log "SUCCESS" "✓ User Authentication - Sukses (Status: $http_status, Waktu: ${duration}ms)"
|
||||
write_test_log "SUCCESS" "Autentikasi berhasil"
|
||||
((PASS_COUNT++))
|
||||
else
|
||||
write_test_log "ERROR" "Autentikasi gagal - token tidak ditemukan dalam response"
|
||||
AUTH_TOKEN=""
|
||||
((FAIL_COUNT++))
|
||||
fi
|
||||
else
|
||||
write_test_log "ERROR" "Autentikasi gagal - response bukan JSON valid"
|
||||
AUTH_TOKEN=""
|
||||
((FAIL_COUNT++))
|
||||
fi
|
||||
else
|
||||
write_test_log "ERROR" "✗ User Authentication - Gagal (Status: $http_status, Waktu: ${duration}ms)"
|
||||
AUTH_TOKEN=""
|
||||
((FAIL_COUNT++))
|
||||
fi
|
||||
|
||||
local status="PASS"
|
||||
local error_msg=""
|
||||
if [ "$http_status" -ne 200 ] || [[ -z "$AUTH_TOKEN" ]]; then
|
||||
status="FAIL"
|
||||
error_msg="Authentication failed"
|
||||
fi
|
||||
|
||||
result_json=$(jq -n --arg name "User Authentication" --arg ep "/v1/auth/login" --arg meth "POST" \
|
||||
--arg stat "$status" --arg code "$http_status" --arg dur "$duration" \
|
||||
--arg err "$error_msg" \
|
||||
'{TestName: $name, Endpoint: $ep, Method: $meth, Status: $stat, StatusCode: $code, ResponseTimeMs: $dur, Error: $err}')
|
||||
TEST_RESULTS+=("$result_json")
|
||||
}
|
||||
|
||||
test_all_users_login_performance() {
|
||||
printf "\n${CYAN}=== Menguji Login Performance Semua User ===${NC}\n"
|
||||
|
||||
local total_login_time=0
|
||||
local successful_logins=0
|
||||
local failed_logins=0
|
||||
|
||||
local email="admin@example.com"
|
||||
local fullname="${ALL_USERS[$email]}"
|
||||
write_test_log "INFO" "Testing login for: $fullname ($email)"
|
||||
|
||||
local login_data
|
||||
login_data=$(jq -n --arg email "$email" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}')
|
||||
|
||||
local start_time=$(date +%s%3N)
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" -X "POST" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$login_data" \
|
||||
"$BASE_URL/v1/auth/login")
|
||||
|
||||
local http_status=$(echo "$response" | tail -n1)
|
||||
local response_body=$(echo "$response" | sed '$d')
|
||||
local end_time=$(date +%s%3N)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
total_login_time=$((total_login_time + duration))
|
||||
|
||||
if [ "$http_status" -eq 200 ]; then
|
||||
if echo "$response_body" | jq -e '.data.token.access_token' > /dev/null 2>&1; then
|
||||
((successful_logins++))
|
||||
((PASS_COUNT++))
|
||||
write_test_log "SUCCESS" "✓ Login $fullname - ${duration}ms"
|
||||
|
||||
result_json=$(jq -n --arg name "Login Performance - $fullname" --arg ep "/v1/auth/login" --arg meth "POST" \
|
||||
--arg stat "PASS" --arg code "$http_status" --arg dur "$duration" \
|
||||
--arg err "" \
|
||||
'{TestName: $name, Endpoint: $ep, Method: $meth, Status: $stat, StatusCode: $code, ResponseTimeMs: $dur, Error: $err}')
|
||||
TEST_RESULTS+=("$result_json")
|
||||
else
|
||||
((failed_logins++))
|
||||
((FAIL_COUNT++))
|
||||
write_test_log "ERROR" "✗ Login $fullname - No token (${duration}ms)"
|
||||
FAILED_TESTS_SUMMARY+=("✗ Login $fullname - No token in response")
|
||||
fi
|
||||
else
|
||||
((failed_logins++))
|
||||
((FAIL_COUNT++))
|
||||
write_test_log "ERROR" "✗ Login $fullname - HTTP $http_status (${duration}ms)"
|
||||
FAILED_TESTS_SUMMARY+=("✗ Login $fullname - HTTP $http_status")
|
||||
fi
|
||||
|
||||
local total_users=1
|
||||
local avg_login_time=0
|
||||
if [ "$total_users" -gt 0 ]; then
|
||||
avg_login_time=$((total_login_time / total_users))
|
||||
fi
|
||||
|
||||
printf "\n${BLUE}=== Login Performance Summary ===${NC}\n"
|
||||
printf "Total Users Tested: %d\n" "$total_users"
|
||||
printf "${GREEN}Successful Logins: %d${NC}\n" "$successful_logins"
|
||||
printf "${RED}Failed Logins: %d${NC}\n" "$failed_logins"
|
||||
printf "${BLUE}Average Login Time: %dms${NC}\n" "$avg_login_time"
|
||||
printf "${BLUE}Total Login Time: %dms${NC}\n" "$total_login_time"
|
||||
|
||||
if [ "$avg_login_time" -lt 2000 ]; then
|
||||
printf "${GREEN}✅ Performance Status: EXCELLENT (< 2s average)${NC}\n"
|
||||
elif [ "$avg_login_time" -lt 5000 ]; then
|
||||
printf "${YELLOW}⚠️ Performance Status: GOOD (2-5s average)${NC}\n"
|
||||
else
|
||||
printf "${RED}❌ Performance Status: POOR (> 5s average)${NC}\n"
|
||||
fi
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
test_with_user() {
|
||||
local email=$1
|
||||
local fullname=$2
|
||||
local test_name=$3
|
||||
|
||||
write_test_log "INFO" "Testing $test_name dengan user: $fullname ($email)"
|
||||
|
||||
local login_data
|
||||
login_data=$(jq -n --arg email "$email" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}')
|
||||
|
||||
local start_time=$(date +%s%3N)
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" -X "POST" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$login_data" \
|
||||
"$BASE_URL/v1/auth/login")
|
||||
|
||||
local http_status=$(echo "$response" | tail -n1)
|
||||
local response_body=$(echo "$response" | sed '$d')
|
||||
local end_time=$(date +%s%3N)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
if [ "$http_status" -eq 200 ]; then
|
||||
if echo "$response_body" | jq -e '.data.token.access_token' > /dev/null 2>&1; then
|
||||
local user_auth_token=$(echo "$response_body" | jq -r '.data.token.access_token')
|
||||
write_test_log "SUCCESS" "✓ Login $fullname berhasil - ${duration}ms"
|
||||
|
||||
local me_response
|
||||
me_response=$(curl -s -w "\n%{http_code}" -X "GET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $user_auth_token" \
|
||||
"$BASE_URL/v1/users/me")
|
||||
|
||||
local me_status=$(echo "$me_response" | tail -n1)
|
||||
local me_body=$(echo "$me_response" | sed '$d')
|
||||
|
||||
if [ "$me_status" -eq 200 ]; then
|
||||
((PASS_COUNT++))
|
||||
write_test_log "SUCCESS" "✓ Get profile $fullname berhasil"
|
||||
|
||||
local user_email=$(echo "$me_body" | jq -r '.data.email // empty')
|
||||
local user_name=$(echo "$me_body" | jq -r '.data.fullname // empty')
|
||||
|
||||
if [ "$user_email" = "$email" ]; then
|
||||
write_test_log "SUCCESS" "✓ User data verified: $user_name ($user_email)"
|
||||
else
|
||||
write_test_log "WARN" "⚠ User data mismatch: expected $email, got $user_email"
|
||||
fi
|
||||
else
|
||||
((FAIL_COUNT++))
|
||||
write_test_log "ERROR" "✗ Get profile $fullname gagal - HTTP $me_status"
|
||||
FAILED_TESTS_SUMMARY+=("✗ Get profile $fullname - HTTP $me_status")
|
||||
fi
|
||||
|
||||
else
|
||||
((FAIL_COUNT++))
|
||||
write_test_log "ERROR" "✗ Login $fullname - No token (${duration}ms)"
|
||||
FAILED_TESTS_SUMMARY+=("✗ Login $fullname - No token in response")
|
||||
fi
|
||||
else
|
||||
write_test_log "ERROR" "✗ Login $fullname gagal - HTTP $http_status (${duration}ms)"
|
||||
FAILED_TESTS_SUMMARY+=("✗ Login $fullname - HTTP $http_status")
|
||||
fi
|
||||
}
|
||||
|
||||
test_all_users_individually() {
|
||||
printf "\n${CYAN}=== Menguji Semua User Secara Individual ===${NC}\n"
|
||||
|
||||
for email in "${!ALL_USERS[@]}"; do
|
||||
local fullname="${ALL_USERS[$email]}"
|
||||
test_with_user "$email" "$fullname" "Individual User Test"
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
test_comprehensive_with_user() {
|
||||
printf "\n${CYAN}=== Comprehensive Test untuk $fullname ($email) ===${NC}\n"
|
||||
|
||||
local login_data
|
||||
login_data=$(jq -n --arg email "$email" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}')
|
||||
|
||||
local start_time=$(date +%s%3N)
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" -X "POST" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$login_data" \
|
||||
"$BASE_URL/v1/auth/login")
|
||||
|
||||
local http_status=$(echo "$response" | tail -n1)
|
||||
local response_body=$(echo "$response" | sed '$d')
|
||||
local end_time=$(date +%s%3N)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
if [ "$http_status" -eq 200 ]; then
|
||||
if echo "$response_body" | jq -e '.data.token.access_token' > /dev/null 2>&1; then
|
||||
user_token=$(echo "$response_body" | jq -r '.data.token.access_token')
|
||||
write_test_log "SUCCESS" "✓ Login $fullname berhasil - ${duration}ms"
|
||||
|
||||
local original_auth_token="$AUTH_TOKEN"
|
||||
|
||||
AUTH_TOKEN="$user_token"
|
||||
|
||||
printf "\n${BLUE}--- Testing dengan $fullname (Expected results berdasarkan role) ---${NC}\n"
|
||||
|
||||
test_api_endpoint "Get Current User Profile - $fullname" "GET" "/v1/users/me" 200 "" true
|
||||
|
||||
case "$email" in
|
||||
"admin@example.com")
|
||||
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true
|
||||
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 200 "" true
|
||||
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 200 "" true
|
||||
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
||||
test_api_endpoint "Get Mentor Me - $fullname" "GET" "/v1/mentors/me" 403 "" true # Admin is not a mentor
|
||||
test_api_endpoint "Get Mentor Status - $fullname" "GET" "/v1/mentors/status" 403 "" true # Admin is not a mentor
|
||||
test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true
|
||||
test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true
|
||||
|
||||
local testimonial_data
|
||||
testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}')
|
||||
test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true
|
||||
;;
|
||||
|
||||
"staff@example.com")
|
||||
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true
|
||||
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 200 "" true
|
||||
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 200 "" true
|
||||
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
||||
test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true
|
||||
test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true
|
||||
|
||||
local testimonial_data
|
||||
testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}')
|
||||
test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true
|
||||
;;
|
||||
|
||||
"mentor@example.com")
|
||||
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true
|
||||
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
|
||||
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
|
||||
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
||||
test_api_endpoint "Get Mentor Me - $fullname" "GET" "/v1/mentors/me" 200 "" true
|
||||
test_api_endpoint "Get Mentor Status - $fullname" "GET" "/v1/mentors/status" 200 "" true
|
||||
test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true
|
||||
test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true
|
||||
|
||||
local testimonial_data
|
||||
testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}')
|
||||
test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true
|
||||
;;
|
||||
|
||||
"user@example.com")
|
||||
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true
|
||||
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
|
||||
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
|
||||
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
|
||||
test_api_endpoint "Get Mentor Me - $fullname" "GET" "/v1/mentors/me" 403 "" true # User is not a mentor
|
||||
test_api_endpoint "Get Mentor Status - $fullname" "GET" "/v1/mentors/status" 403 "" true # User is not a mentor
|
||||
test_api_endpoint "Get Gacha Items - $fullname" "GET" "/v1/gacha/items" 200 "" true
|
||||
test_api_endpoint "Execute Gacha Roll - $fullname" "POST" "/v1/gacha/rolls/execute" 200 "" true
|
||||
|
||||
local testimonial_data
|
||||
testimonial_data=$(jq -n --arg content "Test testimonial by $fullname $(date +%s)" '{role: "Student", content: $content}')
|
||||
test_api_endpoint "Create Testimonial - $fullname" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true
|
||||
;;
|
||||
esac
|
||||
|
||||
test_api_endpoint "Events with Advanced Filter - $fullname" "GET" "/v1/cms/landing/events?filter=online&filter_by=is_online" 200 "" false
|
||||
test_api_endpoint "Testimonials with Search - $fullname" "GET" "/v1/cms/landing/testimonials?search=test" 200 "" false
|
||||
|
||||
case "$email" in
|
||||
"admin@example.com"|"staff@example.com")
|
||||
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true
|
||||
;;
|
||||
*)
|
||||
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 403 "" true
|
||||
;;
|
||||
esac
|
||||
|
||||
AUTH_TOKEN="$original_auth_token"
|
||||
|
||||
write_test_log "SUCCESS" "✓ Comprehensive test untuk $fullname selesai"
|
||||
|
||||
else
|
||||
write_test_log "ERROR" "✗ Login $fullname gagal - No token (${duration}ms)"
|
||||
fi
|
||||
else
|
||||
write_test_log "ERROR" "✗ Login $fullname gagal - HTTP $http_status (${duration}ms)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_all_endpoints_with_all_users() {
|
||||
printf "\n${CYAN}=== Menjalankan Semua Test dengan Semua User ===${NC}\n"
|
||||
|
||||
for email in "${!ALL_USERS[@]}"; do
|
||||
local fullname="${ALL_USERS[$email]}"
|
||||
test_comprehensive_with_user "$email" "$fullname"
|
||||
printf "\n${BLUE}--- Selesai testing dengan $fullname ---${NC}\n\n"
|
||||
done
|
||||
}
|
||||
|
||||
test_public_endpoints() {
|
||||
printf "\n${CYAN}=== Menguji Public Endpoints ===${NC}\n"
|
||||
test_api_endpoint "Get Events List" "GET" "/v1/cms/landing/events" 200
|
||||
test_api_endpoint "Get Testimonials List" "GET" "/v1/cms/landing/testimonials" 200
|
||||
}
|
||||
|
||||
test_authentication_endpoints() {
|
||||
printf "\n${CYAN}=== Menguji Authentication Endpoints ===${NC}\n"
|
||||
get_auth_token
|
||||
|
||||
local invalid_login
|
||||
invalid_login=$(jq -n '{email: "invalid@example.com", password: "wrongpassword"}')
|
||||
test_api_endpoint "Invalid Login Test" "POST" "/v1/auth/login" 401 "$invalid_login"
|
||||
|
||||
# User registration and verification tests currently rely on external email service or OTP logic
|
||||
# that is not easily testable in a simple curl script without actual email sending/receiving.
|
||||
# Skipping these tests for now.
|
||||
# local register_email="test_user_$(date +%s%N)@example.com"
|
||||
# local register_data=$(jq -n --arg email "$register_email" --arg pass "$TEST_PASSWORD" --arg fullname "Test Register" --arg phone "081234567899" '{email: $email, password: $pass, fullname: $fullname, phone_number: $phone}')
|
||||
# test_api_endpoint "User Registration Test" "POST" "/v1/auth/register" 200 "$register_data"
|
||||
# local verify_otp_data=$(jq -n --arg email "$register_email" --arg otp "123456" '{email: $email, otp: ($otp | tonumber)}')
|
||||
# test_api_endpoint "Verify Email Test (Invalid OTP)" "POST" "/v1/auth/verify-email" 400 "$verify_otp_data"
|
||||
|
||||
local forgot_password_data
|
||||
forgot_password_data=$(jq -n --arg email "$TEST_EMAIL" '{email: $email}')
|
||||
test_api_endpoint "Forgot Password Test" "POST" "/v1/auth/forgot" 400 "$forgot_password_data"
|
||||
|
||||
local new_password_data
|
||||
new_password_data=$(jq -n --arg token "some_reset_token" --arg pass "newpassword123!A" '{token: $token, password: $pass}')
|
||||
test_api_endpoint "New Password Test (Invalid Token)" "POST" "/v1/auth/new-password" 400 "$new_password_data"
|
||||
|
||||
local refresh_token=$(curl -s -X POST -H "Content-Type: application/json" -d "$(jq -n --arg email "$TEST_EMAIL" --arg pass "$TEST_PASSWORD" '{email: $email, password: $pass}')" "$BASE_URL/v1/auth/login" | jq -r '.data.token.refresh_token // empty')
|
||||
if [ -n "$refresh_token" ]; then
|
||||
local refresh_data
|
||||
refresh_data=$(jq -n --arg token "$refresh_token" '{refresh_token: $token}')
|
||||
test_api_endpoint "Refresh Token Test" "POST" "/v1/auth/refresh" 200 "$refresh_data"
|
||||
else
|
||||
write_test_log "WARN" "✗ Refresh Token Test - Dilewati: Refresh token tidak tersedia dari login"
|
||||
fi
|
||||
}
|
||||
|
||||
test_error_handling() {
|
||||
printf "\n${CYAN}=== Menguji Error Handling ===${NC}\n"
|
||||
test_api_endpoint "Non-existent Endpoint" "GET" "/v1/nonexistent" 404
|
||||
test_api_endpoint "Unauthorized Access" "GET" "/v1/users" 401 "" false
|
||||
}
|
||||
|
||||
test_user_management_endpoints() {
|
||||
printf "\n${CYAN}=== Menguji User Management Endpoints ===${NC}\n"
|
||||
test_api_endpoint "Get Users List" "GET" "/v1/users" 200 "" true
|
||||
|
||||
local test_user_id="c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2"
|
||||
test_api_endpoint "Get User By ID" "GET" "/v1/users/detail/$test_user_id" 200 "" true
|
||||
|
||||
local new_user_email="new_test_user_$(date +%s%N)@example.com"
|
||||
local new_user_fullname="New Test User $(date +%s%N)"
|
||||
local new_user_phone="089876543211"
|
||||
local new_user_password="NewPassword123!"
|
||||
local new_user_role_id="5713cb37-dc02-4e87-8048-d7a41d352059" # User role ID from seed_users.rs
|
||||
|
||||
local create_user_data=$(jq -n \
|
||||
--arg email "$new_user_email" \
|
||||
--arg pass "$new_user_password" \
|
||||
--arg fullname "$new_user_fullname" \
|
||||
--arg phone "$new_user_phone" \
|
||||
--arg is_active true \
|
||||
--arg role_id "$new_user_role_id" \
|
||||
'{email: $email, password: $pass, fullname: $fullname, phone_number: $phone, is_active: $is_active | fromjson, role_id: $role_id}')
|
||||
test_api_endpoint "Create New User" "POST" "/v1/users/create" 201 "$create_user_data" true
|
||||
|
||||
# Assuming the created user can be fetched by email for update/delete
|
||||
local created_user_id=$(curl -s -X GET -H "Authorization: Bearer $AUTH_TOKEN" "$BASE_URL/v1/users?search=$new_user_email" | jq -r '.data[0].id // empty')
|
||||
|
||||
if [ -n "$created_user_id" ]; then
|
||||
local updated_user_fullname="Updated Test User $(date +%s%N)"
|
||||
local updated_user_data=$(jq -n \
|
||||
--arg email "$new_user_email" \
|
||||
--arg pass "$new_user_password" \
|
||||
--arg fullname "$updated_user_fullname" \
|
||||
--arg phone "$new_user_phone" \
|
||||
--arg is_active true \
|
||||
--arg gender "Male" \
|
||||
--arg birthdate "1990-01-01" \
|
||||
--arg avatar "https://example.com/avatar.jpg" \
|
||||
--arg role_id "$new_user_role_id" \
|
||||
'{email: $email, password: $pass, fullname: $fullname, phone_number: $phone, is_active: $is_active | fromjson, gender: $gender, birthdate: $birthdate, avatar: $avatar, role_id: $role_id}')
|
||||
test_api_endpoint "Update User" "PUT" "/v1/users/update/$created_user_id" 200 "$updated_user_data" true
|
||||
|
||||
local set_active_data=$(jq -n --arg is_active false '{is_active: $is_active | fromjson}')
|
||||
test_api_endpoint "Deactivate User" "PUT" "/v1/users/activate/$created_user_id" 200 "$set_active_data" true
|
||||
|
||||
local set_active_data=$(jq -n --arg is_active true '{is_active: $is_active | fromjson}')
|
||||
test_api_endpoint "Reactivate User" "PUT" "/v1/users/activate/$created_user_id" 200 "$set_active_data" true
|
||||
|
||||
test_api_endpoint "Delete User" "DELETE" "/v1/users/delete/$created_user_id" 200 "" true
|
||||
else
|
||||
write_test_log "WARN" "✗ Skipping User Update/Delete tests: Failed to retrieve ID of newly created user."
|
||||
fi
|
||||
}
|
||||
|
||||
test_crud_operations() {
|
||||
printf "\n${CYAN}=== Menguji CRUD Operations ===${NC}\n"
|
||||
|
||||
local testimonial_data
|
||||
testimonial_data=$(jq -n --arg content "Test testimonial via Bash $(date +%s)" '{role: "Student", content: $content}')
|
||||
local testimonial_response=$(test_api_endpoint "Create Testimonial" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true)
|
||||
TEST_TESTIMONIAL_ID=$(echo "$testimonial_response" | jq -r '.data.id // empty')
|
||||
write_test_log "INFO" "TEST_TESTIMONIAL_ID: $TEST_TESTIMONIAL_ID"
|
||||
write_test_log "INFO" "Captured Testimonial ID: $TEST_TESTIMONIAL_ID"
|
||||
sleep 0.2
|
||||
|
||||
local permission_data
|
||||
permission_data=$(jq -n --arg name "Test Permission $(date +%s)" '{name: $name}')
|
||||
test_api_endpoint "Create Permission" "POST" "/v1/permissions/create" 201 "$permission_data" true
|
||||
|
||||
local gacha_item_data
|
||||
gacha_item_data=$(jq -n --arg name "Test Item $(date +%s)" '{name: $name, image_url: "https://example.com/id.jpg"}')
|
||||
test_api_endpoint "Create Gacha Item" "POST" "/v1/gacha/items/create" 201 "$gacha_item_data" true
|
||||
|
||||
local event_data
|
||||
event_data=$(jq -n --arg name "Test Event $(date +%s)" '{
|
||||
name: $name,
|
||||
description: "Test event description",
|
||||
detail_link: "https://example.com/event",
|
||||
price: 50.0,
|
||||
is_online: true,
|
||||
start_date: "2025-12-01T10:00:00Z",
|
||||
end_date: "2025-12-01T16:00:00Z",
|
||||
location: null
|
||||
}')
|
||||
test_api_endpoint "Create Event" "POST" "/v1/cms/landing/events/create" 201 "$event_data" true
|
||||
}
|
||||
|
||||
test_roles_and_permissions() {
|
||||
printf "\n${CYAN}=== Menguji Roles & Permissions Endpoints ===${NC}\n"
|
||||
test_api_endpoint "Get Roles List" "GET" "/v1/roles" 200 "" true
|
||||
test_api_endpoint "Get Permissions List" "GET" "/v1/permissions" 200 "" true
|
||||
|
||||
local test_role_id="3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a"
|
||||
test_api_endpoint "Get Role By ID" "GET" "/v1/roles/detail/$test_role_id" 200 "" true
|
||||
}
|
||||
|
||||
test_mentor_endpoints() {
|
||||
printf "\n${CYAN}=== Menguji Mentor Endpoints ===${NC}\n"
|
||||
test_api_endpoint "Get Mentors List" "GET" "/v1/mentors" 200 "" true
|
||||
# These tests are run with AUTH_TOKEN set to admin. Since admin is not a mentor, these should be 403.
|
||||
test_api_endpoint "Get Mentor Me" "GET" "/v1/mentors/me" 403 "" true
|
||||
test_api_endpoint "Get Mentor Status" "GET" "/v1/mentors/status" 403 "" true
|
||||
|
||||
local test_mentor_id="e6f78d23-83bf-5c2b-bcd4-001345678901"
|
||||
test_api_endpoint "Get Mentor By ID" "GET" "/v1/mentors/detail/$test_mentor_id" 200 "" true
|
||||
|
||||
local mentor_register_data
|
||||
mentor_register_data=$(jq -n --arg email "test.mentor.$(date +%s%N)@example.com" '{
|
||||
identity_and_verification: {
|
||||
legal_name: "Test Mentor Legal Name",
|
||||
identity_document_url: "https://example.com/id.jpg",
|
||||
phone_for_verification: "+1234567890"
|
||||
},
|
||||
professional_profile: {
|
||||
bio: "Test mentor bio",
|
||||
linkedin_url: "https://linkedin.com/in/testmentor",
|
||||
industries: ["Technology", "Software"],
|
||||
expertise: ["JavaScript", "Python"],
|
||||
languages: ["English", "Indonesian"],
|
||||
current_company: "Test Company",
|
||||
current_role: "Senior Developer",
|
||||
years_of_experience: 5
|
||||
},
|
||||
mentoring_logistics: {
|
||||
topics_of_interest: ["Career Development", "Technical Skills"],
|
||||
preferred_mentee_level: ["Junior", "Mid-level"],
|
||||
preferred_mentoring_formats: ["1-on-1", "Group"],
|
||||
availability_commitment: "2-3 hours per week",
|
||||
mentoring_rate: {
|
||||
amount: 100000,
|
||||
currency: "IDR",
|
||||
per_duration: "hour"
|
||||
}
|
||||
},
|
||||
email: $email
|
||||
}')
|
||||
test_api_endpoint "Register as Mentor" "POST" "/v1/mentors/register" 422 "$mentor_register_data" true
|
||||
}
|
||||
|
||||
test_events_endpoints() {
|
||||
printf "\n${CYAN}=== Menguji Events Endpoints ===${NC}\n"
|
||||
test_api_endpoint "Get Events with Pagination" "GET" "/v1/cms/landing/events?page=1&per_page=5" 200
|
||||
test_api_endpoint "Get Events with Search" "GET" "/v1/cms/landing/events?search=tech" 200
|
||||
|
||||
local test_event_id="e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o"
|
||||
test_api_endpoint "Get Event By ID" "GET" "/v1/cms/landing/events/detail/$test_event_id" 200
|
||||
}
|
||||
|
||||
test_testimonials_endpoints() {
|
||||
printf "\n${CYAN}=== Menguji Testimonials Endpoints ===${NC}\n"
|
||||
test_api_endpoint "Get Testimonials with Pagination" "GET" "/v1/cms/landing/testimonials?page=1&per_page=5" 200
|
||||
|
||||
# Ensure TEST_TESTIMONIAL_ID is not empty before testing
|
||||
if [ -n "$TEST_TESTIMONIAL_ID" ]; then
|
||||
test_api_endpoint "Get Testimonial By ID" "GET" "/v1/cms/landing/testimonials/detail/$TEST_TESTIMONIAL_ID" 200
|
||||
else
|
||||
write_test_log "WARN" "✗ Get Testimonial By ID - Dilewati: TEST_TESTIMONIAL_ID tidak tersedia"
|
||||
fi
|
||||
}
|
||||
|
||||
test_gacha_endpoints() {
|
||||
printf "\n${CYAN}=== Menguji Gacha Endpoints ===${NC}\n"
|
||||
test_api_endpoint "Get Gacha Items" "GET" "/v1/gacha/items" 200 "" true
|
||||
test_api_endpoint "Execute Gacha Roll" "POST" "/v1/gacha/rolls/execute" 200 "" true
|
||||
}
|
||||
|
||||
test_advanced_scenarios() {
|
||||
printf "\n${CYAN}=== Menguji Advanced Scenarios ===${NC}\n"
|
||||
|
||||
test_api_endpoint "Events with Advanced Filter" "GET" "/v1/cms/landing/events?filter=online&filter_by=is_online" 200
|
||||
test_api_endpoint "Users with Sort" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true
|
||||
test_api_endpoint "Testimonials with Search" "GET" "/v1/cms/landing/testimonials?search=test" 200
|
||||
|
||||
local mentor_register_data
|
||||
mentor_register_data=$(jq -n --arg email "test.mentor.$(date +%s%N)@example.com" '{
|
||||
identity_and_verification: {
|
||||
legal_name: "Test Mentor Legal Name",
|
||||
identity_document_url: "https://example.com/id.jpg",
|
||||
phone_for_verification: "+1234567890"
|
||||
},
|
||||
professional_profile: {
|
||||
bio: "Test mentor bio",
|
||||
linkedin_url: "https://linkedin.com/in/testmentor",
|
||||
industries: ["Technology", "Software"],
|
||||
expertise: ["JavaScript", "Python"],
|
||||
languages: ["English", "Indonesian"],
|
||||
current_company: "Test Company",
|
||||
current_role: "Senior Developer",
|
||||
years_of_experience: 5
|
||||
},
|
||||
mentoring_logistics: {
|
||||
topics_of_interest: ["Career Development", "Technical Skills"],
|
||||
preferred_mentee_level: ["Junior", "Mid-level"],
|
||||
preferred_mentoring_formats: ["1-on-1", "Group"],
|
||||
availability_commitment: "2-3 hours per week",
|
||||
mentoring_rate: {
|
||||
amount: 100000,
|
||||
currency: "IDR",
|
||||
per_duration: "hour"
|
||||
}
|
||||
},
|
||||
email: $email
|
||||
}')
|
||||
test_api_endpoint "Register as Mentor" "POST" "/v1/mentors/register" 422 "$mentor_register_data" true
|
||||
|
||||
test_api_endpoint "Invalid POST to GET endpoint" "POST" "/v1/cms/landing/events" 405
|
||||
test_api_endpoint "Invalid PUT with Invalid ID" "PUT" "/v1/users/update/some_invalid_id" 400 "" true
|
||||
}
|
||||
|
||||
show_test_summary() {
|
||||
printf "\n${YELLOW}=== Test Coverage Summary ===${NC}\n"
|
||||
printf "📋 Authentication: Login, Forgot Password, OTP\n"
|
||||
printf "👥 Users: List, Details, Profile Management\n"
|
||||
printf "🔐 Roles & Permissions: RBAC System Testing\n"
|
||||
printf "👨🏫 Mentors: Registration, Profile, Status\n"
|
||||
printf "📅 Events: CRUD Operations, Filtering\n"
|
||||
printf "💬 Testimonials: Management & Creation\n"
|
||||
printf "🎲 Gacha: Items, Rolls, Claims\n"
|
||||
printf "🔧 Advanced: Pagination, Search, Edge Cases\n"
|
||||
printf "❌ Error Handling: 401, 404, Invalid Requests\n"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
printf "${CYAN}=== IMPHNEN API Comprehensive Test Suite ===${NC}\n"
|
||||
printf "${YELLOW}Base URL: %s${NC}\n" "$BASE_URL"
|
||||
show_test_summary
|
||||
|
||||
if [ "$START_SERVER" = true ]; then
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
write_test_log "ERROR" "Perintah 'cargo' tidak ditemukan. Tidak bisa memulai server."
|
||||
exit 1
|
||||
fi
|
||||
printf "${YELLOW}Memulai server backend...${NC}\n"
|
||||
cargo run --bin api &
|
||||
SERVER_PID=$!
|
||||
|
||||
printf "${YELLOW}Menunggu server siap...${NC}\n"
|
||||
retries=0
|
||||
max_retries=100 # Increased from 15 to 30
|
||||
until test_server_connection; do
|
||||
((retries++))
|
||||
if [ $retries -ge $max_retries ]; then
|
||||
write_test_log "ERROR" "Gagal memulai server dalam timeout\. Cek output terminal untuk detail\."
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
write_test_log "SUCCESS" "Server berjalan!"
|
||||
else
|
||||
if ! test_server_connection; then
|
||||
write_test_log "ERROR" "Server tidak berjalan di $BASE_URL"
|
||||
write_test_log "WARN" "Silakan jalankan server secara manual atau gunakan flag -s"
|
||||
exit 1
|
||||
fi
|
||||
write_test_log "SUCCESS" "Server sudah berjalan di $BASE_URL"
|
||||
fi
|
||||
|
||||
clear_database
|
||||
|
||||
printf "\n${CYAN}=== Menjalankan Seeders ===${NC}\n"
|
||||
if ! cargo run --bin seeder; then
|
||||
write_test_log "ERROR" "Gagal menjalankan seeder roles permissions."
|
||||
exit 1
|
||||
fi
|
||||
write_test_log "SUCCESS" "Seeders selesai."
|
||||
|
||||
|
||||
printf "\n${CYAN}=== Menampilkan User yang Tersedia ===${NC}\n"
|
||||
for email in "${!ALL_USERS[@]}"; do
|
||||
fullname="${ALL_USERS[$email]}"
|
||||
printf "${BLUE}• $fullname${NC} - ${email}\n"
|
||||
done
|
||||
printf "\n"
|
||||
|
||||
test_public_endpoints
|
||||
|
||||
test_all_users_login_performance
|
||||
|
||||
test_all_users_individually
|
||||
|
||||
if [ "$SKIP_BASIC" = false ]; then
|
||||
test_authentication_endpoints
|
||||
test_error_handling
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_CRUD" = false && -n "$AUTH_TOKEN" ]]; then
|
||||
test_crud_operations # This will now set TEST_TESTIMONIAL_ID
|
||||
fi
|
||||
|
||||
if [ "$SKIP_COMPREHENSIVE" = false ]; then
|
||||
test_all_endpoints_with_all_users
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_COMPREHENSIVE" = false && -n "$AUTH_TOKEN" ]]; then
|
||||
printf "\n${CYAN}=== Test Comprehensive dengan Admin Token ===${NC}\n"
|
||||
test_user_management_endpoints
|
||||
test_roles_and_permissions
|
||||
test_mentor_endpoints
|
||||
test_events_endpoints
|
||||
test_testimonials_endpoints # This will now use TEST_TESTIMONIAL_ID
|
||||
test_gacha_endpoints
|
||||
fi
|
||||
|
||||
test_advanced_scenarios
|
||||
|
||||
TEST_END_TIME=$(date +%s)
|
||||
TOTAL_DURATION=$((TEST_END_TIME - TEST_START_TIME))
|
||||
TOTAL_TESTS=$((PASS_COUNT + FAIL_COUNT))
|
||||
SUCCESS_RATE="0"
|
||||
if [ "$TOTAL_TESTS" -gt 0 ]; then
|
||||
SUCCESS_RATE=$(( (PASS_COUNT * 100) / TOTAL_TESTS ))
|
||||
fi
|
||||
|
||||
if [ "$GENERATE_REPORT" = true ]; then
|
||||
printf "\n${CYAN}=== Membuat Laporan Tes ===${NC}\n"
|
||||
|
||||
all_results_json=$(printf "%s," "${TEST_RESULTS[@]}")
|
||||
all_results_json="[${all_results_json%,}]"
|
||||
|
||||
report_file="api-test-report-$(date +'%Y%m%d-%H%M%S').json"
|
||||
|
||||
jq -n --arg start "$(date -d @$TEST_START_TIME +'%Y-%m-%d %H:%M:%S')" \
|
||||
--arg end "$(date -d @$TEST_END_TIME +'%Y-%m-%d %H:%M:%S')" \
|
||||
--arg dur "$TOTAL_DURATION" \
|
||||
--arg url "$BASE_URL" \
|
||||
--arg total "$TOTAL_TESTS" \
|
||||
--arg pass "$PASS_COUNT" \
|
||||
--arg fail "$FAIL_COUNT" \
|
||||
--arg rate "${SUCCESS_RATE}%" \
|
||||
--argjson results "$all_results_json" \
|
||||
'{
|
||||
TestRun: {StartTime: $start, EndTime: $end, DurationSec: $dur, BaseUrl: $url},
|
||||
Summary: {TotalTests: $total, PassedTests: $pass, FailedTests: $fail, SuccessRate: $rate},
|
||||
Results: $results
|
||||
}' > "$report_file"
|
||||
|
||||
printf "${BLUE}Laporan tes detail disimpan di: %s${NC}\n" "$report_file"
|
||||
fi
|
||||
|
||||
printf "\n${CYAN}=== Ringkasan Test Suite ===${NC}\n"
|
||||
printf "Total Durasi: %s detik\n" "$TOTAL_DURATION"
|
||||
printf "Total Tes : %s\n" "$TOTAL_TESTS"
|
||||
printf "${GREEN}Lolos : %s${NC}\n" "$PASS_COUNT"
|
||||
printf "${RED}Gagal : %s${NC}\n" "$FAIL_COUNT"
|
||||
printf "Tingkat Sukses: %s%%\n" "$SUCCESS_RATE"
|
||||
|
||||
if [ "$FAIL_COUNT" -gt 0 ]; then
|
||||
printf "\n${RED}Tes yang Gagal:${NC}\n"
|
||||
for summary in "${FAILED_TESTS_SUMMARY[@]}"; do
|
||||
printf " %s\n" "$summary"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$FAIL_COUNT" -eq 0 ]; then
|
||||
printf "\n${GREEN}Test suite selesai dengan sukses.${NC}\n"
|
||||
exit 0
|
||||
else
|
||||
printf "\n${RED}Test suite selesai dengan beberapa kegagalan.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
+23
-10
@@ -1,16 +1,29 @@
|
||||
[package]
|
||||
name = "tests"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
axum.workspace = true
|
||||
axum-test.workspace = true
|
||||
chrono.workspace = true
|
||||
surrealdb.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
http-body-util.workspace = true
|
||||
hyper.workspace = true
|
||||
hyper-util.workspace = true
|
||||
serde_json.workspace = true
|
||||
dotenvy.workspace = true
|
||||
tower.workspace = true
|
||||
tracing.workspace = true
|
||||
surrealdb.workspace = true
|
||||
serde.workspace = true
|
||||
strum.workspace = true
|
||||
uuid.workspace = true
|
||||
rand.workspace = true
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
axum.workspace = true
|
||||
@@ -0,0 +1,758 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Method, Request, StatusCode},
|
||||
response::Response,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use http_body_util::BodyExt; // for `buffer` method
|
||||
use imphnen_dimentorin::{
|
||||
mentors_controller,
|
||||
mentors_dto::{MentorUserRegisterRequestDto, MentorRegisterResponseDto},
|
||||
};
|
||||
use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient};
|
||||
use imphnen_iam::{RolesRepository, UsersRepository};
|
||||
use imphnen_iam::{RolesEnum, UsersSchema, RolesSchema};
|
||||
use imphnen_utils::{hash_password, make_thing, get_iso_date};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
use imphnen_libs::{ResourceEnum, surrealdb_init_ws, surrealdb_init_mem, Env};
|
||||
use dotenvy::dotenv;
|
||||
use tower::ServiceExt; // Added ServiceExt for .oneshot()
|
||||
use crate::mock_test::setup_all_test_environment; // Import the new setup function
|
||||
|
||||
// Helper function to create a test AppState
|
||||
async fn setup_app_state() -> AppState {
|
||||
dotenv().ok();
|
||||
// Use the surrealdb initialization functions from imphnen_libs
|
||||
let surrealdb_ws = surrealdb_init_ws().await.expect("Failed to initialize SurrealDB WS client");
|
||||
let surrealdb_mem = surrealdb_init_mem().await.expect("Failed to initialize SurrealDB MEM client");
|
||||
|
||||
let app_state = AppState {
|
||||
surrealdb_ws,
|
||||
surrealdb_mem,
|
||||
};
|
||||
|
||||
// Manually seed roles since seed_roles is not directly callable as a repository method
|
||||
let db = &app_state.surrealdb_ws;
|
||||
let roles_to_seed = vec![
|
||||
(RolesEnum::Admin.to_string(), Vec::new()),
|
||||
(RolesEnum::User.to_string(), Vec::new()),
|
||||
(RolesEnum::Staff.to_string(), Vec::new()),
|
||||
(RolesEnum::Mentor.to_string(), Vec::new()),
|
||||
];
|
||||
|
||||
for (name, permissions) in roles_to_seed {
|
||||
// Check if role already exists to avoid errors on rerun
|
||||
let existing_role: Option<RolesSchema> = db.query(format!("SELECT * FROM ONLY role WHERE name = '{}'", name)).await.unwrap().take(0).unwrap_or(None);
|
||||
if existing_role.is_none() {
|
||||
let role_id = Uuid::new_v4().to_string();
|
||||
let role = RolesSchema {
|
||||
id: make_thing(&ResourceEnum::Roles.to_string(), &role_id),
|
||||
name: name.clone(),
|
||||
is_deleted: false,
|
||||
permissions,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
db.create::<RolesSchema>((ResourceEnum::Roles.to_string().as_str(), role_id)).content(role).await.unwrap(); // Corrected syntax
|
||||
}
|
||||
}
|
||||
|
||||
app_state
|
||||
}
|
||||
|
||||
// Helper function to create a test application (router)
|
||||
fn app(app_state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/v1/mentors/register", post(mentors_controller::post_register_mentor))
|
||||
.with_state(app_state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_new_user_as_mentor_success() {
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "newmentor@example.com";
|
||||
let test_password = "Password123!";
|
||||
let test_fullname = "New Mentor User";
|
||||
let test_phone = "1234567890";
|
||||
|
||||
// Clean up before test
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let _ = user_repo.query_delete_user(test_email.to_string()).await;
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta Selatan".to_string()),
|
||||
identity_document_url: "http://example.com/id.pdf".to_string(),
|
||||
phone_for_verification: "0987654321".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let mentor_register_response: MentorRegisterResponseDto = serde_json::from_slice(&body).unwrap();
|
||||
|
||||
assert!(!mentor_register_response.id.is_empty());
|
||||
assert!(!mentor_register_response.user_id.is_empty());
|
||||
assert_eq!(mentor_register_response.email, Some(test_email.to_string()));
|
||||
assert_eq!(mentor_register_response.status, "pending".to_string()); // New mentors should be pending verification
|
||||
|
||||
// Verify user created and has mentor role, and is inactive
|
||||
let user = user_repo.query_user_by_email(test_email.to_string()).await.unwrap();
|
||||
assert_eq!(user.email, test_email);
|
||||
assert_eq!(user.is_active, false); // Should be inactive awaiting OTP
|
||||
let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role
|
||||
let mentor_role = role_repo.query_role_by_name(RolesEnum::Mentor.to_string()).await.unwrap();
|
||||
assert_eq!(user.role.to_raw(), mentor_role.id.to_raw()); // Compare raw IDs
|
||||
|
||||
// Clean up after test
|
||||
let _ = user_repo.query_delete_user(test_email.to_string()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_existing_user_as_mentor_success() {
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "existinguser_mentor@example.com";
|
||||
let test_password = "Password123!";
|
||||
let test_fullname = "Existing User Becoming Mentor";
|
||||
let test_phone = "1122334455";
|
||||
|
||||
// Clean up and create an existing user
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let _ = user_repo.query_delete_user(test_email.to_string()).await;
|
||||
|
||||
// Manually create a user with 'User' role first
|
||||
let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role
|
||||
let user_role_id_item = role_repo.query_role_by_name(RolesEnum::User.to_string()).await.unwrap(); // Get RolesDetailItemDto
|
||||
let user_role_id_thing = make_thing(&ResourceEnum::Roles.to_string(), &user_role_id_item.id); // Convert to Thing
|
||||
let hashed_password = hash_password(test_password).unwrap();
|
||||
user_repo.query_create_user(imphnen_iam::UsersSchema {
|
||||
id: make_thing(&ResourceEnum::Users.to_string(), &Uuid::new_v4().to_string()),
|
||||
email: test_email.to_string(),
|
||||
password: hashed_password,
|
||||
fullname: "Original User Name".to_string(),
|
||||
phone_number: "0000000000".to_string(),
|
||||
is_active: true, // Initially active
|
||||
role: user_role_id_thing,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
..Default::default()
|
||||
}).await.unwrap();
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(), // Keep password same for simplicity in test
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name Updated".to_string(),
|
||||
gender: Some("Perempuan".to_string()),
|
||||
domicile: Some("Surabaya".to_string()),
|
||||
identity_document_url: "http://example.com/id_updated.pdf".to_string(),
|
||||
phone_for_verification: "0987654322".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Existing user now a mentor.".to_string(),
|
||||
last_education: Some("S2".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor_existing".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio_existing".to_string()),
|
||||
industries: vec!["Education".to_string()],
|
||||
expertise: vec!["Marketing".to_string()],
|
||||
languages: vec!["Indonesian".to_string()],
|
||||
current_company: "New Company".to_string(),
|
||||
current_role: "Manager".to_string(),
|
||||
years_of_experience: 10,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Business Strategy".to_string()],
|
||||
preferred_mentee_level: vec!["Experienced".to_string()],
|
||||
preferred_mentoring_formats: vec!["Offline".to_string()],
|
||||
availability_commitment: "10 hours/month".to_string(),
|
||||
mentoring_rate_amount: 200,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let mentor_register_response: MentorRegisterResponseDto = serde_json::from_slice(&body).unwrap();
|
||||
|
||||
assert!(!mentor_register_response.id.is_empty());
|
||||
assert!(!mentor_register_response.user_id.is_empty());
|
||||
assert_eq!(mentor_register_response.email, Some(test_email.to_string()));
|
||||
assert_eq!(mentor_register_response.status, "pending".to_string()); // Should be pending verification
|
||||
|
||||
// Verify user updated and has mentor role, and is inactive
|
||||
let user = user_repo.query_user_by_email(test_email.to_string()).await.unwrap();
|
||||
assert_eq!(user.email, test_email);
|
||||
assert_eq!(user.fullname, test_fullname);
|
||||
assert_eq!(user.phone_number, test_phone);
|
||||
assert_eq!(user.is_active, false); // Should be inactive awaiting OTP
|
||||
let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role
|
||||
let mentor_role = role_repo.query_role_by_name(RolesEnum::Mentor.to_string()).await.unwrap();
|
||||
assert_eq!(user.role.to_raw(), mentor_role.id.to_raw()); // Compare raw IDs
|
||||
|
||||
// Clean up after test
|
||||
let _ = user_repo.query_delete_user(test_email.to_string()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_mentor_already_has_profile() {
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "existing_mentor_profile@example.com";
|
||||
let test_password = "Password123!";
|
||||
let test_fullname = "Existing Mentor Profile";
|
||||
let test_phone = "5551234567";
|
||||
|
||||
// Clean up and create a user that is already a mentor
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mentor_repo = imphnen_dimentorin::mentors_repository::MentorsRepository::new(&app_state);
|
||||
let _ = user_repo.query_delete_user(test_email.to_string()).await;
|
||||
|
||||
// Manually create a user with 'Mentor' role
|
||||
let role_repo = RolesRepository::new(&app_state); // Use RolesRepository to get role
|
||||
let mentor_role_thing = role_repo.query_role_by_name(RolesEnum::Mentor.to_string()).await.unwrap().id;
|
||||
let hashed_password = imphnen_utils::hash_password(test_password).unwrap();
|
||||
let user_id = imphnen_utils::make_thing(&ResourceEnum::Users.to_string(), &Uuid::new_v4().to_string());
|
||||
user_repo.query_create_user(imphnen_iam::UsersSchema {
|
||||
id: user_id.clone(),
|
||||
email: test_email.to_string(),
|
||||
password: hashed_password,
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
is_active: true,
|
||||
role: mentor_role_thing.clone(),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
// Removed mentor_user_id
|
||||
}).await.unwrap();
|
||||
|
||||
// Manually create a mentor profile for this user
|
||||
let existing_mentor_profile_id = imphnen_utils::make_thing(&ResourceEnum::Mentors.to_string(), &Uuid::new_v4().to_string());
|
||||
mentor_repo.query_create_mentor(imphnen_dimentorin::mentors_schema::MentorSchema {
|
||||
id: existing_mentor_profile_id.clone(),
|
||||
user_id: Some(user_id.clone()),
|
||||
email: Some(test_email.to_string()),
|
||||
legal_name: "Existing Mentor Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Bandung".to_string()),
|
||||
identity_document_url: "http://example.com/existing_id.pdf".to_string(),
|
||||
phone_for_verification: "1234567890".to_string(),
|
||||
bio: "Already an existing mentor in the system.".to_string(),
|
||||
last_education: Some("S3".to_string()),
|
||||
linkedin_url: None,
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/existing_portfolio".to_string()),
|
||||
industries: vec!["Finance".to_string()],
|
||||
expertise: vec!["Investments".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Finance Corp".to_string(),
|
||||
current_role: "Analyst".to_string(),
|
||||
years_of_experience: 7,
|
||||
topics_of_interest: vec!["Stocks".to_string()],
|
||||
preferred_mentee_level: vec!["All".to_string()],
|
||||
preferred_mentoring_formats: vec!["Any".to_string()],
|
||||
availability_commitment: "Flexible".to_string(),
|
||||
mentoring_rate: imphnen_dimentorin::mentors_dto::MentoringRate::default(),
|
||||
status: "Approved".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
}).await.unwrap();
|
||||
|
||||
// Update user to link mentor profile
|
||||
let user_after_mentor_creation_dto = user_repo.query_user_by_email(test_email.to_string()).await.unwrap();
|
||||
let mut user_after_mentor_creation_schema = UsersSchema::from(user_after_mentor_creation_dto);
|
||||
user_after_mentor_creation_schema = user_after_mentor_creation_schema.update_mentor_id(Some(existing_mentor_profile_id.clone().to_raw()));
|
||||
user_repo.query_update_user(user_after_mentor_creation_schema).await.unwrap();
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Perempuan".to_string()),
|
||||
domicile: Some("Yogyakarta".to_string()),
|
||||
identity_document_url: "http://example.com/id.pdf".to_string(),
|
||||
phone_for_verification: "0987654321".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("SMA".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio_new".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::CONFLICT); // Expecting conflict
|
||||
|
||||
// Clean up after test
|
||||
let _ = user_repo.query_delete_user(test_email.to_string()).await;
|
||||
let _ = mentor_repo.query_delete_mentor(&existing_mentor_profile_id.to_raw()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_mentor_invalid_email_format() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "invalid-email"; // Invalid email format
|
||||
let test_password = "Password123!";
|
||||
let test_fullname = "Test User";
|
||||
let test_phone = "1234567890";
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta Selatan".to_string()),
|
||||
identity_document_url: "http://example.com/id.pdf".to_string(),
|
||||
phone_for_verification: "0987654321".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(error_response["message"].as_str().unwrap().contains("email"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_mentor_weak_password() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "weakpass@example.com";
|
||||
let test_password = "weak"; // Weak password
|
||||
let test_fullname = "Test User";
|
||||
let test_phone = "1234567890";
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta Selatan".to_string()),
|
||||
identity_document_url: "http://example.com/id.pdf".to_string(),
|
||||
phone_for_verification: "0987654321".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(error_response["message"].as_str().unwrap().contains("password"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_mentor_missing_fullname() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "missingfullname@example.com";
|
||||
let test_password = "Password123!";
|
||||
let test_phone = "1234567890";
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: "".to_string(), // Missing fullname
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta Selatan".to_string()),
|
||||
identity_document_url: "http://example.com/id.pdf".to_string(),
|
||||
phone_for_verification: "0987654321".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(error_response["message"].as_str().unwrap().contains("fullname"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_mentor_missing_phone_number() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "missingphone@example.com";
|
||||
let test_password = "Password123!";
|
||||
let test_fullname = "Test User";
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: "".to_string(), // Missing phone number
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta Selatan".to_string()),
|
||||
identity_document_url: "http://example.com/id.pdf".to_string(),
|
||||
phone_for_verification: "0987654321".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(error_response["message"].as_str().unwrap().contains("phone_number"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_mentor_missing_identity_document_url() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "missingdocurl@example.com";
|
||||
let test_password = "Password123!";
|
||||
let test_fullname = "Test User";
|
||||
let test_phone = "1234567890";
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta Selatan".to_string()),
|
||||
identity_document_url: "".to_string(), // Missing identity document url
|
||||
phone_for_verification: "0987654321".to_string(),
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(error_response["message"].as_str().unwrap().contains("identity_document_url"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_mentor_invalid_phone_for_verification_format() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let app = app(app_state.clone());
|
||||
|
||||
let test_email = "invalidphoneveri@example.com";
|
||||
let test_password = "Password123!";
|
||||
let test_fullname = "Test User";
|
||||
let test_phone = "1234567890";
|
||||
|
||||
let dto = MentorUserRegisterRequestDto {
|
||||
email: test_email.to_string(),
|
||||
password: test_password.to_string(),
|
||||
fullname: test_fullname.to_string(),
|
||||
phone_number: test_phone.to_string(),
|
||||
identity_and_verification: imphnen_dimentorin::mentors_dto::IdentityAndVerification {
|
||||
legal_name: "Legal Name".to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta Selatan".to_string()),
|
||||
identity_document_url: "http://example.com/id.pdf".to_string(),
|
||||
phone_for_verification: "invalid".to_string(), // Invalid phone for verification
|
||||
},
|
||||
professional_profile: imphnen_dimentorin::mentors_dto::ProfessionalProfile {
|
||||
bio: "Experienced professional seeking to mentor others in software development.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("http://linkedin.com/in/mentor".to_string()),
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Technology".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Debugging".to_string()],
|
||||
languages: vec!["English".to_string()],
|
||||
current_company: "Acme Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
},
|
||||
mentoring_logistics: imphnen_dimentorin::mentors_dto::MentoringLogistics {
|
||||
topics_of_interest: vec!["Career Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["Online".to_string()],
|
||||
availability_commitment: "5 hours/week".to_string(),
|
||||
mentoring_rate_amount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/v1/mentors/register")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&dto).unwrap()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let error_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(error_response["message"].as_str().unwrap().contains("phone_for_verification"));
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
use anyhow::Result;
|
||||
use imphnen_dimentorin::v1::mentors::{MentorSchema, MentoringRate, MentorsRepository};
|
||||
use surrealdb::sql::Thing;
|
||||
use imphnen_iam::v1::users::UsersRepository;
|
||||
use tests::{
|
||||
create_test_user, get_role_id, ResourceEnum, cleanup_db,
|
||||
generate_unique_email, setup_all_test_environment,
|
||||
};
|
||||
use surrealdb::Uuid;
|
||||
|
||||
/// Helper to create a full-featured MentorSchema for tests
|
||||
fn create_full_mentor_schema(id: &str, user_id: &str, email: &str, legal_name: &str) -> MentorSchema {
|
||||
MentorSchema {
|
||||
id: Thing::from(("app_mentors", id)),
|
||||
user_id: Some(Thing::from(("app_users", user_id))),
|
||||
email: Some(email.to_string()),
|
||||
legal_name: legal_name.to_string(),
|
||||
gender: Some("Laki-laki".to_string()),
|
||||
domicile: Some("Jakarta".to_string()),
|
||||
identity_document_url: "https://example.com/ktp.jpg".to_string(),
|
||||
phone_for_verification: "+6281234567890".to_string(),
|
||||
bio: "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant.".to_string(),
|
||||
last_education: Some("S1".to_string()),
|
||||
linkedin_url: Some("https://linkedin.com/in/mentor".to_string()),
|
||||
github_url: Some("http://github.com/test".to_string()),
|
||||
cv_url: Some("http://example.com/cv.pdf".to_string()),
|
||||
portfolio_url: Some("http://example.com/portfolio".to_string()),
|
||||
industries: vec!["Software".to_string(), "Education".to_string()],
|
||||
expertise: vec!["Rust".to_string(), "Microservices".to_string()],
|
||||
languages: vec!["Indonesian".to_string(), "English".to_string()],
|
||||
current_company: "PT Contoh".to_string(),
|
||||
current_role: "Senior Backend Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
topics_of_interest: vec!["Rust Programming".to_string(), "Backend Development".to_string()],
|
||||
preferred_mentee_level: vec!["beginner".to_string(), "intermediate".to_string()],
|
||||
preferred_mentoring_formats: vec!["Test Format".to_string()],
|
||||
availability_commitment: "2 jam per minggu untuk mentoring online dan offline".to_string(),
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: 100_000,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
status: "verified".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: "2024-01-01T00:00:00Z".to_string(),
|
||||
updated_at: "2024-01-01T00:00:00Z".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_mentor() -> Result<()> {
|
||||
cleanup_db().await; // Keep for now as setup_all_test_environment doesn't clean up
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let email = generate_unique_email("test_create_mentor");
|
||||
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mut user = create_test_user(&email, "Mentor User", true, &get_role_id(&app_state).await);
|
||||
user.id = Thing::from(("app_users", id.as_str()));
|
||||
user.email = email.to_string();
|
||||
user.mentor_id = Some(Thing::from(("app_mentors", id.as_str())));
|
||||
let create_result = user_repo.query_create_user(user.clone()).await;
|
||||
assert!(create_result.is_ok(), "Failed to create user: {:?}", create_result.err());
|
||||
|
||||
let mentor = create_full_mentor_schema(&id, &id, &email, "Mentor User");
|
||||
let create_res = repo.query_create_mentor(mentor.clone()).await;
|
||||
assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err());
|
||||
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str()));
|
||||
let mentor_fetched = repo.query_mentor_by_id(&thing_id, false).await;
|
||||
assert!(mentor_fetched.is_ok(), "Failed to fetch mentor after create: {:?}", mentor_fetched.err());
|
||||
let mentor_fetched = mentor_fetched.unwrap();
|
||||
assert_eq!(mentor_fetched.legal_name, "Mentor User");
|
||||
assert_eq!(mentor_fetched.status, "verified");
|
||||
assert_eq!(mentor_fetched.user_id, Thing::from(("app_users", id.as_str())));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_all_mentors() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let email = generate_unique_email("test_get_all_mentors");
|
||||
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mut user = create_test_user(&email, "Test Mentor Get All User", true, &get_role_id(&app_state).await);
|
||||
user.id = Thing::from(("app_users", id.as_str()));
|
||||
user.email = email.to_string();
|
||||
user.mentor_id = Some(Thing::from(("app_mentors", id.as_str())));
|
||||
let create_user_res = user_repo.query_create_user(user.clone()).await;
|
||||
assert!(create_user_res.is_ok(), "Failed to create user for get_all test: {:?}", create_user_res.err());
|
||||
|
||||
let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Get All");
|
||||
let create_res = repo.query_create_mentor(mentor.clone()).await;
|
||||
assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err());
|
||||
|
||||
let mut meta = imphnen_libs::MetaRequestDto::default();
|
||||
meta.search = Some("Test Mentor Get All".to_string());
|
||||
|
||||
let mentors_res = repo.query_mentor_list(meta).await;
|
||||
assert!(mentors_res.is_ok(), "Failed to get all mentors: {:?}", mentors_res.err());
|
||||
let mentors = mentors_res.unwrap().data;
|
||||
assert!(!mentors.is_empty(), "Mentors list should not be empty");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_mentor_by_id() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let email = generate_unique_email("test_get_mentor_by_id");
|
||||
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mut user = create_test_user(&email, "Test Mentor Get By Id User", true, &get_role_id(&app_state).await);
|
||||
user.id = Thing::from(("app_users", id.as_str()));
|
||||
user.email = email.to_string();
|
||||
user.mentor_id = Some(Thing::from(("app_mentors", id.as_str())));
|
||||
let create_user_res = user_repo.query_create_user(user.clone()).await;
|
||||
assert!(create_user_res.is_ok(), "Failed to create user for get_by_id test: {:?}", create_user_res.err());
|
||||
|
||||
let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Get By Id");
|
||||
let create_res = repo.query_create_mentor(mentor.clone()).await;
|
||||
assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err());
|
||||
|
||||
let mentor_res = repo
|
||||
.query_mentor_by_id(&Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str())), false)
|
||||
.await;
|
||||
assert!(mentor_res.is_ok(), "Failed to get mentor by id: {:?}", mentor_res.err());
|
||||
let mentor = mentor_res.unwrap();
|
||||
assert_eq!(mentor.legal_name, "Test Mentor Get By Id");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_mentor() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let email = generate_unique_email("test_update_mentor");
|
||||
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mut user = create_test_user(&email, "Test Mentor Update User", true, &get_role_id(&app_state).await);
|
||||
user.id = Thing::from(("app_users", id.as_str()));
|
||||
user.email = email.to_string();
|
||||
user.mentor_id = Some(Thing::from(("app_mentors", id.as_str())));
|
||||
let create_user_res = user_repo.query_create_user(user.clone()).await;
|
||||
assert!(create_user_res.is_ok(), "Failed to create user for update test: {:?}", create_user_res.err());
|
||||
|
||||
let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Update");
|
||||
let create_res = repo.query_create_mentor(mentor.clone()).await;
|
||||
assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err());
|
||||
|
||||
let mut updated_mentor = mentor.clone();
|
||||
updated_mentor.legal_name = "Test Mentor Updated".to_string();
|
||||
updated_mentor.gender = Some("Perempuan".to_string());
|
||||
updated_mentor.domicile = Some("Bandung".to_string());
|
||||
updated_mentor.last_education = Some("S2".to_string());
|
||||
updated_mentor.portfolio_url = Some("http://example.com/updated_portfolio".to_string());
|
||||
updated_mentor.mentoring_rate.amount = 200_000;
|
||||
|
||||
let update_res = repo.query_update_mentor(updated_mentor.clone()).await;
|
||||
assert!(update_res.is_ok(), "Failed to update mentor: {:?}", update_res.err());
|
||||
|
||||
let mentor_fetched = repo
|
||||
.query_mentor_by_id(&Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str())), false)
|
||||
.await;
|
||||
assert!(mentor_fetched.is_ok(), "Failed to fetch mentor after update: {:?}", mentor_fetched.err());
|
||||
let mentor_fetched = mentor_fetched.unwrap();
|
||||
assert_eq!(mentor_fetched.legal_name, "Test Mentor Updated");
|
||||
assert_eq!(mentor_fetched.gender, Some("Perempuan".to_string()));
|
||||
assert_eq!(mentor_fetched.domicile, Some("Bandung".to_string()));
|
||||
assert_eq!(mentor_fetched.last_education, Some("S2".to_string()));
|
||||
assert_eq!(mentor_fetched.portfolio_url, Some("http://example.com/updated_portfolio".to_string()));
|
||||
assert_eq!(mentor_fetched.mentoring_rate.amount, 200_000);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_mentor() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let email = generate_unique_email("test_delete_mentor");
|
||||
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mut user = create_test_user(&email, "Test Mentor Delete User", true, &get_role_id(&app_state).await);
|
||||
user.id = Thing::from(("app_users", id.as_str()));
|
||||
user.email = email.to_string();
|
||||
user.mentor_id = Some(Thing::from(("app_mentors", id.as_str())));
|
||||
let create_user_res = user_repo.query_create_user(user.clone()).await;
|
||||
assert!(create_user_res.is_ok(), "Failed to create user for delete test: {:?}", create_user_res.err());
|
||||
|
||||
let mentor = create_full_mentor_schema(&id, &id, &email, "Test Mentor Delete");
|
||||
let create_res = repo.query_create_mentor(mentor.clone()).await;
|
||||
assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err());
|
||||
|
||||
let delete_res = repo.query_delete_mentor(id.clone()).await;
|
||||
assert!(delete_res.is_ok(), "Failed to delete mentor: {:?}", delete_res.err());
|
||||
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id.as_str()));
|
||||
let mentor_fetched = repo.query_mentor_by_id(&thing_id, false).await;
|
||||
assert!(mentor_fetched.is_err(), "Mentor should not be found after soft delete");
|
||||
if let Some(err) = mentor_fetched.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Mentor not found in database")
|
||||
|| err.to_string().contains("Mentor has been deleted"),
|
||||
"Expected 'Mentor not found in database' or 'Mentor has been deleted' error, got: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
let delete_again_res = repo.query_delete_mentor(id.clone()).await;
|
||||
assert!(delete_again_res.is_err(), "Should not be able to soft delete an already deleted mentor");
|
||||
if let Some(err) = delete_again_res.err() {
|
||||
assert!(
|
||||
err.to_string().contains("already soft deleted"),
|
||||
"Expected 'already soft deleted' error, got: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_by_user_email() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let user_email = generate_unique_email("test_mentor_by_email");
|
||||
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mut user = create_test_user(&user_email, "Test User By Email", true, &get_role_id(&app_state).await);
|
||||
user.id = Thing::from(("app_users", id.as_str()));
|
||||
user.email = user_email.clone();
|
||||
user.mentor_id = Some(Thing::from(("app_mentors", id.as_str())));
|
||||
let create_user_res = user_repo.query_create_user(user.clone()).await;
|
||||
assert!(create_user_res.is_ok(), "Failed to create user for get_by_user_email test: {:?}", create_user_res.err());
|
||||
|
||||
let mentor = create_full_mentor_schema(&id, &id, &user_email, "Test Mentor By Email");
|
||||
let create_res = repo.query_create_mentor(mentor.clone()).await;
|
||||
assert!(create_res.is_ok(), "Failed to create mentor: {:?}", create_res.err());
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
let mentor_detail = repo
|
||||
.query_mentor_by_email(user_email.to_string(), false)
|
||||
.await;
|
||||
if mentor_detail.is_err() {
|
||||
let mentor_by_id = repo
|
||||
.query_mentor_by_id(&Thing::from(("app_mentors", id.as_str())), false)
|
||||
.await;
|
||||
assert!(
|
||||
mentor_by_id.is_ok(),
|
||||
"Failed to get mentor by user_id fallback: {:?}",
|
||||
mentor_by_id.err()
|
||||
);
|
||||
let mentor_by_id = mentor_by_id.unwrap();
|
||||
assert_eq!(mentor_by_id.legal_name, "Test Mentor By Email");
|
||||
return Ok(());
|
||||
}
|
||||
assert!(
|
||||
mentor_detail.is_ok(),
|
||||
"Failed to get mentor by user email: {:?}",
|
||||
mentor_detail.err()
|
||||
);
|
||||
let mentor_detail = mentor_detail.unwrap();
|
||||
assert_eq!(mentor_detail.legal_name, "Test Mentor By Email");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_mentor_with_duplicate_user_id() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let email = generate_unique_email("test_duplicate_user_id");
|
||||
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let mut user = create_test_user(&email, "Duplicate User", true, &get_role_id(&app_state).await);
|
||||
user.id = Thing::from(("app_users", id.as_str()));
|
||||
user.email = email.to_string();
|
||||
user.mentor_id = Some(Thing::from(("app_mentors", id.as_str())));
|
||||
let _ = user_repo.query_create_user(user.clone()).await;
|
||||
|
||||
let mentor1 = create_full_mentor_schema(&id, &id, &email, "Mentor 1");
|
||||
let create_res1 = repo.query_create_mentor(mentor1.clone()).await;
|
||||
assert!(create_res1.is_ok(), "Failed to create first mentor: {:?}", create_res1.err());
|
||||
|
||||
// Attempt to create another mentor with the same user_id
|
||||
let duplicate_mentor_id = Uuid::new_v4().to_string();
|
||||
let mentor2 = create_full_mentor_schema(&duplicate_mentor_id, &id, &generate_unique_email("test_duplicate_user_id_2"), "Mentor 2");
|
||||
let create_res2 = repo.query_create_mentor(mentor2.clone()).await;
|
||||
assert!(create_res2.is_err(), "Should not be able to create mentor with duplicate user_id");
|
||||
if let Some(err) = create_res2.err() {
|
||||
assert!(err.to_string().contains("already has a mentor profile"), "Expected 'already has a mentor profile' error, got: {}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_non_existent_mentor() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let non_existent_id = Uuid::new_v4().to_string();
|
||||
let non_existent_email = generate_unique_email("test_non_existent_mentor");
|
||||
let non_existent_user_id = Uuid::new_v4().to_string();
|
||||
|
||||
let mentor_to_update = create_full_mentor_schema(&non_existent_id, &non_existent_user_id, &non_existent_email, "Non Existent Mentor");
|
||||
let update_res = repo.query_update_mentor(mentor_to_update).await;
|
||||
assert!(update_res.is_err(), "Should not be able to update a non-existent mentor");
|
||||
if let Some(err) = update_res.err() {
|
||||
assert!(err.to_string().contains("Mentor not found"), "Expected 'Mentor not found' error, got: {}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_non_existent_mentor() -> Result<()> {
|
||||
cleanup_db().await;
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let repo = MentorsRepository::new(&app_state);
|
||||
|
||||
let non_existent_id = Uuid::new_v4().to_string();
|
||||
let delete_res = repo.query_delete_mentor(non_existent_id.clone()).await;
|
||||
assert!(delete_res.is_err(), "Should not be able to delete a non-existent mentor");
|
||||
if let Some(err) = delete_res.err() {
|
||||
assert!(err.to_string().contains("Mentor not found"), "Expected 'Mentor not found' error, got: {}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#[cfg(test)]
|
||||
pub mod mentor_repository_test;
|
||||
#[cfg(test)]
|
||||
pub mod mentor_registration_tests;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod mentor;
|
||||
@@ -0,0 +1,379 @@
|
||||
#[cfg(test)]
|
||||
mod auth_login_tests {
|
||||
use crate::generate_unique_email;
|
||||
use crate::hash_password;
|
||||
use crate::mock_test::setup_all_test_environment;
|
||||
use axum::http::StatusCode;
|
||||
use imphnen_iam::{
|
||||
v1::auth::{AuthLoginRequestDto, AuthService},
|
||||
AppState, UsersRepository, UsersSchema,
|
||||
};
|
||||
use serde_json::Value; // Import the new setup function
|
||||
|
||||
async fn setup_test_environment() -> AppState {
|
||||
setup_all_test_environment().await
|
||||
}
|
||||
|
||||
async fn create_test_user_with_role(
|
||||
state: &AppState,
|
||||
email: &str,
|
||||
password: &str,
|
||||
role_name: &str,
|
||||
is_active: bool,
|
||||
) -> UsersSchema {
|
||||
let role_repo = imphnen_iam::RolesRepository::new(state);
|
||||
|
||||
let role = match role_repo.query_role_by_name(role_name.to_string()).await {
|
||||
Ok(role) => role,
|
||||
Err(_) => {
|
||||
let _ = role_repo
|
||||
.query_create_role(imphnen_iam::RolesRequestCreateDto {
|
||||
name: role_name.to_string(),
|
||||
permissions: vec![],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
role_repo
|
||||
.query_role_by_name(role_name.to_string())
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("Failed to create {role_name} role");
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let user = UsersSchema {
|
||||
id: crate::make_thing("app_users", &uuid::Uuid::new_v4().to_string()),
|
||||
email: email.to_string(),
|
||||
fullname: "Test User".to_string(),
|
||||
password: hash_password(password).unwrap(),
|
||||
is_deleted: false,
|
||||
avatar: None,
|
||||
phone_number: "081234567890".to_string(),
|
||||
is_active,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
role: crate::make_thing("app_roles", &role.id),
|
||||
mentor_id: None,
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
};
|
||||
let user_repo = UsersRepository::new(state);
|
||||
user_repo
|
||||
.query_create_user(user.clone())
|
||||
.await
|
||||
.expect("Failed to create test user");
|
||||
|
||||
user
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_successful_login_with_valid_credentials() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_login_success");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::OK);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
|
||||
assert!(response_json.get("data").is_some());
|
||||
assert!(response_json["data"].get("token").is_some());
|
||||
assert!(response_json["data"]["token"].get("access_token").is_some());
|
||||
assert!(response_json["data"]["token"]
|
||||
.get("refresh_token")
|
||||
.is_some());
|
||||
assert!(response_json["data"].get("user").is_some());
|
||||
assert_eq!(response_json["data"]["user"]["email"], email);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_invalid_email_format() {
|
||||
let state = setup_test_environment().await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: "invalid-email".to_string(),
|
||||
password: "TestPass123!".to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
assert_eq!(response_json["message"], "Email not valid");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_empty_email() {
|
||||
let state = setup_test_environment().await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: "".to_string(),
|
||||
password: "TestPass123!".to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
let message = response_json["message"].as_str().unwrap();
|
||||
assert!(message.contains("Email cannot be empty"));
|
||||
assert!(message.contains("Email not valid"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_empty_password() {
|
||||
let state = setup_test_environment().await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: generate_unique_email("test_empty_pass"),
|
||||
password: "".to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
assert_eq!(response_json["message"], "Password cannot be empty");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_wrong_password() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_wrong_pass");
|
||||
let correct_password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, correct_password, "User", true).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: "WrongPassword123!".to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
|
||||
assert_eq!(response_json["message"], "Email or password not correct");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_nonexistent_user() {
|
||||
let state = setup_test_environment().await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: generate_unique_email("nonexistent"),
|
||||
password: "TestPass123!".to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::UNAUTHORIZED);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
|
||||
assert!(response_json["message"]
|
||||
.to_string()
|
||||
.contains("User not found"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_inactive_user() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_inactive");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "User", false).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response_json["message"],
|
||||
"Account not active, please verify your email"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_successful_mentor_login() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_mentor_login");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "Mentor", true).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_mentor_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::OK);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
|
||||
assert!(response_json.get("data").is_some());
|
||||
assert_eq!(response_json["data"]["user"]["role"]["name"], "Mentor");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mentor_login_with_non_mentor_user() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_user_not_mentor");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_mentor_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::FORBIDDEN);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response_json["message"],
|
||||
"User does not have mentor privileges"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mentor_login_with_inactive_mentor() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_inactive_mentor");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "Mentor", false).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_mentor_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::BAD_REQUEST);
|
||||
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response_json["message"],
|
||||
"Account not active, please verify your email"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_creates_user_cache() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_cache");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, _) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::OK);
|
||||
|
||||
// Verify user was cached
|
||||
let auth_repo = imphnen_iam::AuthRepository::new(&state);
|
||||
let cached_user = auth_repo.query_get_stored_user(email.clone()).await;
|
||||
assert!(cached_user.is_ok());
|
||||
assert_eq!(cached_user.unwrap().email, email);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_special_characters_in_email() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test+special");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, _) = response.into_parts();
|
||||
|
||||
assert_eq!(parts.status, StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login_with_case_sensitive_email() {
|
||||
let state = setup_test_environment().await;
|
||||
let email = generate_unique_email("test_case");
|
||||
let password = "TestPass123!";
|
||||
|
||||
create_test_user_with_role(&state, &email, password, "User", true).await;
|
||||
|
||||
let login_dto = AuthLoginRequestDto {
|
||||
email: email.to_uppercase(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
|
||||
let response = AuthService::mutation_login(login_dto, &state).await;
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
// Email should be case-sensitive
|
||||
assert_eq!(parts.status, StatusCode::UNAUTHORIZED);
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
|
||||
assert!(response_json["message"]
|
||||
.to_string()
|
||||
.contains("User not found"));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
#[cfg(test)]
|
||||
mod auth_repository_test {
|
||||
use crate::{
|
||||
AuthOtpSchema, AuthRepository, ResourceEnum, UsersRepository, UsersSchema,
|
||||
create_mock_app_state, generate_unique_email, get_iso_date, get_role_id,
|
||||
generate_unique_email,
|
||||
get_iso_date,
|
||||
get_role_id,
|
||||
make_thing,
|
||||
setup_all_test_environment, // Import the new setup function
|
||||
AuthOtpSchema,
|
||||
AuthRepository,
|
||||
ResourceEnum,
|
||||
UsersRepository,
|
||||
UsersSchema,
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use imphnen_iam::{AppState, RolesDetailQueryDto, UsersDetailQueryDto};
|
||||
@@ -22,6 +29,7 @@ mod auth_repository_test {
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
role: make_thing("app_roles", &get_role_id(state).await),
|
||||
mentor_id: None,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
@@ -29,27 +37,35 @@ mod auth_repository_test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_and_get_user() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let email = generate_unique_email("forgot");
|
||||
let user = create_mock_user(&app_state, &email).await;
|
||||
let mut user = create_mock_user(&app_state, &email).await;
|
||||
user.role = make_thing("app_roles", &get_role_id(&app_state).await);
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
let create_user = user_repo.query_create_user(user.clone()).await;
|
||||
assert!(create_user.is_ok());
|
||||
let user_data = user_repo
|
||||
.query_user_by_email(email.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let user_data = user_repo.query_user_by_email(email).await;
|
||||
assert!(
|
||||
user_data.is_ok(),
|
||||
"Failed to get user by email: {:?}",
|
||||
user_data.err()
|
||||
);
|
||||
let user_data = user_data.unwrap();
|
||||
let store = repo.query_store_user(user_data.clone()).await;
|
||||
assert!(store.is_ok());
|
||||
assert!(store.is_ok(), "Failed to store user: {:?}", store.err());
|
||||
let fetched = repo.query_get_stored_user(user.email.clone()).await;
|
||||
assert!(fetched.is_ok());
|
||||
assert!(
|
||||
fetched.is_ok(),
|
||||
"Failed to fetch stored user: {:?}",
|
||||
fetched.err()
|
||||
);
|
||||
assert_eq!(fetched.unwrap().email, user.email);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_stored_user() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let auth_repo = AuthRepository::new(&state);
|
||||
let email = "delete_me@example.com".to_string();
|
||||
let mock_user = UsersDetailQueryDto {
|
||||
@@ -71,15 +87,20 @@ mod auth_repository_test {
|
||||
},
|
||||
is_deleted: false,
|
||||
password: "".into(),
|
||||
mentor_id: None,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
let _: Option<UsersDetailQueryDto> = state
|
||||
let created: Result<Option<UsersDetailQueryDto>, surrealdb::Error> = state
|
||||
.surrealdb_mem
|
||||
.create((ResourceEnum::UsersCache.to_string(), email.clone()))
|
||||
.content(mock_user)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
assert!(
|
||||
created.is_ok(),
|
||||
"Failed to create mock user: {:?}",
|
||||
created.err()
|
||||
);
|
||||
let result = auth_repo.query_delete_stored_user(email.clone()).await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
@@ -91,101 +112,120 @@ mod auth_repository_test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_and_get_otp() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let email = "otp_user@example.com".to_string();
|
||||
let otp = 123456;
|
||||
let stored = repo.query_store_otp(email.clone(), otp).await;
|
||||
assert!(stored.is_ok());
|
||||
assert!(stored.is_ok(), "Failed to store OTP: {:?}", stored.err());
|
||||
let fetched = repo.query_get_stored_otp(email.clone()).await;
|
||||
assert!(fetched.is_ok());
|
||||
assert!(fetched.is_ok(), "Failed to fetch OTP: {:?}", fetched.err());
|
||||
assert_eq!(fetched.unwrap(), otp);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_stored_otp() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let email = "otp_del@example.com".to_string();
|
||||
let otp = 654321;
|
||||
repo.query_store_otp(email.clone(), otp).await.unwrap();
|
||||
let store_res = repo.query_store_otp(email.clone(), otp).await;
|
||||
assert!(
|
||||
store_res.is_ok(),
|
||||
"Failed to store OTP: {:?}",
|
||||
store_res.err()
|
||||
);
|
||||
let deleted = repo.query_delete_stored_otp(email.clone()).await;
|
||||
assert!(deleted.is_ok());
|
||||
assert!(deleted.is_ok(), "Failed to delete OTP: {:?}", deleted.err());
|
||||
let fetched = repo.query_get_stored_otp(email.clone()).await;
|
||||
assert!(fetched.is_err());
|
||||
assert!(
|
||||
fetched.is_err(),
|
||||
"OTP should be deleted, but got: {fetched:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_expired_otp() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let email = "expired_otp@example.com".to_string();
|
||||
let otp = 789012;
|
||||
let table = ResourceEnum::OtpCache.to_string();
|
||||
let expires_at = Utc::now() - Duration::seconds(1);
|
||||
let _: Option<AuthOtpSchema> = repo
|
||||
let created: Result<Option<AuthOtpSchema>, surrealdb::Error> = repo
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.create((table.clone(), email.as_str()))
|
||||
.content(AuthOtpSchema { otp, expires_at })
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
assert!(
|
||||
created.is_ok(),
|
||||
"Failed to create expired OTP: {:?}",
|
||||
created.err()
|
||||
);
|
||||
let result = repo.query_get_stored_otp(email.clone()).await;
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expired OTP should not be retrievable, got: {result:?}"
|
||||
);
|
||||
if let Some(err) = result.err() {
|
||||
assert!(
|
||||
err.to_string().contains("OTP expired"),
|
||||
"Expected 'OTP expired' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_non_existent_stored_user_should_fail() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let result = repo
|
||||
.query_get_stored_user("not_found@example.com".into())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
if let Some(err) = result.err() {
|
||||
assert!(
|
||||
err.to_string().contains("No stored user data found"),
|
||||
"Expected 'No stored user data found' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_non_existent_user_should_fail() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let result = repo
|
||||
.query_delete_stored_user("ghost@example.com".into())
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_expired_otp_should_fail() {
|
||||
use chrono::Duration;
|
||||
let app_state = create_mock_app_state().await;
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let email = "expired_otp@example.com";
|
||||
let expired_time = chrono::Utc::now() - Duration::seconds(10);
|
||||
let otp = 123456;
|
||||
let _: Option<AuthOtpSchema> = repo
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.create((ResourceEnum::OtpCache.to_string(), email))
|
||||
.content(AuthOtpSchema {
|
||||
otp,
|
||||
expires_at: expired_time,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = repo.query_get_stored_otp(email.into()).await;
|
||||
assert!(result.is_err());
|
||||
if let Some(err) = result.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Failed delete stored user"),
|
||||
"Expected 'Failed delete stored user' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_and_get_valid_otp() {
|
||||
let app_state = create_mock_app_state().await;
|
||||
let app_state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = AuthRepository::new(&app_state);
|
||||
let email = "valid_otp@example.com";
|
||||
let otp = 654321;
|
||||
let store_result = repo.query_store_otp(email.into(), otp).await;
|
||||
assert!(store_result.is_ok());
|
||||
assert!(
|
||||
store_result.is_ok(),
|
||||
"Failed to store valid OTP: {:?}",
|
||||
store_result.err()
|
||||
);
|
||||
let get_result = repo.query_get_stored_otp(email.into()).await;
|
||||
assert!(
|
||||
get_result.is_ok(),
|
||||
"Failed to get valid OTP: {:?}",
|
||||
get_result.err()
|
||||
);
|
||||
assert_eq!(get_result.unwrap(), otp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
#[cfg(test)]
|
||||
pub mod auth_login_tests;
|
||||
#[cfg(test)]
|
||||
pub mod auth_repository_test;
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
use crate::{
|
||||
create_mock_app_state,
|
||||
get_iso_date, // Import the new setup function and get_iso_date
|
||||
permissions::{PermissionsRepository, PermissionsSchema},
|
||||
setup_all_test_environment,
|
||||
};
|
||||
use chrono::Utc;
|
||||
|
||||
fn create_dummy_permission(name: &str) -> PermissionsSchema {
|
||||
PermissionsSchema {
|
||||
name: name.to_string(),
|
||||
created_at: Some(get_iso_date()), // Ensure created_at is always set
|
||||
updated_at: Some(get_iso_date()), // Ensure updated_at is always set
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_permission_should_succeed() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
let permission = create_dummy_permission("Test Permission");
|
||||
let result = repo.query_create_permission(permission).await;
|
||||
@@ -22,7 +25,7 @@ async fn test_create_permission_should_succeed() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_permission_list_should_return_data() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
|
||||
let _ = repo
|
||||
@@ -45,7 +48,7 @@ async fn test_query_permission_list_should_return_data() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_permission_by_id_should_succeed() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
let permission = create_dummy_permission("Detail");
|
||||
let _ = repo.query_create_permission(permission.clone()).await;
|
||||
@@ -56,7 +59,7 @@ async fn test_query_permission_by_id_should_succeed() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_permission_should_succeed() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
let mut permission = create_dummy_permission("Update This");
|
||||
let _ = repo.query_create_permission(permission.clone()).await;
|
||||
@@ -68,7 +71,7 @@ async fn test_update_permission_should_succeed() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_permission_should_succeed() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
let permission = create_dummy_permission("To Be Deleted");
|
||||
let _ = repo.query_create_permission(permission.clone()).await;
|
||||
@@ -79,33 +82,64 @@ async fn test_delete_permission_should_succeed() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_permission_should_fail_if_already_deleted() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
let permission = create_dummy_permission("Delete Twice");
|
||||
let _ = repo.query_create_permission(permission.clone()).await;
|
||||
let id = permission.id.id.to_raw();
|
||||
let _ = repo.query_delete_permission(id.clone()).await;
|
||||
let second = repo.query_delete_permission(id).await;
|
||||
assert!(second.is_err(), "Should fail on second delete");
|
||||
let delete_result = repo.query_delete_permission(id.clone()).await;
|
||||
assert!(
|
||||
delete_result.is_ok(),
|
||||
"Initial delete failed: {:?}",
|
||||
delete_result.err()
|
||||
);
|
||||
let second_delete_result = repo.query_delete_permission(id).await;
|
||||
assert!(
|
||||
second_delete_result.is_err(),
|
||||
"Should fail on second delete"
|
||||
);
|
||||
if let Some(err) = second_delete_result.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Permission not found"),
|
||||
"Expected 'Permission not found' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_permission_should_fail_if_deleted() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
let mut permission = create_dummy_permission("To Be Updated Then Deleted");
|
||||
let _ = repo.query_create_permission(permission.clone()).await;
|
||||
let id = permission.id.id.to_raw();
|
||||
let _ = repo.query_delete_permission(id.clone()).await;
|
||||
let delete_result = repo.query_delete_permission(id.clone()).await;
|
||||
assert!(
|
||||
delete_result.is_ok(),
|
||||
"Initial delete failed: {:?}",
|
||||
delete_result.err()
|
||||
);
|
||||
permission.name = "Try Update".into();
|
||||
let result = repo.query_update_permission(permission).await;
|
||||
assert!(result.is_err(), "Update on deleted should fail");
|
||||
if let Some(err) = result.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Permission not found"),
|
||||
"Expected 'Permission not found' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_permission_by_id_should_fail_if_not_found() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await; // Use the new setup function
|
||||
let repo = PermissionsRepository::new(&state);
|
||||
let result = repo.query_permission_by_id("non-existent-id".into()).await;
|
||||
assert!(result.is_err(), "Expected error for not found id");
|
||||
if let Some(err) = result.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Permission not found"),
|
||||
"Expected 'Permission not found' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
ResourceEnum, create_mock_app_state, get_iso_date, make_thing,
|
||||
get_iso_date, make_thing,
|
||||
permissions::{
|
||||
permissions_repository::PermissionsRepository,
|
||||
permissions_schema::PermissionsSchema,
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
roles_dto::{RolesRequestCreateDto, RolesRequestUpdateDto},
|
||||
roles_repository::RolesRepository,
|
||||
},
|
||||
setup_all_test_environment, ResourceEnum,
|
||||
};
|
||||
use surrealdb::Uuid;
|
||||
|
||||
@@ -17,58 +18,92 @@ fn generate_unique_name(prefix: &str) -> String {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_create_role_should_succeed() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await;
|
||||
let perm_repo = PermissionsRepository::new(&state);
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let perm_id = Uuid::new_v4().to_string();
|
||||
let permission = PermissionsSchema {
|
||||
id: make_thing(&ResourceEnum::Permissions.to_string(), &perm_id.clone()),
|
||||
id: make_thing(&ResourceEnum::Permissions.to_string(), &perm_id),
|
||||
name: generate_unique_name("read_quiz"),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
perm_repo.query_create_permission(permission).await.unwrap();
|
||||
let perm_res = perm_repo.query_create_permission(permission).await;
|
||||
assert!(
|
||||
perm_res.is_ok(),
|
||||
"Failed to create permission: {:?}",
|
||||
perm_res.err()
|
||||
);
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: generate_unique_name("user"),
|
||||
permissions: vec![perm_id.clone()],
|
||||
};
|
||||
let result = role_repo.query_create_role(payload).await;
|
||||
assert!(result.is_ok());
|
||||
assert!(result.is_ok(), "Failed to create role: {:?}", result.err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_role_by_name_should_return_data() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let name = generate_unique_name("viewer");
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: name.clone(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo.query_role_by_name(name.clone()).await.unwrap();
|
||||
let create_res = role_repo.query_create_role(payload.clone()).await;
|
||||
assert!(
|
||||
create_res.is_ok(),
|
||||
"Failed to create role: {:?}",
|
||||
create_res.err()
|
||||
);
|
||||
let role = role_repo.query_role_by_name(name.clone()).await;
|
||||
assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err());
|
||||
let role = role.unwrap();
|
||||
assert_eq!(role.name, name.clone());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_role_by_id_should_return_data() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
|
||||
let name = generate_unique_name("tester");
|
||||
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: name.clone(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo.query_role_by_name(name.clone()).await.unwrap();
|
||||
let result = role_repo.query_role_by_id(role.id).await.unwrap();
|
||||
assert_eq!(result.name, name.clone());
|
||||
|
||||
let create_res = role_repo.query_create_role(payload.clone()).await;
|
||||
|
||||
assert!(
|
||||
create_res.is_ok(),
|
||||
"Failed to create role: {:?}",
|
||||
create_res.err()
|
||||
);
|
||||
|
||||
let role = role_repo.query_role_by_name(name.clone()).await;
|
||||
|
||||
assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err());
|
||||
let role = role.unwrap();
|
||||
|
||||
let result = role_repo.query_role_by_id(role.id.clone()).await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to get role by id: {:?}",
|
||||
result.err()
|
||||
);
|
||||
let result_role = result.unwrap();
|
||||
|
||||
assert_eq!(result_role.name, name.clone());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_update_role_should_update_name_and_permissions() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await;
|
||||
let repo = RolesRepository::new(&state);
|
||||
let perm_repo = PermissionsRepository::new(&state);
|
||||
let original_perm_id = Uuid::new_v4().to_string();
|
||||
@@ -77,22 +112,32 @@ async fn test_query_update_role_should_update_name_and_permissions() {
|
||||
name: generate_unique_name("original_permission"),
|
||||
is_deleted: false,
|
||||
created_at: Some(crate::get_iso_date()),
|
||||
updated_at: None,
|
||||
updated_at: Some(crate::get_iso_date()),
|
||||
};
|
||||
perm_repo
|
||||
.query_create_permission(original_perm)
|
||||
.await
|
||||
.unwrap();
|
||||
let perm_res = perm_repo.query_create_permission(original_perm).await;
|
||||
assert!(
|
||||
perm_res.is_ok(),
|
||||
"Failed to create original permission: {:?}",
|
||||
perm_res.err()
|
||||
);
|
||||
let role_upadate_name = generate_unique_name("role_for_update");
|
||||
let create_payload = RolesRequestCreateDto {
|
||||
name: role_upadate_name.clone(),
|
||||
permissions: vec![original_perm_id.clone()],
|
||||
};
|
||||
repo.query_create_role(create_payload).await.unwrap();
|
||||
let existing_role = repo
|
||||
.query_role_by_name(role_upadate_name.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let create_res = repo.query_create_role(create_payload).await;
|
||||
assert!(
|
||||
create_res.is_ok(),
|
||||
"Failed to create role: {:?}",
|
||||
create_res.err()
|
||||
);
|
||||
let existing_role = repo.query_role_by_name(role_upadate_name.clone()).await;
|
||||
assert!(
|
||||
existing_role.is_ok(),
|
||||
"Failed to get role by name: {:?}",
|
||||
existing_role.err()
|
||||
);
|
||||
let existing_role = existing_role.unwrap();
|
||||
let existing_role_id = existing_role.id.clone();
|
||||
let new_perm_id = Uuid::new_v4().to_string();
|
||||
let new_perm = PermissionsSchema {
|
||||
@@ -100,10 +145,15 @@ async fn test_query_update_role_should_update_name_and_permissions() {
|
||||
name: "New Permission".into(),
|
||||
is_deleted: false,
|
||||
created_at: Some(crate::get_iso_date()),
|
||||
updated_at: None,
|
||||
updated_at: Some(crate::get_iso_date()),
|
||||
};
|
||||
let new_role_name = generate_unique_name("updated_role_name");
|
||||
perm_repo.query_create_permission(new_perm).await.unwrap();
|
||||
let perm_res = perm_repo.query_create_permission(new_perm).await;
|
||||
assert!(
|
||||
perm_res.is_ok(),
|
||||
"Failed to create new permission: {:?}",
|
||||
perm_res.err()
|
||||
);
|
||||
let update_payload = RolesRequestUpdateDto {
|
||||
name: Some(new_role_name.clone()),
|
||||
permissions: Some(vec![new_perm_id.clone()]),
|
||||
@@ -112,34 +162,57 @@ async fn test_query_update_role_should_update_name_and_permissions() {
|
||||
let update_result = repo
|
||||
.query_update_role(existing_role_id.clone(), update_payload)
|
||||
.await;
|
||||
assert!(update_result.is_ok());
|
||||
let updated = repo
|
||||
.query_role_by_id(existing_role_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, new_role_name.clone());
|
||||
assert!(
|
||||
update_result.is_ok(),
|
||||
"Failed to update role: {:?}",
|
||||
update_result.err()
|
||||
);
|
||||
let updated = repo.query_role_by_id(existing_role_id.clone()).await;
|
||||
assert!(
|
||||
updated.is_ok(),
|
||||
"Failed to get updated role: {:?}",
|
||||
updated.err()
|
||||
);
|
||||
assert_eq!(updated.unwrap().name, new_role_name.clone());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_delete_role_should_soft_delete() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let name = generate_unique_name("temporary");
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: name.clone(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo.query_role_by_name(name.clone()).await.unwrap();
|
||||
let create_res = role_repo.query_create_role(payload.clone()).await;
|
||||
assert!(
|
||||
create_res.is_ok(),
|
||||
"Failed to create role: {:?}",
|
||||
create_res.err()
|
||||
);
|
||||
let role = role_repo.query_role_by_name(name.clone()).await;
|
||||
assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err());
|
||||
let role = role.unwrap();
|
||||
let result = role_repo.query_delete_role(role.id.clone()).await;
|
||||
assert!(result.is_ok());
|
||||
assert!(result.is_ok(), "Failed to delete role: {:?}", result.err());
|
||||
let deleted = role_repo.query_role_by_id(role.id).await;
|
||||
assert!(deleted.is_err());
|
||||
assert!(
|
||||
deleted.is_err(),
|
||||
"Role should be deleted, but got: {deleted:?}"
|
||||
);
|
||||
if let Some(err) = deleted.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Role not found"),
|
||||
"Expected 'Role not found' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_provided()
|
||||
{
|
||||
let state = create_mock_app_state().await;
|
||||
async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_provided(
|
||||
) {
|
||||
let state = setup_all_test_environment().await;
|
||||
let repo = RolesRepository::new(&state);
|
||||
let perm_repo = PermissionsRepository::new(&state);
|
||||
let perm_id = Uuid::new_v4().to_string();
|
||||
@@ -148,18 +221,31 @@ async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_
|
||||
name: "Permission for Fallback".into(),
|
||||
is_deleted: false,
|
||||
created_at: Some(crate::get_iso_date()),
|
||||
updated_at: None,
|
||||
updated_at: Some(crate::get_iso_date()),
|
||||
};
|
||||
perm_repo.query_create_permission(permission).await.unwrap();
|
||||
let perm_res = perm_repo.query_create_permission(permission).await;
|
||||
assert!(
|
||||
perm_res.is_ok(),
|
||||
"Failed to create permission: {:?}",
|
||||
perm_res.err()
|
||||
);
|
||||
let create_payload = RolesRequestCreateDto {
|
||||
name: "Role With Permission".into(),
|
||||
permissions: vec![perm_id.clone()],
|
||||
};
|
||||
repo.query_create_role(create_payload).await.unwrap();
|
||||
let existing = repo
|
||||
.query_role_by_name("Role With Permission".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let create_res = repo.query_create_role(create_payload).await;
|
||||
assert!(
|
||||
create_res.is_ok(),
|
||||
"Failed to create role: {:?}",
|
||||
create_res.err()
|
||||
);
|
||||
let existing = repo.query_role_by_name("Role With Permission".into()).await;
|
||||
assert!(
|
||||
existing.is_ok(),
|
||||
"Failed to get role by name: {:?}",
|
||||
existing.err()
|
||||
);
|
||||
let existing = existing.unwrap();
|
||||
let existing_id = existing.id.clone();
|
||||
let update_payload = RolesRequestUpdateDto {
|
||||
name: Some("Updated Role Name".into()),
|
||||
@@ -169,29 +255,61 @@ async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_
|
||||
let update_res = repo
|
||||
.query_update_role(existing_id.clone(), update_payload)
|
||||
.await;
|
||||
assert!(update_res.is_ok());
|
||||
assert!(
|
||||
update_res.is_ok(),
|
||||
"Failed to update role (fallback): {:?}",
|
||||
update_res.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_role_by_name_should_fail_if_not_found() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let result = role_repo.query_role_by_name("ghost-role".into()).await;
|
||||
assert!(result.is_err());
|
||||
if let Some(err) = result.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Role not found"),
|
||||
"Expected 'Role not found' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_delete_role_should_fail_if_already_deleted() {
|
||||
let state = create_mock_app_state().await;
|
||||
let state = setup_all_test_environment().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let name = generate_unique_name("soft_delete_test");
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: name.clone(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo.query_role_by_name(name.clone()).await.unwrap();
|
||||
role_repo.query_delete_role(role.id.clone()).await.unwrap();
|
||||
let result = role_repo.query_delete_role(role.id);
|
||||
assert!(result.await.is_err());
|
||||
let create_res = role_repo.query_create_role(payload.clone()).await;
|
||||
assert!(
|
||||
create_res.is_ok(),
|
||||
"Failed to create role: {:?}",
|
||||
create_res.err()
|
||||
);
|
||||
let role = role_repo.query_role_by_name(name.clone()).await;
|
||||
assert!(role.is_ok(), "Failed to get role by name: {:?}", role.err());
|
||||
let role = role.unwrap();
|
||||
let del_res = role_repo.query_delete_role(role.id.clone()).await;
|
||||
assert!(
|
||||
del_res.is_ok(),
|
||||
"Failed to delete role: {:?}",
|
||||
del_res.err()
|
||||
);
|
||||
let result_fut = role_repo.query_delete_role(role.id);
|
||||
let result_val = result_fut.await;
|
||||
assert!(
|
||||
result_val.is_err(),
|
||||
"Role should already be deleted, but got: {result_val:?}"
|
||||
);
|
||||
if let Some(err) = result_val.err() {
|
||||
assert!(
|
||||
err.to_string().contains("Role not found"),
|
||||
"Expected 'Role not found' error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user