Compare commits

...
Author SHA1 Message Date
MythEclipse 1a2e0c58b6 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.
2025-07-21 21:29:04 +07:00
Maulana Sodiqin e66f1f1634 chore: remove deprecated deployment 2025-06-30 09:28:11 +07:00
Maulana Sodiqin 6f45cf80c8 Merge pull request #38 from IMPHNEN/feat/client-dynamic-wss-or-ws
refactor: update surrealdb client usage to support 'any' engine type
2025-06-30 09:27:16 +07:00
104 changed files with 7851 additions and 1574 deletions
+24 -8
View File
@@ -1,8 +1,24 @@
PORT= RUST_ENV=development
SURREALDB_URL= PORT=4099
SURREALDB_USERNAME= SURREALDB_URL=ws://localhost:8000/rpc
SURREALDB_PASSWORD= SURREALDB_USERNAME=root
SURREALDB_NAMESPACE= SURREALDB_PASSWORD=root
SURREALDB_DBNAME= SURREALDB_NAMESPACE=test
ACCESS_TOKEN_SECRET= SURREALDB_DBNAME=test
REFRESH_TOKEN_SECRET= 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
-65
View File
@@ -1,65 +0,0 @@
name: Deploy
on:
push:
branches:
- develop
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Build the project
run: cargo build --release
- name: Stop service on VPS before upload
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
script: |
set -e
echo "Stopping the service before uploading the binary"
sudo systemctl stop imphnen-backend-service
- name: Upload artifact to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
source: ./target/release/*
target: /opt/imphnen-backend-service/imphnen-backend-service
rm: true
overwrite: true
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
script: |
set -e
echo "Restarting the service"
sudo systemctl daemon-reload
sudo systemctl restart imphnen-backend-service
echo "Deployment completed successfully"
Generated
+727 -578
View File
File diff suppressed because it is too large Load Diff
+32 -5
View File
@@ -20,28 +20,55 @@ axum = { version = "0.8.4", features = ["multipart"] }
log = "0.4.25" log = "0.4.25"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138" 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"] } argon2 = { version = "0.5.3", features = ["password-hash"] }
jsonwebtoken = "9.3.1" jsonwebtoken = "9.3.1"
chrono = "0.4.41" chrono = "0.4.41"
utoipa = { version = "5.3.1", features = ["axum_extras"] } utoipa = { version = "5.3.1", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] } 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" thiserror = "2.0.12"
anyhow = "1.0.98" anyhow = "1.0.98"
rand = { version = "0.9.1", features = ["std", "alloc"] } rand = { version = "0.9.1", features = ["std", "alloc"] }
rand_distr = "0.5.1" rand_distr = "0.5.1"
tower-http = { version = "0.6.4", features = ["cors"] } tower-http = { version = "0.6.4", features = ["cors", "trace"] }
validator = { version = "0.12", features = ["derive"] } http-body-util = "0.1.1"
validator = { version = "0.20.0", features = ["derive"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
regex = "1.11.1" regex = "1.11.1"
axum-test = "17.2.0" axum-test = "17.2.0"
fancy-regex = "0.14.0" fancy-regex = "0.15.0"
futures = "0.3.31" futures = "0.3.31"
tower = "0.5.2" tower = "0.5.2"
env_logger = "0.11.8" env_logger = "0.11.8"
dotenvy = "0.15.7" 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] [profile.release]
lto = "fat" lto = "fat"
codegen-units = 1 codegen-units = 1
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.85-alpine AS builder FROM rust:1.86-alpine AS builder
RUN apk add --no-cache \ RUN apk add --no-cache \
curl \ curl \
+13 -6
View File
@@ -4,12 +4,13 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-gateway = { version = "0.1.0", path = "../imphnen-gateway" } imphnen-gateway.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } imphnen-iam.workspace = true
imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" } imphnen-cms.workspace = true
imphnen-dimentorin.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -26,3 +27,9 @@ anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
env_logger.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
View File
@@ -1,4 +1,3 @@
use env_logger;
use imphnen_gateway::gateway_service; use imphnen_gateway::gateway_service;
use imphnen_libs::axum_init; use imphnen_libs::axum_init;
+169
View File
@@ -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(())
}
+21 -10
View File
@@ -1,9 +1,9 @@
use imphnen_cms::v1::landing::events::events_schema::EventsSchema; use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, Env}; use imphnen_utils::{get_iso_date, Env};
use std::error::Error; use std::error::Error;
use surrealdb::engine::any; use surrealdb::engine::any;
use surrealdb::{opt::auth::Root, sql::Thing}; use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
use imphnen_libs::enviroment::load_env;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); load_env();
@@ -20,7 +20,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
let events = vec![ let events = vec![
( (
"e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o",
"Tech Conference 2025", "Tech Conference 2025",
"Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.", "Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.",
"https://techconf2025.example.com", "https://techconf2025.example.com",
@@ -31,7 +30,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
"2025-06-17T18:00:00Z", "2025-06-17T18:00:00Z",
), ),
( (
"f2b3c4d5-6e7f-8g9h-0i1j-2k3l4m5n6o7p",
"Online Web Development Workshop", "Online Web Development Workshop",
"Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.", "Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.",
"https://webdev-workshop.example.com", "https://webdev-workshop.example.com",
@@ -42,7 +40,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
"2025-07-10T17:00:00Z", "2025-07-10T17:00:00Z",
), ),
( (
"g3c4d5e6-7f8g-9h0i-1j2k-3l4m5n6o7p8q",
"Startup Pitch Competition", "Startup Pitch Competition",
"Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.", "Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.",
"https://startup-pitch.example.com", "https://startup-pitch.example.com",
@@ -53,7 +50,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
"2025-08-05T16:00:00Z", "2025-08-05T16:00:00Z",
), ),
( (
"h4d5e6f7-8g9h-0i1j-2k3l-4m5n6o7p8q9r",
"Digital Marketing Masterclass", "Digital Marketing Masterclass",
"Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.", "Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.",
"https://digital-marketing.example.com", "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 { let event = EventsSchema {
id: Thing::from(("app_events", id)), id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID
name: name.into(), name: name.into(),
description: description.into(), description: description.into(),
detail_link: detail_link.into(), detail_link: detail_link.into(),
@@ -81,11 +88,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
updated_at: get_iso_date(), 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) .content(event)
.await?; .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"); 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(())
}
File diff suppressed because one or more lines are too long
+10 -1
View File
@@ -48,6 +48,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
PermissionsEnum::ReadDetailGachaRolls, PermissionsEnum::ReadDetailGachaRolls,
PermissionsEnum::CreateGachaRolls, PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls, 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") db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
.bind(("id", permission.id())) .bind(("id", permission.id()))
@@ -61,9 +69,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
}), }),
)) ))
.await?; .await?;
println!("✅ Inserted: {}", permission.to_string()); println!("✅ Inserted: {permission}");
} }
println!("✅ All Permissions seeded"); println!("✅ All Permissions seeded");
Ok(()) Ok(())
} }
+12 -3
View File
@@ -1,9 +1,9 @@
use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, Env}; use imphnen_utils::{get_iso_date, Env};
use serde_json::json; use serde_json::json;
use std::error::Error; use std::error::Error;
use surrealdb::opt::auth::Root;
use imphnen_libs::enviroment::load_env;
use surrealdb::engine::any; use surrealdb::engine::any;
use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); load_env();
@@ -49,9 +49,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
None, None,
Some("2025-02-22T15:38:39.868306+00"), 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 { 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") db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
.bind(("id", id)) .bind(("id", id))
.bind(( .bind((
@@ -65,7 +74,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
}), }),
)) ))
.await?; .await?;
println!("✅ Inserted role: {}", name); println!("✅ Inserted role: {name}");
} }
println!("✅ All Roles seeded"); println!("✅ All Roles seeded");
Ok(()) Ok(())
+117 -41
View File
@@ -1,8 +1,9 @@
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum}; 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 imphnen_libs::enviroment::load_env;
use std::error::Error;
use surrealdb::engine::any;
use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); load_env();
@@ -16,43 +17,118 @@ async fn main() -> Result<(), Box<dyn Error>> {
db.use_ns(env.surrealdb_namespace) db.use_ns(env.surrealdb_namespace)
.use_db(env.surrealdb_dbname) .use_db(env.surrealdb_dbname)
.await?; .await?;
let permission_refs_admin: Vec<_> = [
PermissionsEnum::ReadListUsers, let roles_permissions = vec![
PermissionsEnum::ReadDetailUsers, (
PermissionsEnum::CreateUsers, "f6b03f25-e416-4893-ac88-caaa690afb07",
PermissionsEnum::DeleteUsers, vec![
PermissionsEnum::UpdateUsers, PermissionsEnum::ReadListUsers,
PermissionsEnum::ActivateUsers, PermissionsEnum::ReadDetailUsers,
PermissionsEnum::ReadListRoles, PermissionsEnum::CreateUsers,
PermissionsEnum::ReadDetailRoles, PermissionsEnum::DeleteUsers,
PermissionsEnum::CreateRoles, PermissionsEnum::UpdateUsers,
PermissionsEnum::DeleteRoles, PermissionsEnum::ActivateUsers,
PermissionsEnum::UpdateRoles, PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadListPermissions, PermissionsEnum::ReadDetailRoles,
PermissionsEnum::ReadDetailPermissions, PermissionsEnum::CreateRoles,
PermissionsEnum::CreatePermissions, PermissionsEnum::DeleteRoles,
PermissionsEnum::DeletePermissions, PermissionsEnum::UpdateRoles,
PermissionsEnum::UpdatePermissions, PermissionsEnum::ReadListPermissions,
PermissionsEnum::CreateGachaClaims, PermissionsEnum::ReadDetailPermissions,
PermissionsEnum::ReadDetailGachaClaims, PermissionsEnum::CreatePermissions,
PermissionsEnum::ReadListGachaItems, PermissionsEnum::DeletePermissions,
PermissionsEnum::ReadDetailGachaItems, PermissionsEnum::UpdatePermissions,
PermissionsEnum::CreateGachaItems, PermissionsEnum::CreateGachaClaims,
PermissionsEnum::DeleteGachaItems, PermissionsEnum::ReadDetailGachaClaims,
PermissionsEnum::UpdateGachaItems, PermissionsEnum::ReadListGachaItems,
PermissionsEnum::ReadDetailGachaRolls, PermissionsEnum::ReadDetailGachaItems,
PermissionsEnum::CreateGachaRolls, PermissionsEnum::CreateGachaItems,
PermissionsEnum::ExecuteGachaRolls, PermissionsEnum::DeleteGachaItems,
] PermissionsEnum::UpdateGachaItems,
.iter() PermissionsEnum::ReadDetailGachaRolls,
.map(|perm| make_thing("app_permissions", perm.id())) PermissionsEnum::CreateGachaRolls,
.collect(); PermissionsEnum::ExecuteGachaRolls,
let admin_role_id = "f6b03f25-e416-4893-ac88-caaa690afb07"; PermissionsEnum::ReadListMentors,
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false") PermissionsEnum::ReadDetailMentors,
.bind(("role_id", admin_role_id)) PermissionsEnum::RegisterMentors,
.bind(("permissions", permission_refs_admin)) PermissionsEnum::UpdateMentors,
.bind(("updated_at", get_iso_date())) PermissionsEnum::VerifyMentors,
.await?; PermissionsEnum::DeleteMentors,
println!("✅ All permissions successfully added to Admin role"); ],
),
(
"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(()) Ok(())
} }
+7 -1
View File
@@ -2,6 +2,7 @@ use imphnen_iam::UsersSchema;
use imphnen_libs::enviroment::load_env; use imphnen_libs::enviroment::load_env;
use imphnen_utils::{get_iso_date, hash_password, Env}; use imphnen_utils::{get_iso_date, hash_password, Env};
use std::error::Error; use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing}; use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
@@ -40,6 +41,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
]; ];
for (id, email, fullname, role_id) in users { for (id, email, fullname, role_id) in users {
db.query("DELETE type::thing('app_users', $id)")
.bind(("id", id))
.await?;
let user = UsersSchema { let user = UsersSchema {
id: Thing::from(("app_users", id)), id: Thing::from(("app_users", id)),
fullname: fullname.into(), fullname: fullname.into(),
@@ -49,6 +54,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
phone_number: "081234567890".into(), phone_number: "081234567890".into(),
is_active: true, is_active: true,
is_deleted: false, is_deleted: false,
mentor_id: None,
gender: None, gender: None,
birthdate: None, birthdate: None,
role: Thing::from(("app_roles", role_id)), role: Thing::from(("app_roles", role_id)),
@@ -60,7 +66,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.content(user) .content(user)
.await?; .await?;
println!("✅ Inserted user: {} ({})", fullname, email); println!("✅ Inserted user: {fullname} ({email})");
} }
println!("✅ All Users seeded"); println!("✅ All Users seeded");
+2 -1
View File
@@ -20,7 +20,8 @@ fn main() -> Result<(), Box<dyn Error>> {
run_seed("seed_roles_permissions")?; run_seed("seed_roles_permissions")?;
run_seed("seed_users")?; run_seed("seed_users")?;
run_seed("seed_events")?; run_seed("seed_events")?;
run_seed("seed_gacha_rolls")?;
run_seed("seed_mentor_user")?;
println!("\n✅ All seeding completed successfully."); println!("\n✅ All seeding completed successfully.");
Ok(()) Ok(())
} }
+2
View File
@@ -3,6 +3,8 @@ use imphnen_libs::axum_init;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt::init();
axum_init(|surrealdb_ws, surrealdb_mem| async { axum_init(|surrealdb_ws, surrealdb_mem| async {
gateway_service(surrealdb_ws, surrealdb_mem).await gateway_service(surrealdb_ws, surrealdb_mem).await
}) })
+9 -4
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } imphnen-iam.workspace = true
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -23,3 +23,8 @@ chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.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 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 serde::{Deserialize, Serialize};
use surrealdb::sql::Thing; use surrealdb::sql::Thing;
use utoipa::ToSchema; use utoipa::ToSchema;
use validator::Validate; 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)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct EventsCreateRequestDto { pub struct EventsCreateRequestDto {
#[validate(length(min = 1, message = "Name is required"))] #[validate(length(min = 1, message = "Name is required"))]
@@ -19,13 +17,10 @@ pub struct EventsCreateRequestDto {
#[validate(length(min = 1, message = "Description is required"))] #[validate(length(min = 1, message = "Description is required"))]
pub description: String, pub description: String,
#[validate(regex( #[validate(url(message = "Detail link must be a valid URL"))]
path = "VALID_URL_REGEX",
message = "Detail link must be a valid URL"
))]
pub detail_link: String, 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, pub price: f64,
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)] #[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
@@ -49,9 +44,11 @@ pub struct EventsUpdateRequestDto {
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)] #[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
pub start_date: DateTime<Utc>, pub start_date: DateTime<Utc>,
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
pub price: f64, pub price: f64,
pub is_online: bool, pub is_online: bool,
pub description: String, pub description: String,
#[validate(url(message = "Detail link must be a valid URL"))]
pub detail_link: String, pub detail_link: String,
pub location: Option<String>, pub location: Option<String>,
} }
@@ -2,6 +2,8 @@ use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date}; use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use std::time::Instant;
use tracing::instrument;
pub struct EventsRepository<'a> { pub struct EventsRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -12,17 +14,25 @@ impl<'a> EventsRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, meta), err)]
pub async fn query_event_list( pub async fn query_event_list(
&self, &self,
meta: MetaRequestDto, meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> { ) -> 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_select_fields(vec!["*"])
.with_pagination(meta.page, Some(10)) .with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref()) .with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build(); .build();
let res: Vec<EventsQueryDto> = let res: Vec<EventsQueryDto> =
self.state.surrealdb_ws.query(query).await?.take(0)?; self.state.surrealdb_ws.query(query).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_event_list' took: {elapsed:.2?}");
}
let data = ResponseListSuccessDto { let data = ResponseListSuccessDto {
data: res, data: res,
meta: None, meta: None,
@@ -30,8 +40,9 @@ impl<'a> EventsRepository<'a> {
Ok(data) Ok(data)
} }
// Get event by ID #[instrument(skip(self, id), err)]
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> { pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
.with_id(&id) .with_id(&id)
@@ -39,6 +50,12 @@ impl<'a> EventsRepository<'a> {
let sql = builder.build(); let sql = builder.build();
let result: Option<EventsQueryDto> = let result: Option<EventsQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_event_by_id' took: {elapsed:.2?}");
}
match result { match result {
Some(event) => { 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> { pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let record: Option<EventsSchema> = db let record: Option<EventsSchema> = db
.create(ResourceEnum::Events.to_string()) .create(ResourceEnum::Events.to_string())
.content(data) .content(data)
.await?; .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 { match record {
Some(_) => Ok("Success create event".into()), 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> { pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
// Cek apakah event ada
let existing = self.query_event_by_id(data.id.id.to_raw()).await?; let existing = self.query_event_by_id(data.id.id.to_raw()).await?;
if existing.is_deleted { if existing.is_deleted {
bail!("Event already deleted"); bail!("Event already deleted");
} }
// Merge field tertentu jika diperlukan
let merged = EventsSchema { let merged = EventsSchema {
created_at: existing.created_at, created_at: existing.created_at,
updated_at: get_iso_date(), updated_at: get_iso_date(),
@@ -84,6 +107,12 @@ impl<'a> EventsRepository<'a> {
let record_key = get_id(&merged.id)?; let record_key = get_id(&merged.id)?;
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?; let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_event' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success update event".into()), 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> { pub async fn query_delete_event(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let event = self.query_event_by_id(id).await?; let event = self.query_event_by_id(id).await?;
if event.is_deleted { if event.is_deleted {
@@ -104,6 +134,12 @@ impl<'a> EventsRepository<'a> {
.update(record_key) .update(record_key)
.merge(serde_json::json!({ "is_deleted": true })) .merge(serde_json::json!({ "is_deleted": true }))
.await?; .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 { match record {
Some(_) => Ok("Success delete event".into()), Some(_) => Ok("Success delete event".into()),
@@ -1,10 +1,12 @@
use imphnen_libs::ResourceEnum; use imphnen_libs::ResourceEnum;
use imphnen_utils::{get_iso_date, make_thing};
use serde::{Deserialize, Serialize};
use surrealdb::Uuid; use surrealdb::Uuid;
use surrealdb::sql::Thing; 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EventsSchema { pub struct EventsSchema {
@@ -1,86 +1,101 @@
use super::{ use super::{
events_dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto, EventsUpdateRequestDto}, events_dto::{
events_repository::EventsRepository, EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
events_schema::EventsSchema, 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 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; pub struct EventsService;
impl EventsService { impl EventsService {
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response { pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
let repo = EventsRepository::new(state); let repo = EventsRepository::new(state);
match repo.query_event_list(meta).await { match repo.query_event_list(meta).await {
Ok(data) => { Ok(data) => {
let items: Vec<EventsListItemDto> = data.data let items: Vec<EventsListItemDto> = data
.into_iter() .data
.filter(|e| !e.is_deleted) .into_iter()
.map(EventsQueryDto::from) .filter(|e| !e.is_deleted)
.collect(); .map(EventsQueryDto::from)
let response = ResponseListSuccessDto { .collect();
data: items, let response = ResponseListSuccessDto {
meta: data.meta, data: items,
}; meta: data.meta,
success_list_response(response) };
} success_list_response(response)
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), }
} Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
} }
}
pub async fn get_event_by_id(state: &AppState, id: String) -> Response { pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state); let repo = EventsRepository::new(state);
match repo.query_event_by_id(id).await { match repo.query_event_by_id(id).await {
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto { Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
data: EventsDetailItemDto { data: EventsDetailItemDto {
id: event.id.id.to_raw(), id: event.id.id.to_raw(),
name: event.name, name: event.name,
description: event.description, description: event.description,
detail_link: event.detail_link, detail_link: event.detail_link,
price: event.price, price: event.price,
is_online: event.is_online, is_online: event.is_online,
start_date: event.start_date, start_date: event.start_date,
end_date: event.end_date, end_date: event.end_date,
created_at: event.created_at, created_at: event.created_at,
updated_at: event.updated_at, updated_at: event.updated_at,
location: event.location, location: event.location,
}, },
}), }),
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"), Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
} }
} }
pub async fn create_event(state: &AppState, payload: EventsCreateRequestDto) -> Response { pub async fn create_event(
if let Err((status, message)) = validate_request(&payload) { state: &AppState,
return common_response(status, &message); payload: EventsCreateRequestDto,
} ) -> Response {
let repo = EventsRepository::new(state); if let Err((status, message)) = validate_request(&payload) {
let schema = EventsSchema::create(payload); return common_response(status, &message);
match repo.query_create_event(schema).await { }
Ok(msg) => common_response(StatusCode::CREATED, &msg), let repo = EventsRepository::new(state);
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), 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 { pub async fn update_event(
if let Err((status, message)) = validate_request(&payload) { state: &AppState,
return common_response(status, &message); id: String,
} payload: EventsUpdateRequestDto,
let repo = EventsRepository::new(state); ) -> Response {
let schema = EventsSchema::update(payload, id); if let Err((status, message)) = validate_request(&payload) {
match repo.query_update_event(schema).await { return common_response(status, &message);
Ok(msg) => common_response(StatusCode::OK, &msg), }
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), 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 { pub async fn delete_event(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state); let repo = EventsRepository::new(state);
match repo.query_delete_event(id).await { match repo.query_delete_event(id).await {
Ok(msg) => common_response(StatusCode::OK, &msg), Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
} }
} }
} }
@@ -73,7 +73,6 @@ pub async fn post_create_testimonial(
Extension(authenticated_user): Extension<UsersDetailQueryDto>, Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsCreateRequestDto>, Json(payload): Json<TestimonialsCreateRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
println!("Authenticated User Now: {:?}", authenticated_user);
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
} }
@@ -34,7 +34,7 @@ pub struct TestimonialsUpdateRequestDto {
pub struct TestimonialsListItemDto { pub struct TestimonialsListItemDto {
pub id: String, pub id: String,
pub user_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 role: String,
pub content: String, pub content: String,
pub created_at: String, pub created_at: String,
@@ -45,7 +45,7 @@ pub struct TestimonialsListItemDto {
pub struct TestimonialsDetailItemDto { pub struct TestimonialsDetailItemDto {
pub id: String, pub id: String,
pub user_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 role: String,
pub content: String, pub content: String,
pub created_at: String, pub created_at: String,
@@ -55,7 +55,7 @@ pub struct TestimonialsDetailItemDto {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestimonialsQueryDto { pub struct TestimonialsQueryDto {
pub id: Thing, pub id: Thing,
pub user: UsersSchema, // Change from Thing to UsersSchema pub user: UsersSchema,
pub role: String, pub role: String,
pub content: String, pub content: String,
pub is_deleted: bool, pub is_deleted: bool,
@@ -68,7 +68,7 @@ impl TestimonialsQueryDto {
TestimonialsListItemDto { TestimonialsListItemDto {
id: self.id.id.to_raw(), id: self.id.id.to_raw(),
user_id: self.user.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, role: self.role,
content: self.content, content: self.content,
created_at: self.created_at, created_at: self.created_at,
@@ -4,6 +4,9 @@ use super::{
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date}; use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use serde_json;
use std::time::Instant;
use tracing::instrument;
pub struct TestimonialsRepository<'a> { pub struct TestimonialsRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -14,17 +17,25 @@ impl<'a> TestimonialsRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, meta), err)]
pub async fn query_testimonial_list( pub async fn query_testimonial_list(
&self, &self,
meta: MetaRequestDto, meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> { ) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
let query = ListQueryBuilder::new(&ResourceEnum::Testimonials.to_string()) let now = Instant::now();
.with_select_fields(vec!["*", "user.* as user"]) // Select user details let query = ListQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_select_fields(vec!["*", "user.* as user"])
.with_pagination(meta.page, Some(10)) .with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref()) .with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build(); .build();
let res: Vec<TestimonialsQueryDto> = let res: Vec<TestimonialsQueryDto> =
self.state.surrealdb_ws.query(query).await?.take(0)?; self.state.surrealdb_ws.query(query).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_testimonial_list' took: {elapsed:.2?}");
}
let data = ResponseListSuccessDto { let data = ResponseListSuccessDto {
data: res, data: res,
meta: None, meta: None,
@@ -32,17 +43,26 @@ impl<'a> TestimonialsRepository<'a> {
Ok(data) Ok(data)
} }
#[instrument(skip(self, id), err)]
pub async fn query_testimonial_by_id( pub async fn query_testimonial_by_id(
&self, &self,
id: String, id: String,
) -> Result<TestimonialsQueryDto> { ) -> Result<TestimonialsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_id(&id) .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 sql = builder.build();
let result: Option<TestimonialsQueryDto> = let result: Option<TestimonialsQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_testimonial_by_id' took: {elapsed:.2?}");
}
match result { match result {
Some(testimonial) => { Some(testimonial) => {
@@ -55,15 +75,23 @@ impl<'a> TestimonialsRepository<'a> {
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_create_testimonial( pub async fn query_create_testimonial(
&self, &self,
data: TestimonialsSchema, data: TestimonialsSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let record: Option<TestimonialsSchema> = db let record: Option<TestimonialsSchema> = db
.create(ResourceEnum::Testimonials.to_string()) .create(ResourceEnum::Testimonials.to_string())
.content(data) .content(data)
.await?; .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 { match record {
Some(_) => Ok("Success create testimonial".into()), 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( pub async fn query_update_testimonial(
&self, &self,
data: TestimonialsSchema, data: TestimonialsSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?; let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
@@ -85,13 +115,19 @@ impl<'a> TestimonialsRepository<'a> {
let merged = TestimonialsSchema { let merged = TestimonialsSchema {
created_at: existing.created_at, created_at: existing.created_at,
updated_at: get_iso_date(), updated_at: get_iso_date(),
user: existing.user.id, // Preserve user ID user: existing.user.id,
..data ..data
}; };
let record_key = get_id(&merged.id)?; let record_key = get_id(&merged.id)?;
let record: Option<TestimonialsSchema> = let record: Option<TestimonialsSchema> =
db.update(record_key).merge(merged).await?; db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_testimonial' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success update testimonial".into()), 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> { pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let testimonial = self.query_testimonial_by_id(id).await?; let testimonial = self.query_testimonial_by_id(id).await?;
if testimonial.is_deleted { if testimonial.is_deleted {
@@ -111,6 +149,12 @@ impl<'a> TestimonialsRepository<'a> {
.update(record_key) .update(record_key)
.merge(serde_json::json!({ "is_deleted": true })) .merge(serde_json::json!({ "is_deleted": true }))
.await?; .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 { match record {
Some(_) => Ok("Success delete testimonial".into()), Some(_) => Ok("Success delete testimonial".into()),
@@ -11,7 +11,7 @@ use super::testimonials_dto::{
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestimonialsSchema { pub struct TestimonialsSchema {
pub id: Thing, pub id: Thing,
pub user: Thing, // Link to app_users table pub user: Thing,
pub role: String, pub role: String,
pub content: String, pub content: String,
pub is_deleted: bool, pub is_deleted: bool,
@@ -28,7 +28,7 @@ impl Default for TestimonialsSchema {
), ),
user: make_thing( user: make_thing(
&ResourceEnum::Users.to_string(), &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(), role: String::new(),
content: String::new(), content: String::new(),
+9 -3
View File
@@ -4,9 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
imphnen-iam.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -22,3 +23,8 @@ chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
tracing.workspace = true
[dev-dependencies]
dotenvy.workspace = true
http-body-util.workspace = true
+2 -14
View File
@@ -1,14 +1,2 @@
pub fn add(left: u64, right: u64) -> u64 { pub mod v1;
left + right pub use v1::*;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
@@ -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()),
}
}
}
+30
View File
@@ -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)))
}
+7
View File
@@ -0,0 +1,7 @@
use axum::Router;
pub mod mentors;
pub fn dimentorin_router() -> Router {
Router::new().nest("/mentors", mentors::mentors_router())
}
+1
View File
@@ -0,0 +1 @@
+1 -5
View File
@@ -1,9 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use surrealdb::{ use surrealdb::{Surreal, engine::any::Any, engine::local::Db};
engine::local::Db,
engine::any::Any,
Surreal,
};
use utoipa::{IntoParams, ToSchema}; use utoipa::{IntoParams, ToSchema};
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+11 -6
View File
@@ -1,26 +1,31 @@
pub mod error { pub mod error {
use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::response::Response; use axum::response::Response;
use axum::Json;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("database error")] #[error("database error: {0}")]
Db, Db(String),
} }
impl IntoResponse for Error { impl IntoResponse for Error {
fn into_response(self) -> Response { 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 { impl From<surrealdb::Error> for Error {
fn from(error: surrealdb::Error) -> Self { fn from(error: surrealdb::Error) -> Self {
eprintln!("{error}"); Self::Db(error.to_string())
Self::Db
} }
} }
} }
+6 -4
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-iam ={ version = "0.1.0", path = "../imphnen-iam" } imphnen-iam.workspace = true
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -24,3 +24,5 @@ anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
rand_distr.workspace = true rand_distr.workspace = true
log.workspace = true
tracing.workspace = true
-4
View File
@@ -1,7 +1,3 @@
use imphnen_entities::*;
use imphnen_libs::*;
use imphnen_utils::*;
pub mod v1; pub mod v1;
pub use imphnen_entities::*; pub use imphnen_entities::*;
@@ -2,6 +2,8 @@ use super::{GachaClaimQueryDto, GachaClaimSchema};
use crate::{AppState, ResourceEnum}; use crate::{AppState, ResourceEnum};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_iam::DetailQueryBuilder; use imphnen_iam::DetailQueryBuilder;
use std::time::Instant;
use tracing::instrument;
pub struct GachaClaimRepository<'a> { pub struct GachaClaimRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -12,10 +14,12 @@ impl<'a> GachaClaimRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, id), err)]
pub async fn query_gacha_claim_by_id( pub async fn query_gacha_claim_by_id(
&self, &self,
id: String, id: String,
) -> Result<GachaClaimQueryDto> { ) -> Result<GachaClaimQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string())
.with_id(id.clone()) .with_id(id.clone())
@@ -25,21 +29,35 @@ impl<'a> GachaClaimRepository<'a> {
let sql = builder.build(); let sql = builder.build();
let result: Option<GachaClaimQueryDto> = let result: Option<GachaClaimQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
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 { match result {
Some(claim) if !claim.is_deleted => Ok(claim), Some(claim) if !claim.is_deleted => Ok(claim),
_ => bail!("Gacha Claim not found"), _ => bail!("Gacha Claim not found"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_create_gacha_claim( pub async fn query_create_gacha_claim(
&self, &self,
data: GachaClaimSchema, data: GachaClaimSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let record: Option<GachaClaimSchema> = db let record: Option<GachaClaimSchema> = db
.create(ResourceEnum::GachaClaims.to_string()) .create(ResourceEnum::GachaClaims.to_string())
.content(data) .content(data)
.await?; .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 { match record {
Some(_) => Ok("Success create Gacha Claim".into()), Some(_) => Ok("Success create Gacha Claim".into()),
None => bail!("Failed to create Gacha Claim"), None => bail!("Failed to create Gacha Claim"),
@@ -2,7 +2,9 @@ use super::{GachaCreditRequestDto, GachaCreditSchema};
use crate::{AppState, ResourceEnum}; use crate::{AppState, ResourceEnum};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_iam::make_thing; use imphnen_iam::make_thing;
use std::time::Instant;
use surrealdb::Uuid; use surrealdb::Uuid;
use tracing::instrument;
pub struct GachaCreditRepository<'a> { pub struct GachaCreditRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -13,28 +15,54 @@ impl<'a> GachaCreditRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, user_id), err)]
pub async fn query_by_user_id( pub async fn query_by_user_id(
&self, &self,
user_id: String, user_id: String,
) -> Result<Option<GachaCreditSchema>> { ) -> Result<Option<GachaCreditSchema>> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let sql = format!( let sql = format!(
"SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1", "SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1",
ResourceEnum::GachaCredits.to_string(), ResourceEnum::GachaCredits,
ResourceEnum::Users.to_string() ResourceEnum::Users
); );
let result: Vec<GachaCreditSchema> = let result: Vec<GachaCreditSchema> =
db.query(sql).bind(("user_id", user_id)).await?.take(0)?; db.query(sql).bind(("user_id", user_id)).await?.take(0)?;
let elapsed = now.elapsed();
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()) Ok(result.into_iter().next())
} }
#[instrument(skip(self, user_id), err)]
pub async fn query_consume_credit(&self, user_id: String) -> Result<()> { pub async fn query_consume_credit(&self, user_id: String) -> Result<()> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let credit_opt = self.query_by_user_id(user_id).await?; let credit_opt = self.query_by_user_id(user_id).await?;
let Some(mut credit) = credit_opt else { 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(()); return Ok(());
}; };
if credit.available_rolls <= 0 { 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"); bail!("No extra roll credits remaining");
} }
credit.available_rolls -= 1; credit.available_rolls -= 1;
@@ -45,13 +73,21 @@ impl<'a> GachaCreditRepository<'a> {
)) ))
.merge(credit) .merge(credit)
.await?; .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(()) Ok(())
} }
#[instrument(skip(self, payload), err)]
pub async fn query_add_credit( pub async fn query_add_credit(
&self, &self,
payload: GachaCreditRequestDto, payload: GachaCreditRequestDto,
) -> Result<()> { ) -> Result<()> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? { if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
credit.available_rolls += payload.amount; credit.available_rolls += payload.amount;
@@ -63,7 +99,7 @@ impl<'a> GachaCreditRepository<'a> {
.merge(credit) .merge(credit)
.await?; .await?;
} else { } else {
let data = GachaCreditSchema::from(&GachaCreditSchema { let data = GachaCreditSchema {
id: make_thing( id: make_thing(
&ResourceEnum::GachaCredits.to_string(), &ResourceEnum::GachaCredits.to_string(),
&Uuid::new_v4().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), user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id),
available_rolls: payload.amount, available_rolls: payload.amount,
..Default::default() ..Default::default()
}); };
let _: Option<GachaCreditSchema> = db let _: Option<GachaCreditSchema> = db
.create(&ResourceEnum::GachaCredits.to_string()) .create(ResourceEnum::GachaCredits.to_string())
.content(data) .content(data)
.await?; .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(()) Ok(())
} }
} }
@@ -5,6 +5,10 @@ use crate::{
}; };
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_iam::QueryListBuilder; 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> { pub struct GachaItemRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -15,10 +19,12 @@ impl<'a> GachaItemRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, meta), err)]
pub async fn query_gacha_item_list( pub async fn query_gacha_item_list(
&self, &self,
meta: MetaRequestDto, meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> { ) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
let now = Instant::now();
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> = let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
QueryListBuilder::new( QueryListBuilder::new(
&self.state.surrealdb_ws, &self.state.surrealdb_ws,
@@ -30,6 +36,12 @@ impl<'a> GachaItemRepository<'a> {
.select_fields(vec!["*"]) .select_fields(vec!["*"])
.build() .build()
.await?; .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 let data = raw_result
.data .data
.into_iter() .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> { pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let result: Option<GachaItemSchema> = db let result: Option<GachaItemSchema> = db
.select((ResourceEnum::GachaItems.to_string(), id.clone())) .select((ResourceEnum::GachaItems.to_string(), id.clone()))
.await?; .await?;
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 { match result {
Some(item) if !item.is_deleted => Ok(item), Some(item) if !item.is_deleted => Ok(item),
_ => bail!("Gacha Item not found"), _ => bail!("Gacha Item not found"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_create_gacha_item( pub async fn query_create_gacha_item(
&self, &self,
data: GachaItemSchema, data: GachaItemSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let record: Option<GachaItemSchema> = db let record: Option<GachaItemSchema> = db
.create(ResourceEnum::GachaItems.to_string()) .create(ResourceEnum::GachaItems.to_string())
.content(data) .content(data)
.await?; .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 { match record {
Some(_) => Ok("Success create Gacha Item".into()), Some(_) => Ok("Success create Gacha Item".into()),
None => bail!("Failed to create Gacha Item"), None => bail!("Failed to create Gacha Item"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_update_gacha_item( pub async fn query_update_gacha_item(
&self, &self,
data: GachaItemSchema, data: GachaItemSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let record_key = get_id(&data.id)?; let record_key = get_id(&data.id)?;
let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?; 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> = let record: Option<GachaItemSchema> =
db.update(record_key).merge(merged).await?; 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 { match record {
Some(_) => Ok("Success update Gacha Item".into()), Some(_) => Ok("Success update Gacha Item".into()),
None => bail!("Failed to update Gacha Item"), None => bail!("Failed to update Gacha Item"),
} }
} }
#[instrument(skip(self, id), err)]
pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> { pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id); let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?; 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"); bail!("Gacha Item already deleted");
} }
let record_key = get_id(&item.id)?; let record_key = get_id(&item.id)?;
let record: Option<GachaItemSchema> = db let mut patch = Map::new();
.update(record_key) patch.insert("is_deleted".to_string(), Value::Bool(true));
.merge(serde_json::json!({ "is_deleted": true })) patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
.await?;
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 { match record {
Some(_) => Ok("Success delete Gacha Item".into()), Some(_) => Ok("Success soft delete Gacha Item".into()),
None => bail!("Failed to delete Gacha Item"), None => bail!("Failed to soft delete Gacha Item"),
} }
} }
} }
@@ -45,7 +45,12 @@ impl GachaItemService {
return common_response(status, &message); return common_response(status, &message);
} }
let repo = GachaItemRepository::new(state); 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 { match repo.query_create_gacha_item(schema).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg), Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), 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, 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 { Self {
id: dto.id.id.to_raw(), id: dto.id.id.to_raw(),
item: GachaItemDto::from(dto.item.clone()), item: GachaItemDto::from(dto.item.clone()),
weight: dto.weight.clone(), weight: dto.weight,
quantity: dto.quantity, quantity: dto.quantity,
is_deleted: dto.is_deleted, is_deleted: dto.is_deleted,
created_at: dto.created_at.clone(), created_at: dto.created_at.clone(),
@@ -1,11 +1,15 @@
use super::GachaRollQueryDto; use super::GachaRollQueryDto;
use super::GachaRollSchema; use super::GachaRollSchema;
use crate::{AppState, DetailQueryBuilder, ResourceEnum}; use crate::{AppState, DetailQueryBuilder, ResourceEnum, get_id, make_thing};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_iam::ListQueryBuilder;
use rand::prelude::*; use rand::prelude::*;
use rand::rng;
use imphnen_utils::get_iso_date;
use rand_distr::weighted::WeightedIndex; use rand_distr::weighted::WeightedIndex;
use serde_json::{Map, Value};
use std::time::Instant;
use tracing::instrument;
pub struct GachaRollRepository<'a> { pub struct GachaRollRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -16,46 +20,70 @@ impl<'a> GachaRollRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, id), err)]
pub async fn query_gacha_roll_by_id( pub async fn query_gacha_roll_by_id(
&self, &self,
id: String, id: String,
) -> Result<GachaRollQueryDto> { ) -> Result<GachaRollQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string())
.with_id(id.clone()) .with_id(id.clone())
.with_condition("is_deleted = false")
.with_select_fields(vec!["*"]) .with_select_fields(vec!["*"])
.with_fetch("item"); .with_fetch("item");
let sql = builder.build(); let sql = builder.build();
let result: Option<GachaRollQueryDto> = let result: Option<GachaRollQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
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 { match result {
Some(roll) if !roll.is_deleted => Ok(roll), Some(roll) if !roll.is_deleted => Ok(roll),
_ => bail!("Gacha Roll not found"), _ => bail!("Gacha Roll not found"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_create_gacha_roll( pub async fn query_create_gacha_roll(
&self, &self,
data: GachaRollSchema, data: GachaRollSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let record: Option<GachaRollSchema> = db let record: Option<GachaRollSchema> = db
.create(ResourceEnum::GachaRolls.to_string()) .create(ResourceEnum::GachaRolls.to_string())
.content(data) .content(data)
.await?; .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 { match record {
Some(_) => Ok("Success create Gacha Roll".into()), Some(_) => Ok("Success create Gacha Roll".into()),
None => bail!("Failed to create Gacha Roll"), None => bail!("Failed to create Gacha Roll"),
} }
} }
#[instrument(skip(self), err)]
pub async fn query_all_active_rolls(&self) -> Result<Vec<GachaRollQueryDto>> { pub async fn query_all_active_rolls(&self) -> Result<Vec<GachaRollQueryDto>> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let builder = ListQueryBuilder::new(ResourceEnum::GachaRolls.to_string()) let table_name = ResourceEnum::GachaRolls.to_string();
.with_select_fields(vec!["*"]) let sql =
.with_fetch(Some(vec!["item"])); format!("SELECT * FROM {table_name} WHERE is_deleted = false FETCH item");
let sql = builder.build();
let result: Vec<GachaRollQueryDto> = db.query(sql).await?.take(0)?; 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) Ok(result)
} }
@@ -72,8 +100,36 @@ impl<'a> GachaRollRepository<'a> {
return None; return None;
} }
let dist = WeightedIndex::new(&weights).ok()?; let dist = WeightedIndex::new(&weights).ok()?;
let mut rng = rng(); let mut rng = rand::rngs::ThreadRng::default();
let index = dist.sample(&mut rng); let index = dist.sample(&mut rng);
Some(filtered[index].clone()) 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()), 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()),
}
}
} }
+8 -7
View File
@@ -4,13 +4,14 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } imphnen-iam.workspace = true
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-gacha = { version = "0.1.0", path = "../imphnen-gacha" } imphnen-gacha.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
imphnen-middleware = { version = "0.1.0", path = "../imphnen-middleware" } imphnen-middleware.workspace = true
imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" } imphnen-cms.workspace = true
imphnen-dimentorin.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More