Merge pull request #46 from IMPHNEN/submissions

Submissions
This commit is contained in:
Maulana Sodiqin
2025-10-29 17:13:16 +07:00
committed by GitHub
261 changed files with 39019 additions and 3606 deletions
+1
View File
@@ -13,3 +13,4 @@
.env.development .env.development
.env.staging .env.staging
.env.production .env.production
**/**.log
+1
View File
@@ -0,0 +1 @@
/cache
+71
View File
@@ -0,0 +1,71 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: rust
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "imphnen-backend-service"
Generated
+61 -1
View File
@@ -2038,7 +2038,9 @@ dependencies = [
"imphnen-cms", "imphnen-cms",
"imphnen-dimentorin", "imphnen-dimentorin",
"imphnen-entities", "imphnen-entities",
"imphnen-gacha",
"imphnen-gateway", "imphnen-gateway",
"imphnen-hackathon",
"imphnen-iam", "imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-utils", "imphnen-utils",
@@ -2092,6 +2094,7 @@ name = "imphnen-dimentorin"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"axum", "axum",
"axum-test", "axum-test",
"chrono", "chrono",
@@ -2100,6 +2103,7 @@ dependencies = [
"imphnen-entities", "imphnen-entities",
"imphnen-iam", "imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-middleware",
"imphnen-utils", "imphnen-utils",
"lazy_static", "lazy_static",
"rand 0.9.2", "rand 0.9.2",
@@ -2121,10 +2125,15 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"chrono",
"serde", "serde",
"serde_json",
"strum 0.27.2",
"strum_macros 0.27.2",
"surrealdb", "surrealdb",
"thiserror 2.0.14", "thiserror 2.0.14",
"utoipa", "utoipa",
"uuid",
] ]
[[package]] [[package]]
@@ -2167,6 +2176,7 @@ dependencies = [
"imphnen-dimentorin", "imphnen-dimentorin",
"imphnen-entities", "imphnen-entities",
"imphnen-gacha", "imphnen-gacha",
"imphnen-hackathon",
"imphnen-iam", "imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-middleware", "imphnen-middleware",
@@ -2184,11 +2194,53 @@ dependencies = [
"validator", "validator",
] ]
[[package]]
name = "imphnen-hackathon"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"axum",
"axum-extra",
"axum-test",
"chrono",
"dotenvy",
"futures",
"http-body-util",
"imphnen-entities",
"imphnen-iam",
"imphnen-libs",
"imphnen-utils",
"lazy_static",
"log",
"mockall",
"oauth2",
"once_cell",
"rand 0.9.2",
"regex",
"reqwest",
"serde",
"serde_json",
"strum 0.27.2",
"strum_macros 0.27.2",
"surrealdb",
"tokio",
"tokio-test",
"tower",
"tower-http",
"tracing",
"utoipa",
"utoipa-swagger-ui",
"uuid",
"validator",
]
[[package]] [[package]]
name = "imphnen-iam" name = "imphnen-iam"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"axum", "axum",
"axum-extra", "axum-extra",
"axum-test", "axum-test",
@@ -2227,6 +2279,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
"async-trait",
"axum", "axum",
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
@@ -2240,11 +2293,13 @@ dependencies = [
"once_cell", "once_cell",
"reqwest", "reqwest",
"serde", "serde",
"serde_json",
"sha2", "sha2",
"surrealdb", "surrealdb",
"tokio", "tokio",
"urlencoding", "urlencoding",
"uuid", "uuid",
"validator",
] ]
[[package]] [[package]]
@@ -2255,13 +2310,14 @@ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
"axum-test", "axum-test",
"base64 0.22.1",
"chrono", "chrono",
"futures", "futures",
"imphnen-entities", "imphnen-entities",
"imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-utils", "imphnen-utils",
"lazy_static", "lazy_static",
"log",
"rand 0.9.2", "rand 0.9.2",
"regex", "regex",
"serde", "serde",
@@ -2288,6 +2344,7 @@ dependencies = [
"imphnen-entities", "imphnen-entities",
"imphnen-libs", "imphnen-libs",
"rand 0.9.2", "rand 0.9.2",
"regex",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@@ -4844,6 +4901,7 @@ dependencies = [
"hyper-util", "hyper-util",
"imphnen-dimentorin", "imphnen-dimentorin",
"imphnen-entities", "imphnen-entities",
"imphnen-hackathon",
"imphnen-iam", "imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-utils", "imphnen-utils",
@@ -4856,6 +4914,8 @@ dependencies = [
"tower", "tower",
"tracing", "tracing",
"uuid", "uuid",
"yoke",
"yoke-derive",
] ]
[[package]] [[package]]
+13 -10
View File
@@ -2,20 +2,22 @@
resolver = "2" resolver = "2"
members = [ members = [
"tests", "tests",
"imphnen-iam", "imphnen-entities", # Most basic - core data structures
"imphnen-cms", "imphnen-libs", # Depends on entities
"imphnen-libs", "imphnen-utils", # Depends on libs and entities
"imphnen-utils", "imphnen-middleware",# Utility for permissions
"imphnen-gacha", "imphnen-iam", # Core auth service, depends on libs, utils, entities
"imphnen-gateway", "imphnen-cms", # Content management, depends on core services
"imphnen-backend", "imphnen-gacha", # Game mechanics, depends on core services
"imphnen-entities", "imphnen-dimentorin",# Learning platform, depends on core services
"imphnen-dimentorin", "imphnen-hackathon", # Hackathon service, depends on core services
"imphnen-middleware", "imphnen-gateway", # API gateway, depends on all services
"imphnen-backend", # Main application, depends on all services
] ]
[workspace.dependencies] [workspace.dependencies]
async-trait = "0.1.83"
oauth2 = "5.0.0" oauth2 = "5.0.0"
reqwest = { version = "0.12.23", features = ["json"] } reqwest = { version = "0.12.23", features = ["json"] }
serde_json = "1.0.142" serde_json = "1.0.142"
@@ -78,6 +80,7 @@ imphnen-gateway = { path = "./imphnen-gateway" }
imphnen-backend = { path = "./imphnen-backend" } imphnen-backend = { path = "./imphnen-backend" }
imphnen-entities = { path = "./imphnen-entities" } imphnen-entities = { path = "./imphnen-entities" }
imphnen-dimentorin = { path = "./imphnen-dimentorin" } imphnen-dimentorin = { path = "./imphnen-dimentorin" }
imphnen-hackathon = { path = "./imphnen-hackathon" }
imphnen-middleware = { path = "./imphnen-middleware" } imphnen-middleware = { path = "./imphnen-middleware" }
[profile.release] [profile.release]
+31 -23
View File
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# Script deploy ke VPS # Script deploy semua binary hasil build Rust (untuk Linux)
set -e set -e
@@ -7,35 +7,43 @@ REMOTE_USER="asephs"
REMOTE_HOST="70.153.9.42" REMOTE_HOST="70.153.9.42"
REMOTE_PATH="/home/asephs/imphnen-backend-service" REMOTE_PATH="/home/asephs/imphnen-backend-service"
# Build project # Build release binary
taskset -c 0,1 cargo build --release -j 2 echo "🔧 Building Rust project..."
cargo build --release
# Rsync hasil build dan file yang diperlukan # Filter hanya file executable tanpa ekstensi .exe, .rlib, atau .d
rsync -avz --delete \ BINARIES=$(find target/release -maxdepth 1 -type f ! -name "*.exe" ! -name "*.rlib" ! -name "*.d")
target/release/ \
if [ -z "$BINARIES" ]; then
echo "❌ Tidak ada binary Linux (.exe/.rlib/.d diabaikan)"
exit 1
fi
# Upload semua binary yang valid ke server
echo "🚀 Mengirim binary ke server..."
rsync -avz --compress-level=9 --progress \
$BINARIES \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/ $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
# Sync file konfigurasi dan source code (jika perlu) # Jalankan ulang service utama di VPS
rsync -avz --delete \ echo "♻️ Restart service utama di server..."
imphnen-backend/ \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/imphnen-backend/
rsync -avz --delete \
docker-compose.yml Dockerfile \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/
# Tambahkan file lain jika diperlukan
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF' ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
cd /home/asephs/imphnen-backend-service cd /home/asephs/imphnen-backend-service
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin" export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH=$PATH:/root/.local/share/pnpm
export PATH=$PATH:/home/asephs/.nvm/versions/node/v22.17.1/bin
export PATH=$PATH:/home/asephs/.bun/bin/bun
if [ -f ~/.bashrc ]; then if [ -f ~/.bashrc ]; then
source ~/.bashrc source ~/.bashrc
fi fi
pm2 restart 4 --update-env
# Pastikan semua binary bisa dieksekusi
chmod +x target/release/*
# Restart service utama (misalnya api.d)
if pm2 list | grep -q api; then
pm2 restart api --update-env
else
pm2 start target/release/api.d --name api
fi
EOF EOF
echo "Deploy selesai ke $REMOTE_HOST:$REMOTE_PATH" echo "Deploy semua binary Linux selesai ke $REMOTE_HOST:$REMOTE_PATH"
+12 -2
View File
@@ -40,8 +40,16 @@ name = "seed_roles_permissions"
path = "src/bin/seed_roles_permissions.rs" path = "src/bin/seed_roles_permissions.rs"
[[bin]] [[bin]]
name = "seed_users" name = "seed_teams"
path = "src/bin/seed_users.rs" path = "src/bin/seed_teams.rs"
[[bin]]
name = "seed_hackathons"
path = "src/bin/seed_hackathons.rs"
[[bin]]
name = "seed_test_data"
path = "src/bin/seed_test_data.rs"
[dependencies] [dependencies]
imphnen-libs.workspace = true imphnen-libs.workspace = true
@@ -50,7 +58,9 @@ imphnen-gateway.workspace = true
imphnen-entities.workspace = true imphnen-entities.workspace = true
imphnen-iam.workspace = true imphnen-iam.workspace = true
imphnen-cms.workspace = true imphnen-cms.workspace = true
imphnen-gacha.workspace = true
imphnen-dimentorin.workspace = true imphnen-dimentorin.workspace = true
imphnen-hackathon.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
+1 -5
View File
@@ -1,4 +1,3 @@
use axum::Router;
use imphnen_gateway::gateway_service; use imphnen_gateway::gateway_service;
use imphnen_libs::axum_init; use imphnen_libs::axum_init;
@@ -6,10 +5,7 @@ use imphnen_libs::axum_init;
async fn main() { async fn main() {
env_logger::init(); env_logger::init();
axum_init(|surrealdb_ws, surrealdb_mem| async { axum_init(|surrealdb_ws, surrealdb_mem| async {
let app = gateway_service(surrealdb_ws, surrealdb_mem).await; gateway_service(surrealdb_ws, surrealdb_mem).await
let mut router = Router::new();
router = router.nest("/api/v1/auth", imphnen_iam::v1::auth::auth_router());
app.merge(router)
}) })
.await; .await;
} }
+19
View File
@@ -0,0 +1,19 @@
use imphnen_libs::jsonwebtoken::encode_access_token;
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: mk_token <email_or_sub>");
std::process::exit(1);
}
let sub = args[1].clone();
// Use sub as both sub and user_id
match encode_access_token(sub.clone(), sub.clone()) {
Ok(token) => println!("{}", token),
Err(e) => {
eprintln!("Failed to generate token: {:?}", e);
std::process::exit(2);
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV; let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
+5 -6
View File
@@ -5,7 +5,7 @@ use surrealdb::sql::Thing;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV; let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any; use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
@@ -18,13 +18,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
.await?; .await?;
db.query("DELETE type::thing('app_gacha_items', $id)") db.query("DELETE type::thing('app_gacha_items', $id)")
.bind(("id", "gacha_item_test_id")) .bind(("id", "1"))
.await?; .await?;
db.query("DELETE type::thing('app_gacha_rolls', $id)") db.query("DELETE type::thing('app_gacha_rolls', $id)")
.bind(("id", "gacha_roll_test_id")) .bind(("id", "test-gacha-roll-001"))
.await?; .await?;
let gacha_item_id = "1";
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") 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(("id", gacha_item_id))
.bind(("name", "Test Gacha Item")) .bind(("name", "Test Gacha Item"))
@@ -35,7 +34,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.await?; .await?;
println!("Gacha Item seeded successfully!"); println!("Gacha Item seeded successfully!");
let gacha_roll_id = "gacha_roll_test_id"; let gacha_roll_id = "test-gacha-roll-001";
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") 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(("id", gacha_roll_id))
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id)))) .bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
+368
View File
@@ -0,0 +1,368 @@
use chrono::{DateTime, Utc};
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
HackathonStatus, HackathonEventType, HackathonPhase, SubmissionStatus, Prize
};
use imphnen_utils::get_iso_date;
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
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.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
// Sample hackathon data
let hackathons = vec![
(
"hackathon-001",
"AI Innovation Challenge 2025",
"Build the next generation of AI-powered applications that solve real-world problems.",
"2025-10-15T09:00:00Z",
"2025-10-17T18:00:00Z",
"2025-10-01T23:59:59Z",
Some(100),
HackathonStatus::RegistrationOpen,
Some("Artificial Intelligence & Machine Learning".to_string()),
Some("1. All code must be original\n2. Teams can have 2-5 members\n3. Projects must use AI/ML technologies".to_string()),
Some(vec![
Prize { position: 1, title: "Grand Prize".to_string(), description: Some("Winner gets full scholarship".to_string()), value: Some("$10,000".to_string()) },
Prize { position: 2, title: "Second Place".to_string(), description: Some("Runner-up prize".to_string()), value: Some("$5,000".to_string()) },
Prize { position: 3, title: "Third Place".to_string(), description: Some("Third place prize".to_string()), value: Some("$2,500".to_string()) },
]),
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
),
(
"hackathon-002",
"Green Tech Hackathon",
"Develop sustainable technology solutions for environmental challenges.",
"2025-11-20T10:00:00Z",
"2025-11-22T17:00:00Z",
"2025-11-05T23:59:59Z",
Some(75),
HackathonStatus::Draft,
Some("Sustainability & Green Technology".to_string()),
Some("Focus on renewable energy, waste reduction, and environmental monitoring.".to_string()),
Some(vec![
Prize { position: 1, title: "Eco Champion".to_string(), description: Some("Best environmental impact".to_string()), value: Some("$7,500".to_string()) },
Prize { position: 2, title: "Innovation Award".to_string(), description: Some("Most innovative solution".to_string()), value: Some("$3,500".to_string()) },
]),
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
),
];
// Sample hackathon events
let hackathon_events = vec![
(
"hackathon-001",
"event-001",
"Opening Ceremony",
Some("Welcome and kickoff event for the AI Innovation Challenge".to_string()),
HackathonEventType::Ceremony,
"2025-10-15T09:00:00Z",
"2025-10-15T10:00:00Z",
Some("Main Auditorium".to_string()),
None,
Some(150),
true,
),
(
"hackathon-001",
"event-002",
"AI Workshop: Getting Started",
Some("Introduction to AI frameworks and tools".to_string()),
HackathonEventType::Workshop,
"2025-10-15T14:00:00Z",
"2025-10-15T16:00:00Z",
None,
Some("https://zoom.us/meeting/ai-workshop".to_string()),
Some(80),
false,
),
(
"hackathon-001",
"event-003",
"Judging Session",
Some("Final project presentations and judging".to_string()),
HackathonEventType::Judging,
"2025-10-17T14:00:00Z",
"2025-10-17T17:00:00Z",
Some("Innovation Lab".to_string()),
None,
Some(100),
true,
),
];
// Sample hackathon timeline
let hackathon_timeline = vec![
(
"hackathon-001",
HackathonPhase::Registration,
"Registration Phase",
Some("Register your team and submit initial project ideas".to_string()),
"2025-10-01T00:00:00Z",
"2025-10-10T23:59:59Z",
true,
1,
),
(
"hackathon-001",
HackathonPhase::Ideation,
"Ideation & Planning",
Some("Brainstorm and plan your AI solution".to_string()),
"2025-10-11T00:00:00Z",
"2025-10-14T23:59:59Z",
false,
2,
),
(
"hackathon-001",
HackathonPhase::Development,
"Development Sprint",
Some("Build your AI-powered application".to_string()),
"2025-10-15T00:00:00Z",
"2025-10-16T23:59:59Z",
false,
3,
),
(
"hackathon-001",
HackathonPhase::Submission,
"Project Submission",
Some("Submit your final project and demo video".to_string()),
"2025-10-17T00:00:00Z",
"2025-10-17T12:00:00Z",
false,
4,
),
(
"hackathon-001",
HackathonPhase::Judging,
"Judging & Awards",
Some("Presentations and prize ceremony".to_string()),
"2025-10-17T13:00:00Z",
"2025-10-17T18:00:00Z",
false,
5,
),
];
// Sample hackathon submissions
let hackathon_submissions = vec![
(
"hackathon-001",
"team-dev-001",
"AI-Powered Health Monitor",
"A machine learning application that predicts health risks using wearable device data.",
Some("https://github.com/team-dev/ai-health-monitor".to_string()),
Some("https://demo.ai-health-monitor.com".to_string()),
None,
vec!["Python".to_string(), "TensorFlow".to_string(), "React".to_string()],
SubmissionStatus::Submitted,
"2025-10-17T11:30:00Z",
),
(
"hackathon-001",
"team-design-001",
"Smart City Traffic Optimizer",
"AI system that optimizes traffic flow using computer vision and predictive analytics.",
Some("https://github.com/team-design/smart-traffic".to_string()),
Some("https://demo.smart-traffic.com".to_string()),
Some("https://slides.smart-traffic.com/presentation".to_string()),
vec!["JavaScript".to_string(), "Node.js".to_string(), "OpenCV".to_string()],
SubmissionStatus::UnderReview,
"2025-10-17T10:45:00Z",
),
];
// Seed hackathons
for (
id,
name,
description,
start_date,
end_date,
registration_deadline,
max_participants,
status,
theme,
rules,
prizes,
organizers,
) in hackathons {
db.query("DELETE type::thing('app_hackathons', $id)")
.bind(("id", id))
.await?;
let hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", id)),
name: name.into(),
description: description.into(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
max_participants,
status,
theme,
rules,
prizes,
previous_winners: None,
organizers,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
.content(hackathon)
.await?;
println!("✅ Inserted hackathon: {name}");
}
// Seed hackathon events
for (
hackathon_id,
event_id,
title,
description,
event_type,
start_time,
end_time,
location,
virtual_link,
max_attendees,
is_mandatory,
) in hackathon_events {
db.query("DELETE type::thing('app_hackathon_events', $id)")
.bind(("id", event_id))
.await?;
let event = HackathonEventsSchema {
id: Thing::from(("app_hackathon_events", event_id)),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
title: title.into(),
description,
event_type,
start_time: DateTime::parse_from_rfc3339(start_time)?.with_timezone(&Utc),
end_time: DateTime::parse_from_rfc3339(end_time)?.with_timezone(&Utc),
location,
virtual_link,
max_attendees,
is_mandatory,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", event_id))
.content(event)
.await?;
println!("✅ Inserted hackathon event: {title}");
}
// Seed hackathon timeline
for (
hackathon_id,
phase,
title,
description,
start_date,
end_date,
is_active,
order,
) in hackathon_timeline {
let timeline_id = format!("timeline-{}-{}", hackathon_id, order);
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
.bind(("id", timeline_id.clone()))
.await?;
let timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
phase,
title: title.into(),
description,
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
is_active,
order,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
.content(timeline)
.await?;
println!("✅ Inserted hackathon timeline: {title}");
}
// Seed hackathon submissions
for (
hackathon_id,
team_id,
project_name,
description,
repository_url,
demo_url,
slides_url,
technologies,
submission_status,
submitted_at,
) in hackathon_submissions {
let submission_id = format!("submission-{}-{}", hackathon_id, team_id);
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
.bind(("id", submission_id.clone()))
.await?;
let submission = HackathonSubmissionsSchema {
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
judge_feedback: None,
team_id: Some(Thing::from(("app_teams", team_id))),
project_name: Some(project_name.into()),
description: Some(description.into()),
repository_url,
upload_file_url: None,
demo_url,
slides_url,
technologies: Some(technologies),
contact_instagram: None,
contact_twitter: None,
contact_linkedin: None,
contact_facebook: None,
contact_youtube: None,
contact_tiktok: None,
contact_other: None,
submission_status: Some(submission_status),
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
.content(submission)
.await?;
println!("✅ Inserted hackathon submission: {project_name}");
}
println!("✅ All Hackathons seeded");
Ok(())
}
+1 -1
View File
@@ -5,7 +5,7 @@ use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV; let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any; use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
+2 -1
View File
@@ -7,7 +7,7 @@ use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV; let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
@@ -54,6 +54,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
PermissionsEnum::UpdateMentors, PermissionsEnum::UpdateMentors,
PermissionsEnum::VerifyMentors, PermissionsEnum::VerifyMentors,
PermissionsEnum::DeleteMentors, PermissionsEnum::DeleteMentors,
PermissionsEnum::Administrator,
] { ] {
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()))
+4 -4
View File
@@ -5,7 +5,7 @@ use surrealdb::engine::any;
use surrealdb::opt::auth::Root; use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV; let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
@@ -26,7 +26,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
( (
"5713cb37-dc02-4e87-8048-d7a41d352059", "5713cb37-dc02-4e87-8048-d7a41d352059",
"User", "User",
None, Some("2025-02-28T14:53:58.576688+00"),
Some("2025-02-28T14:53:58.576688+00"), Some("2025-02-28T14:53:58.576688+00"),
), ),
( (
@@ -44,13 +44,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
( (
"f6b03f25-e416-4893-ac88-caaa690afb07", "f6b03f25-e416-4893-ac88-caaa690afb07",
"Admin", "Admin",
None, Some("2025-02-22T15:38:39.868306+00"),
Some("2025-02-22T15:38:39.868306+00"), Some("2025-02-22T15:38:39.868306+00"),
), ),
( (
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a", "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
"Mentor", "Mentor",
None, Some("2025-07-06T10:00:00.000000+00"),
Some("2025-07-06T10:00:00.000000+00"), Some("2025-07-06T10:00:00.000000+00"),
), ),
]; ];
@@ -5,7 +5,7 @@ use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV; let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
@@ -28,38 +28,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
( (
"f6b03f25-e416-4893-ac88-caaa690afb07", "f6b03f25-e416-4893-ac88-caaa690afb07",
vec![ vec![
PermissionsEnum::ReadListUsers, // Only Administrator permission - grants access to everything
PermissionsEnum::ReadDetailUsers, PermissionsEnum::Administrator,
PermissionsEnum::CreateUsers,
PermissionsEnum::DeleteUsers,
PermissionsEnum::UpdateUsers,
PermissionsEnum::ActivateUsers,
PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadDetailRoles,
PermissionsEnum::CreateRoles,
PermissionsEnum::DeleteRoles,
PermissionsEnum::UpdateRoles,
PermissionsEnum::ReadListPermissions,
PermissionsEnum::ReadDetailPermissions,
PermissionsEnum::CreatePermissions,
PermissionsEnum::DeletePermissions,
PermissionsEnum::UpdatePermissions,
PermissionsEnum::CreateGachaClaims,
PermissionsEnum::ReadDetailGachaClaims,
PermissionsEnum::ReadListGachaItems,
PermissionsEnum::ReadDetailGachaItems,
PermissionsEnum::CreateGachaItems,
PermissionsEnum::DeleteGachaItems,
PermissionsEnum::UpdateGachaItems,
PermissionsEnum::ReadDetailGachaRolls,
PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls,
PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailMentors,
PermissionsEnum::RegisterMentors,
PermissionsEnum::UpdateMentors,
PermissionsEnum::VerifyMentors,
PermissionsEnum::DeleteMentors,
], ],
), ),
( (
@@ -100,13 +70,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
( (
"50133429-f4b1-4249-9f97-7b86e6ee9d86", "50133429-f4b1-4249-9f97-7b86e6ee9d86",
vec![ vec![
// Staff should be able to list roles and permissions in tests
PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadListPermissions,
PermissionsEnum::ReadListUsers, PermissionsEnum::ReadListUsers,
PermissionsEnum::ReadListMentors, PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailUsers, PermissionsEnum::ReadDetailUsers,
PermissionsEnum::ActivateUsers, PermissionsEnum::ActivateUsers,
PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadDetailRoles, PermissionsEnum::ReadDetailRoles,
PermissionsEnum::ReadListPermissions,
PermissionsEnum::ReadDetailPermissions, PermissionsEnum::ReadDetailPermissions,
PermissionsEnum::ReadListGachaItems, PermissionsEnum::ReadListGachaItems,
PermissionsEnum::ReadDetailGachaItems, PermissionsEnum::ReadDetailGachaItems,
@@ -115,6 +86,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
PermissionsEnum::ReadDetailGachaRolls, PermissionsEnum::ReadDetailGachaRolls,
PermissionsEnum::CreateGachaRolls, PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls, PermissionsEnum::ExecuteGachaRolls,
PermissionsEnum::ManageAllTeams,
], ],
), ),
( (
@@ -127,7 +99,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
for (role_id, permissions) in roles_permissions { for (role_id, permissions) in roles_permissions {
let permission_refs: Vec<_> = permissions let permission_refs: Vec<_> = permissions
.iter() .iter()
.map(|perm| make_thing("app_permissions", perm.id())) .map(|perm| make_thing("app_permissions", &perm.id()))
.collect(); .collect();
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false") db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
+85
View File
@@ -0,0 +1,85 @@
use imphnen_iam::v1::teams::TeamsSchema;
use imphnen_utils::get_iso_date;
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
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.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
let teams = vec![
(
"team-dev-001",
"Development Team",
Some("Core development team for the platform".to_string()),
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
true,
Some(10),
Some(vec!["Rust".to_string(), "Backend".to_string()]),
Some("Remote".to_string()),
),
(
"team-design-001",
"Design Team",
Some("UI/UX design team".to_string()),
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
true,
Some(5),
Some(vec!["Figma".to_string(), "Design".to_string()]),
Some("Remote".to_string()),
),
(
"team-qa-001",
"Quality Assurance Team",
Some("Testing and quality assurance team".to_string()),
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
false,
Some(8),
Some(vec!["Testing".to_string(), "Automation".to_string()]),
Some("Remote".to_string()),
),
];
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in teams {
db.query("DELETE type::thing('app_teams', $id)")
.bind(("id", id))
.await?;
let team = TeamsSchema {
id: Thing::from(("app_teams", id)),
name: name.into(),
description,
leader_id: Thing::from(("app_users", leader_id)),
is_open,
max_members,
skills_required,
location,
avatar: None,
website_url: None,
github_url: None,
is_active: true,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
db.create::<Option<TeamsSchema>>(("app_teams", id))
.content(team)
.await?;
println!("✅ Inserted team: {name}");
}
println!("✅ All Teams seeded");
Ok(())
}
+175
View File
@@ -0,0 +1,175 @@
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
use imphnen_cms::v1::landing::testimonials::testimonials_schema::TestimonialsSchema;
use imphnen_dimentorin::v1::mentors::mentors_schema::MentorSchema;
use imphnen_dimentorin::v1::mentors::mentors_dto::MentoringRate;
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema,
HackathonStatus, HackathonEventType, HackathonPhase
};
use imphnen_utils::get_iso_date;
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
use chrono::Utc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
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.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
// Seed Events - handle existing data
let event = EventsSchema {
id: Thing::from(("app_events", "1")),
name: "Test Event".to_string(),
description: "Test event description".to_string(),
detail_link: "https://example.com/event".to_string(),
price: 50.0,
is_online: true,
start_date: get_iso_date(),
end_date: get_iso_date(),
location: None,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
match db.create::<Option<EventsSchema>>(("app_events", "1"))
.content(event)
.await {
Ok(_) => println!("✅ Inserted test event"),
Err(_) => println!("⚠️ Test event already exists, skipping"),
};
// Seed Testimonials - handle existing data
let testimonial = TestimonialsSchema {
id: Thing::from(("app_testimonials", "1")),
user: Thing::from(("app_users", "c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")),
role: "Student".to_string(),
content: "This is a great platform!".to_string(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
match db.create::<Option<TestimonialsSchema>>(("app_testimonials", "1"))
.content(testimonial)
.await {
Ok(_) => println!("✅ Inserted test testimonial"),
Err(_) => println!("⚠️ Test testimonial already exists, skipping"),
};
// Seed Hackathon - handle existing data
let hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", "1")),
name: "Test Hackathon".to_string(),
description: "Test hackathon description".to_string(),
start_date: Utc::now() + chrono::Duration::days(30),
end_date: Utc::now() + chrono::Duration::days(37),
registration_deadline: Utc::now() + chrono::Duration::days(25),
max_participants: Some(100),
status: HackathonStatus::Draft,
theme: Some("Technology".to_string()),
rules: Some("Follow the rules".to_string()),
prizes: Some(vec![]),
previous_winners: Some(vec![]),
organizers: vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
match db.create::<Option<HackathonSchema>>(("app_hackathons", "1"))
.content(hackathon)
.await {
Ok(_) => println!("✅ Inserted test hackathon"),
Err(_) => println!("⚠️ Test hackathon already exists, skipping"),
};
// Seed Hackathon Event
let hackathon_event = HackathonEventsSchema {
id: Thing::from(("app_hackathon_events", "test-event-001")),
hackathon_id: Thing::from(("app_hackathons", "1")),
title: "Test Event".to_string(),
description: Some("Test hackathon event description".to_string()),
event_type: HackathonEventType::Workshop,
start_time: Utc::now() + chrono::Duration::days(30),
end_time: Utc::now() + chrono::Duration::days(30) + chrono::Duration::hours(6),
location: Some("Online".to_string()),
virtual_link: None,
max_attendees: Some(50),
is_mandatory: false,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
// Try to create hackathon event, skip if already exists
match db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", "test-event-001"))
.content(hackathon_event)
.await {
Ok(_) => println!("✅ Inserted test hackathon event"),
Err(_) => println!("⚠️ Test hackathon event already exists, skipping"),
};
// Seed Hackathon Timeline
let hackathon_timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", "test-timeline-001")),
hackathon_id: Thing::from(("app_hackathons", "1")),
phase: HackathonPhase::Registration,
title: "Test Timeline".to_string(),
description: Some("Test hackathon timeline description".to_string()),
start_date: Utc::now(),
end_date: Utc::now() + chrono::Duration::days(7),
is_active: true,
order: 1,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
// Try to create hackathon timeline, skip if already exists
match db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", "test-timeline-001"))
.content(hackathon_timeline)
.await {
Ok(_) => println!("✅ Inserted test hackathon timeline"),
Err(_) => println!("⚠️ Test hackathon timeline already exists, skipping"),
};
// Seed Mentor - handle existing data
let mentor = MentorSchema {
id: Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
user_id: Some(Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))),
industries: vec!["Technology".to_string(), "Education".to_string()],
expertise: vec!["Software Development".to_string()],
languages: vec!["English".to_string(), "Indonesian".to_string()],
current_company: "Tech Corp".to_string(),
current_role: "Senior Engineer".to_string(),
years_of_experience: 5,
topics_of_interest: vec!["Rust".to_string(), "Web Development".to_string()],
preferred_mentee_level: vec!["Beginner".to_string()],
preferred_mentoring_formats: vec!["1:1".to_string(), "Group".to_string()],
availability_commitment: "Weekly".to_string(),
mentoring_rate: MentoringRate {
amount: 100,
currency: "IDR".to_string(),
per_duration: "hour".to_string(),
},
status: "active".to_string(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
match db.create::<Option<MentorSchema>>(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
.content(mentor)
.await {
Ok(_) => println!("✅ Inserted test mentor"),
Err(_) => println!("⚠️ Test mentor already exists, skipping"),
};
println!("✅ All test data seeded successfully");
Ok(())
}
@@ -0,0 +1,433 @@
use chrono::{DateTime, Utc};
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
HackathonSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
HackathonStatus, HackathonPhase, SubmissionStatus, Prize,
};
use imphnen_iam::{UsersSchema, v1::teams::TeamsSchema};
use imphnen_utils::{get_iso_date, hash_password};
use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::environment::ENV;
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.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
// Test users for submission testing
let test_users = vec![
(
"test-user-001",
"testuser1@example.com",
"Test User 1",
"5713cb37-dc02-4e87-8048-d7a41d352059", // User role
),
(
"test-user-002",
"testuser2@example.com",
"Test User 2",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
(
"test-user-003",
"testuser3@example.com",
"Test User 3",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
];
// Test teams for submission testing
let test_teams = vec![
(
"test-team-001",
"Test Team Alpha",
Some("Team for testing hackathon submissions".to_string()),
"test-user-001", // team leader
true,
Some(5),
Some(vec!["JavaScript".to_string(), "React".to_string()]),
Some("Remote".to_string()),
),
(
"test-team-002",
"Test Team Beta",
Some("Another team for testing submissions".to_string()),
"test-user-002",
true,
Some(4),
Some(vec!["Python".to_string(), "Django".to_string()]),
Some("Remote".to_string()),
),
];
// Test hackathon for submission testing
let test_hackathons = vec![
(
"test-hackathon-001",
"Test Hackathon 2025",
"Hackathon for testing submission functionality.",
"2025-12-01T09:00:00Z",
"2025-12-03T18:00:00Z",
"2025-11-25T23:59:59Z",
Some(50),
HackathonStatus::RegistrationOpen,
Some("Testing & Development".to_string()),
Some("1. Test all submission features\n2. Teams can have 2-5 members\n3. Submit by deadline".to_string()),
Some(vec![
Prize { position: 1, title: "Test Winner".to_string(), description: Some("Best test submission".to_string()), value: Some("$1,000".to_string()) },
Prize { position: 2, title: "Test Runner-up".to_string(), description: Some("Second best submission".to_string()), value: Some("$500".to_string()) },
]),
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
),
];
// Test hackathon timeline
let test_timeline = vec![
(
"test-hackathon-001",
HackathonPhase::Registration,
"Registration Phase",
Some("Register your team for the test hackathon".to_string()),
"2025-11-20T00:00:00Z",
"2025-11-25T23:59:59Z",
true,
1,
),
(
"test-hackathon-001",
HackathonPhase::Development,
"Development Phase",
Some("Build your test project".to_string()),
"2025-12-01T00:00:00Z",
"2025-12-02T23:59:59Z",
false,
2,
),
(
"test-hackathon-001",
HackathonPhase::Submission,
"Submission Phase",
Some("Submit your test project".to_string()),
"2025-12-03T00:00:00Z",
"2025-12-03T12:00:00Z",
false,
3,
),
];
// Test submissions
let test_submissions = vec![
(
"test-hackathon-001",
"test-team-001",
"Test Project Alpha",
"A comprehensive test project demonstrating all features.",
Some("https://github.com/test-team-alpha/test-project".to_string()),
Some("https://demo.test-project-alpha.com".to_string()),
Some("https://slides.test-project-alpha.com".to_string()),
vec!["JavaScript".to_string(), "React".to_string(), "Node.js".to_string()],
SubmissionStatus::Draft,
"2025-12-02T10:00:00Z",
),
(
"test-hackathon-001",
"test-team-002",
"Test Project Beta",
"Another test project with different technologies.",
Some("https://github.com/test-team-beta/test-project".to_string()),
Some("https://demo.test-project-beta.com".to_string()),
None,
vec!["Python".to_string(), "Django".to_string(), "PostgreSQL".to_string()],
SubmissionStatus::Submitted,
"2025-12-03T09:30:00Z",
),
];
// Seed test users
for (id, email, fullname, role_id) in test_users {
db.query("DELETE type::thing('app_users', $id)")
.bind(("id", id))
.await?;
let user = UsersSchema {
id: Thing::from(("app_users", id)),
fullname: fullname.into(),
legal_name: Some(format!("{} Legal Name", fullname)),
email: email.into(),
password: hash_password("password").unwrap(),
avatar: Some("https://example.com/avatar.jpg".into()),
phone_number: "081234567890".into(),
phone_for_verification: Some("081234567890".into()),
is_active: true,
is_deleted: false,
mentor_id: None,
gender: Some("male".into()),
birthdate: Some("1995-01-01".into()),
domicile: Some("Jakarta, Indonesia".into()),
bio: Some(format!("{} is a test user for hackathon submissions.", fullname)),
last_education: Some("S1 Computer Science".into()),
linkedin_url: Some("https://linkedin.com/in/testuser".into()),
github_url: Some("https://github.com/testuser".into()),
cv_url: Some("https://example.com/cv.pdf".into()),
portfolio_url: Some("https://example.com/portfolio".into()),
website_url: Some("https://example.com/website".into()),
twitter_url: Some("https://twitter.com/testuser".into()),
location: Some("Jakarta, Indonesia".into()),
skills: Some(vec!["JavaScript".to_string(), "Python".to_string()]),
experience: None,
education: None,
career_status: Some("Developer".into()),
role: Thing::from(("app_roles", role_id)),
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
db.create::<Option<UsersSchema>>(("app_users", id))
.content(user)
.await?;
println!("✅ Inserted test user: {fullname} ({email})");
}
// Seed test teams
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in test_teams {
db.query("DELETE type::thing('app_teams', $id)")
.bind(("id", id))
.await?;
let team = TeamsSchema {
id: Thing::from(("app_teams", id)),
name: name.into(),
description,
leader_id: Thing::from(("app_users", leader_id)),
is_open,
max_members,
skills_required,
location,
avatar: None,
website_url: None,
github_url: None,
is_active: true,
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
db.create::<Option<TeamsSchema>>(("app_teams", id))
.content(team)
.await?;
println!("✅ Inserted test team: {name}");
}
// Seed test hackathons
for (
id,
name,
description,
start_date,
end_date,
registration_deadline,
max_participants,
status,
theme,
rules,
prizes,
organizers,
) in test_hackathons {
db.query("DELETE type::thing('app_hackathons', $id)")
.bind(("id", id))
.await?;
let hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", id)),
name: name.into(),
description: description.into(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
max_participants,
status: status.clone(),
theme: theme.clone(),
rules: rules.clone(),
prizes: prizes.clone(),
previous_winners: None,
organizers: organizers.clone(),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
.content(hackathon)
.await?;
println!("✅ Inserted test hackathon: {name}");
// Also create an alias canonical id 'test-hackathon' so tests referencing
// /v1/hackathons/test-hackathon/... can find a hackathon record.
if id != "test-hackathon" && id.starts_with("test-hackathon") {
let alias_id = "test-hackathon";
db.query("DELETE type::thing('app_hackathons', $id)")
.bind(("id", alias_id))
.await?;
let alias_hackathon = HackathonSchema {
id: Thing::from(("app_hackathons", alias_id)),
name: name.into(),
description: description.into(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
max_participants,
status: status.clone(),
theme: theme.clone(),
rules: rules.clone(),
prizes: prizes.clone(),
previous_winners: None,
organizers: organizers.clone(),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSchema>>(("app_hackathons", alias_id))
.content(alias_hackathon)
.await?;
println!("✅ Inserted test hackathon alias: {alias_id}");
}
}
// Seed test hackathon timeline
for (
hackathon_id,
phase,
title,
description,
start_date,
end_date,
is_active,
order,
) in test_timeline {
let timeline_id = format!("test-timeline-{}-{}", hackathon_id, order);
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
.bind(("id", timeline_id.clone()))
.await?;
let timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
phase: phase.clone(),
title: title.into(),
description: description.clone(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
is_active,
order,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
.content(timeline)
.await?;
println!("✅ Inserted test hackathon timeline: {title}");
// Also create alias timeline entries for the canonical test id 'test-hackathon'
if hackathon_id != "test-hackathon" && hackathon_id.starts_with("test-hackathon") {
let alias_hackathon_id = "test-hackathon";
let alias_timeline_id = format!("test-timeline-{}-{}", alias_hackathon_id, order);
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
.bind(("id", alias_timeline_id.clone()))
.await?;
let alias_timeline = HackathonTimelineSchema {
id: Thing::from(("app_hackathon_timeline", alias_timeline_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", alias_hackathon_id)),
phase: phase.clone(),
title: title.into(),
description: description.clone(),
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
is_active,
order,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonTimelineSchema>>( ("app_hackathon_timeline", alias_timeline_id.clone()) )
.content(alias_timeline)
.await?;
println!("✅ Inserted test hackathon timeline alias: {alias_timeline_id}");
}
}
// Seed test hackathon submissions
for (
hackathon_id,
team_id,
project_name,
description,
repository_url,
demo_url,
slides_url,
technologies,
submission_status,
submitted_at,
) in test_submissions {
let submission_id = format!("test-submission-{}-{}", hackathon_id, team_id);
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
.bind(("id", submission_id.clone()))
.await?;
let submission = HackathonSubmissionsSchema {
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
team_id: Some(Thing::from(("app_teams", team_id))),
project_name: Some(project_name.into()),
description: Some(description.into()),
repository_url,
upload_file_url: None,
demo_url,
slides_url,
technologies: Some(technologies),
contact_instagram: None,
contact_twitter: None,
contact_linkedin: None,
contact_facebook: None,
contact_youtube: None,
contact_tiktok: None,
contact_other: None,
submission_status: Some(submission_status),
judge_feedback: None,
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
.content(submission)
.await?;
println!("✅ Inserted test hackathon submission: {project_name}");
}
println!("✅ All test submission data seeded");
Ok(())
}
+19 -1
View File
@@ -5,7 +5,7 @@ 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>> {
let env = &imphnen_libs::enviroment::ENV; let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any; use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
@@ -36,6 +36,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
"User", "User",
"5713cb37-dc02-4e87-8048-d7a41d352059", "5713cb37-dc02-4e87-8048-d7a41d352059",
), ),
(
"testuser1-id",
"testuser1@example.com",
"Test User 1",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
(
"testuser2-id",
"testuser2@example.com",
"Test User 2",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
(
"testuser3-id",
"testuser3@example.com",
"Test User 3",
"5713cb37-dc02-4e87-8048-d7a41d352059",
),
]; ];
for (id, email, fullname, role_id) in users { for (id, email, fullname, role_id) in users {
+2
View File
@@ -20,8 +20,10 @@ 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_hackathons")?;
run_seed("seed_gacha_rolls")?; run_seed("seed_gacha_rolls")?;
run_seed("seed_mentor_user")?; run_seed("seed_mentor_user")?;
run_seed("seed_test_data")?;
println!("\n✅ All seeding completed successfully."); println!("\n✅ All seeding completed successfully.");
Ok(()) Ok(())
} }
+8 -1
View File
@@ -1,2 +1,9 @@
pub mod v1; pub mod v1;
pub use v1::*;
pub use v1::landing;
pub use v1::landing::events;
pub use v1::landing::testimonials;
pub use v1::landing::events::events_public_routes;
pub use v1::landing::events::events_protected_routes;
pub use v1::landing::testimonials::testimonials_public_routes;
pub use v1::landing::testimonials::testimonials_protected_routes;
@@ -7,11 +7,12 @@ use super::{
}; };
use axum::extract::{Path, Query}; use axum::extract::{Path, Query};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::{Extension, Json}; use axum::{Extension, http::HeaderMap};
use imphnen_libs::{ use imphnen_libs::{
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto, ResponseSuccessDto, ValidatedJson,
}; };
use imphnen_iam::{PermissionsEnum, require_permissions};
#[utoipa::path( #[utoipa::path(
get, get,
@@ -26,7 +27,7 @@ use imphnen_libs::{
("filter_by" = Option<String>, Query, description = "Field to filter by"), ("filter_by" = Option<String>, Query, description = "Field to filter by"),
), ),
responses( responses(
(status = 200, description = "Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>) (status = 200, description = "[PUBLIC] Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
), ),
tag = "Events" tag = "Events"
)] )]
@@ -44,7 +45,7 @@ pub async fn get_event_list(
("id" = String, Path, description = "Event ID") ("id" = String, Path, description = "Event ID")
), ),
responses( responses(
(status = 200, description = "Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>) (status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
), ),
tag = "Events" tag = "Events"
)] )]
@@ -63,15 +64,18 @@ pub async fn get_event_by_id(
path = "/v1/cms/landing/events/create", path = "/v1/cms/landing/events/create",
request_body = EventsCreateRequestDto, request_body = EventsCreateRequestDto,
responses( responses(
(status = 201, description = "Create new event", body = MessageResponseDto) (status = 201, description = "[ADMIN] Create new event", body = MessageResponseDto)
), ),
tag = "Events" tag = "Events"
)] )]
pub async fn post_create_event( pub async fn post_create_event(
headers: HeaderMap,
Extension(state): Extension<AppState>, Extension(state): Extension<AppState>,
Json(payload): Json<EventsCreateRequestDto>, ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
EventsService::create_event(&state, payload).await EventsService::create_event(&state, payload).await
})
} }
#[utoipa::path( #[utoipa::path(
@@ -85,16 +89,19 @@ pub async fn post_create_event(
), ),
request_body = EventsUpdateRequestDto, request_body = EventsUpdateRequestDto,
responses( responses(
(status = 200, description = "Update event", body = MessageResponseDto) (status = 200, description = "[ADMIN] Update event", body = MessageResponseDto)
), ),
tag = "Events" tag = "Events"
)] )]
pub async fn patch_update_event( pub async fn patch_update_event(
headers: HeaderMap,
Extension(state): Extension<AppState>, Extension(state): Extension<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
Json(payload): Json<EventsUpdateRequestDto>, ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
EventsService::update_event(&state, id, payload).await EventsService::update_event(&state, id, payload).await
})
} }
#[utoipa::path( #[utoipa::path(
@@ -107,13 +114,16 @@ pub async fn patch_update_event(
("id" = String, Path, description = "Event ID") ("id" = String, Path, description = "Event ID")
), ),
responses( responses(
(status = 200, description = "Soft delete event", body = MessageResponseDto) (status = 200, description = "[ADMIN] Soft delete event", body = MessageResponseDto)
), ),
tag = "Events" tag = "Events"
)] )]
pub async fn delete_event( pub async fn delete_event(
headers: HeaderMap,
Extension(state): Extension<AppState>, Extension(state): Extension<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
EventsService::delete_event(&state, id).await EventsService::delete_event(&state, id).await
})
} }
+46 -10
View File
@@ -1,34 +1,70 @@
use lazy_static::lazy_static;
lazy_static! {
pub static ref VALID_URL_REGEX: regex::Regex =
regex::Regex::new(r"^https?://").unwrap();
}
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use regex::Regex;
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, ValidationError};
// Custom URL validator that ensures valid HTTP/HTTPS URLs
pub fn validate_url(url: &str) -> Result<(), ValidationError> {
lazy_static! {
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap();
}
if VALID_URL_REGEX.is_match(url) {
Ok(())
} else {
Err(ValidationError::new("invalid_url"))
}
}
// Custom validator for future dates
pub fn validate_future_date(end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
let now = Utc::now();
if end_date > &now {
Ok(())
} else {
Err(ValidationError::new("future_date"))
}
}
// Custom validator for event date ranges (for combined validation)
pub fn validate_date_range(start_date: &DateTime<Utc>, end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
if start_date <= end_date {
Ok(())
} else {
Err(ValidationError::new("date_range"))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[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, max = 100, message = "Name must be between 1 and 100 characters"))]
pub name: String, pub name: String,
#[validate(length(min = 1, message = "Description is required"))] #[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
pub description: String, pub description: String,
#[validate(url(message = "Detail link must be a valid URL"))] #[validate(custom(
function = "validate_url",
message = "Detail link must be a valid HTTP/HTTPS URL"
))]
pub detail_link: String, pub detail_link: String,
#[validate(range(min = 0.0, message = "Price cannot be negative"))] #[validate(range(min = 0.0, max = 1_000_000.0, message = "Price must be between 0 and 1,000,000"))]
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)]
#[validate(custom(
function = "validate_future_date",
message = "End date must be in the future"
))]
pub end_date: DateTime<Utc>, pub end_date: DateTime<Utc>,
#[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(length(max = 200, message = "Location name cannot exceed 200 characters"))]
pub location: Option<String>, pub location: Option<String>,
pub is_online: bool, pub is_online: bool,
} }
@@ -84,7 +84,7 @@ impl<'a> EventsRepository<'a> {
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> { pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now(); let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events.to_string()); let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events);
info!(query = %query_str, "Executing SurrealDB query"); info!(query = %query_str, "Executing SurrealDB query");
let record: Option<EventsSchema> = db let record: Option<EventsSchema> = db
.create(ResourceEnum::Events.to_string()) .create(ResourceEnum::Events.to_string())
+19 -25
View File
@@ -9,36 +9,30 @@ pub mod events_repository;
pub mod events_schema; pub mod events_schema;
pub mod events_service; pub mod events_service;
pub use events_controller::*; // Export only the necessary public items
pub use events_dto::*; pub use events_dto::{
pub use events_repository::*; EventsCreateRequestDto,
pub use events_schema::*; EventsUpdateRequestDto,
pub use events_service::*; EventsListItemDto,
EventsDetailItemDto,
};
pub use events_controller::{
get_event_list,
get_event_by_id,
post_create_event,
patch_update_event,
delete_event,
};
pub fn events_public_routes() -> Router { pub fn events_public_routes() -> Router {
Router::new() Router::new()
.route( .route("/cms/landing/events", get(get_event_list))
"/cms/landing/events", .route("/cms/landing/events/detail/{id}", get(get_event_by_id))
get(events_controller::get_event_list),
)
.route(
"/cms/landing/events/detail/{id}",
get(events_controller::get_event_by_id),
)
} }
pub fn events_protected_routes() -> Router { pub fn events_protected_routes() -> Router {
Router::new() Router::new()
.route( .route("/cms/landing/events/create", post(post_create_event))
"/cms/landing/events/create", .route("/cms/landing/events/update/{id}", patch(patch_update_event))
post(events_controller::post_create_event), .route("/cms/landing/events/delete/{id}", delete(delete_event))
)
.route(
"/cms/landing/events/update/{id}",
patch(events_controller::patch_update_event),
)
.route(
"/cms/landing/events/delete/{id}",
delete(events_controller::delete_event),
)
} }
+4 -2
View File
@@ -1,5 +1,7 @@
pub mod events; pub mod events;
pub mod testimonials; pub mod testimonials;
pub use events::*; pub use events::events_public_routes;
pub use testimonials::*; pub use events::events_protected_routes;
pub use testimonials::testimonials_public_routes;
pub use testimonials::testimonials_protected_routes;
+19 -25
View File
@@ -9,36 +9,30 @@ pub mod testimonials_repository;
pub mod testimonials_schema; pub mod testimonials_schema;
pub mod testimonials_service; pub mod testimonials_service;
pub use testimonials_controller::*; // Export only the necessary public items
pub use testimonials_dto::*; pub use testimonials_dto::{
pub use testimonials_repository::*; TestimonialsCreateRequestDto,
pub use testimonials_schema::*; TestimonialsUpdateRequestDto,
pub use testimonials_service::*; TestimonialsListItemDto,
TestimonialsDetailItemDto,
};
pub use testimonials_controller::{
get_testimonial_list,
get_testimonial_by_id,
post_create_testimonial,
patch_update_testimonial,
delete_testimonial,
};
pub fn testimonials_public_routes() -> Router { pub fn testimonials_public_routes() -> Router {
Router::new() Router::new()
.route( .route("/cms/landing/testimonials", get(get_testimonial_list))
"/cms/landing/testimonials", .route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
get(testimonials_controller::get_testimonial_list),
)
.route(
"/cms/landing/testimonials/detail/{id}",
get(testimonials_controller::get_testimonial_by_id),
)
} }
pub fn testimonials_protected_routes() -> Router { pub fn testimonials_protected_routes() -> Router {
Router::new() Router::new()
.route( .route("/cms/landing/testimonials/create", post(post_create_testimonial))
"/cms/landing/testimonials/create", .route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
post(testimonials_controller::post_create_testimonial), .route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
)
.route(
"/cms/landing/testimonials/update/{id}",
patch(testimonials_controller::patch_update_testimonial),
)
.route(
"/cms/landing/testimonials/delete/{id}",
delete(testimonials_controller::delete_testimonial),
)
} }
@@ -7,11 +7,11 @@ use super::{
}; };
use axum::extract::{Path, Query}; use axum::extract::{Path, Query};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::{Extension, Json}; use axum::{Extension, http::HeaderMap};
use imphnen_iam::UsersDetailQueryDto; use imphnen_iam::{UsersDetailQueryDto, require_auth};
use imphnen_libs::{ use imphnen_libs::{
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto, ResponseSuccessDto, ValidatedJson,
}; };
#[utoipa::path( #[utoipa::path(
@@ -27,7 +27,7 @@ use imphnen_libs::{
("filter_by" = Option<String>, Query, description = "Field to filter by"), ("filter_by" = Option<String>, Query, description = "Field to filter by"),
), ),
responses( responses(
(status = 200, description = "Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>) (status = 200, description = "[PUBLIC] Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
), ),
tag = "Testimonials" tag = "Testimonials"
)] )]
@@ -45,7 +45,7 @@ pub async fn get_testimonial_list(
("id" = String, Path, description = "Testimonial ID") ("id" = String, Path, description = "Testimonial ID")
), ),
responses( responses(
(status = 200, description = "Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>) (status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
), ),
tag = "Testimonials" tag = "Testimonials"
)] )]
@@ -64,16 +64,19 @@ pub async fn get_testimonial_by_id(
path = "/v1/cms/landing/testimonials/create", path = "/v1/cms/landing/testimonials/create",
request_body = TestimonialsCreateRequestDto, request_body = TestimonialsCreateRequestDto,
responses( responses(
(status = 201, description = "Create new testimonial", body = MessageResponseDto) (status = 201, description = "[USER] Create new testimonial", body = MessageResponseDto)
), ),
tag = "Testimonials" tag = "Testimonials"
)] )]
pub async fn post_create_testimonial( pub async fn post_create_testimonial(
headers: HeaderMap,
Extension(state): Extension<AppState>, Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>, Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsCreateRequestDto>, ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
require_auth!(headers, state, {
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
})
} }
#[utoipa::path( #[utoipa::path(
@@ -87,18 +90,20 @@ pub async fn post_create_testimonial(
), ),
request_body = TestimonialsUpdateRequestDto, request_body = TestimonialsUpdateRequestDto,
responses( responses(
(status = 200, description = "Update testimonial", body = MessageResponseDto) (status = 200, description = "[USER] Update testimonial", body = MessageResponseDto)
), ),
tag = "Testimonials" tag = "Testimonials"
)] )]
pub async fn patch_update_testimonial( pub async fn patch_update_testimonial(
headers: HeaderMap,
Path(id): Path<String>, Path(id): Path<String>,
Extension(state): Extension<AppState>, Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>, Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsUpdateRequestDto>, ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user) require_auth!(headers, state, {
.await TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
})
} }
#[utoipa::path( #[utoipa::path(
@@ -111,14 +116,17 @@ pub async fn patch_update_testimonial(
("id" = String, Path, description = "Testimonial ID") ("id" = String, Path, description = "Testimonial ID")
), ),
responses( responses(
(status = 200, description = "Soft delete testimonial", body = MessageResponseDto) (status = 200, description = "[USER] Soft delete testimonial", body = MessageResponseDto)
), ),
tag = "Testimonials" tag = "Testimonials"
)] )]
pub async fn delete_testimonial( pub async fn delete_testimonial(
headers: HeaderMap,
Extension(state): Extension<AppState>, Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>, Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Path(id): Path<String>, Path(id): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
require_auth!(headers, state, {
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
})
} }
@@ -1,31 +1,53 @@
use imphnen_iam::users::UsersSchema; use imphnen_iam::v1::users::UsersSchema;
use lazy_static::lazy_static;
use regex::Regex;
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, ValidationError};
// Custom validator for content length and format
pub fn validate_testimonial_content(content: &str) -> Result<(), ValidationError> {
lazy_static! {
static ref CONTENT_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9\s.,!?'-]+$").unwrap();
}
if CONTENT_REGEX.is_match(content) && content.len() <= 1000 {
Ok(())
} else {
Err(ValidationError::new("invalid_content"))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct TestimonialsCreateRequestDto { pub struct TestimonialsCreateRequestDto {
#[validate(length(min = 1, message = "Role is required"))] #[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
pub role: String, pub role: String,
#[validate(length( #[validate(length(
min = 1, min = 1,
max = 500, max = 1000,
message = "Content must be between 1 and 500 characters" message = "Content must be between 1 and 1000 characters"
))]
#[validate(custom(
function = "validate_testimonial_content",
message = "Content contains invalid characters or is too long"
))] ))]
pub content: String, pub content: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct TestimonialsUpdateRequestDto { pub struct TestimonialsUpdateRequestDto {
#[validate(length(min = 1, message = "Role is required"))] #[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
pub role: String, pub role: String,
#[validate(length( #[validate(length(
min = 1, min = 1,
max = 500, max = 1000,
message = "Content must be between 1 and 500 characters" message = "Content must be between 1 and 1000 characters"
))]
#[validate(custom(
function = "validate_testimonial_content",
message = "Content contains invalid characters or is too long"
))] ))]
pub content: String, pub content: String,
} }
@@ -52,8 +52,14 @@ impl<'a> TestimonialsRepository<'a> {
) -> Result<TestimonialsQueryDto> { ) -> Result<TestimonialsQueryDto> {
let now = Instant::now(); let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
// Extract raw id if id is a thing string
let raw_id = if id.contains(':') {
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
} else {
id
};
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_id(&id) .with_id(&raw_id)
.with_condition("is_deleted = false") .with_condition("is_deleted = false")
.with_select_fields(vec!["*", "user.* as user"]); .with_select_fields(vec!["*", "user.* as user"]);
let sql = builder.build(); let sql = builder.build();
@@ -72,8 +72,15 @@ impl TestimonialsSchema {
id: String, id: String,
user_id: &Thing, user_id: &Thing,
) -> Self { ) -> Self {
// Normalize id: accept either raw id (uuid) or Thing-formatted id like "table:⟨id⟩"
let raw_id = if id.contains(':') {
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
} else {
id
};
Self { Self {
id: make_thing(&ResourceEnum::Testimonials.to_string(), &id), id: make_thing(&ResourceEnum::Testimonials.to_string(), &raw_id),
role: payload.role, role: payload.role,
content: payload.content, content: payload.content,
updated_at: get_iso_date(), updated_at: get_iso_date(),
+6 -1
View File
@@ -1,3 +1,8 @@
pub mod landing; pub mod landing;
pub use landing::*; pub use landing::events;
pub use landing::testimonials;
pub use landing::events::events_public_routes;
pub use landing::events::events_protected_routes;
pub use landing::testimonials::testimonials_public_routes;
pub use landing::testimonials::testimonials_protected_routes;
+2
View File
@@ -8,7 +8,9 @@ imphnen-libs.workspace = true
imphnen-utils.workspace = true imphnen-utils.workspace = true
imphnen-entities.workspace = true imphnen-entities.workspace = true
imphnen-iam.workspace = true imphnen-iam.workspace = true
imphnen-middleware.workspace = true
axum.workspace = true axum.workspace = true
async-trait.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
utoipa.workspace = true utoipa.workspace = true
+11 -1
View File
@@ -1,2 +1,12 @@
pub mod v1; pub mod v1;
pub use v1::*;
// Explicitly export only what's needed from v1
pub use v1::dimentorin_router;
pub use v1::mentors::mentors_router;
pub use v1::sessions::{
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
AvailabilitySlotDto,
};
@@ -4,30 +4,30 @@ use super::{
}; };
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto; use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
use ::axum::{ use ::axum::{
extract::{Extension, Json, Path, Query}, extract::{Extension, Path, Query},
http::HeaderMap, http::HeaderMap,
response::{IntoResponse, Response}, response::Response,
}; };
use imphnen_entities::*; use imphnen_entities::MetaRequestDto;
use imphnen_iam::{PermissionsEnum, permissions_guard}; use imphnen_libs::{AppState, ValidatedJson};
use imphnen_iam::{PermissionsEnum, require_permissions};
use imphnen_utils::extract_email; use imphnen_utils::extract_email;
use serde_json::json;
#[utoipa::path( #[utoipa::path(
post, post,
path = "/v1/mentors/register", path = "/v1/mentors/create",
request_body = MentorUserRegisterRequestDto, request_body = MentorUserRegisterRequestDto,
responses( responses(
(status = 200, description = "Mentor registered successfully", body = MentorRegisterResponseDto), (status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
(status = 400, description = "Bad request - validation error"), (status = 400, description = "[PUBLIC] Bad request - validation error"),
(status = 409, description = "Conflict - user already has mentor profile"), (status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
(status = 500, description = "Internal server error") (status = 500, description = "[PUBLIC] Internal server error")
), ),
tag = "Mentors" tag = "Mentors"
)] )]
pub async fn post_register_mentor( pub async fn post_register_mentor(
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
Json(dto): Json<MentorUserRegisterRequestDto>, ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
) -> Response { ) -> Response {
MentorsService::register_mentor(&app_state, dto).await MentorsService::register_mentor(&app_state, dto).await
} }
@@ -43,8 +43,8 @@ pub async fn post_register_mentor(
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"), ("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
), ),
responses( responses(
(status = 200, description = "Get list of mentors", body = Vec<MentorListResponseDto>), (status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
(status = 500, description = "Internal server error") (status = 500, description = "[ADMIN] Internal server error")
), ),
tag = "Mentors", tag = "Mentors",
security( security(
@@ -56,16 +56,9 @@ pub async fn get_mentor_list(
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
Query(meta): Query<MetaRequestDto>, Query(meta): Query<MetaRequestDto>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
headers, MentorsService::get_mentor_list(&app_state, meta).await
Extension(app_state), })
vec![PermissionsEnum::ReadListMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::get_mentor_list(&app_state, meta).await,
Err(response) => response,
}
} }
#[utoipa::path( #[utoipa::path(
@@ -75,9 +68,9 @@ pub async fn get_mentor_list(
("id" = String, Path, description = "Mentor ID") ("id" = String, Path, description = "Mentor ID")
), ),
responses( responses(
(status = 200, description = "Get mentor by ID", body = MentorDetailResponseDto), (status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
(status = 404, description = "Mentor not found"), (status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "Internal server error") (status = 500, description = "[ADMIN] Internal server error")
), ),
tag = "Mentors", tag = "Mentors",
security( security(
@@ -89,16 +82,9 @@ pub async fn get_mentor_by_id(
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
headers, MentorsService::get_mentor_by_id(&app_state, &id).await
Extension(app_state), })
vec![PermissionsEnum::ReadDetailMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::get_mentor_by_id(&app_state, &id).await,
Err(response) => response,
}
} }
#[utoipa::path( #[utoipa::path(
@@ -109,10 +95,10 @@ pub async fn get_mentor_by_id(
), ),
request_body = MentorUpdateRequestDto, request_body = MentorUpdateRequestDto,
responses( responses(
(status = 200, description = "Mentor updated successfully", body = MentorDetailResponseDto), (status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"), (status = 400, description = "[ADMIN] Bad request - validation error"),
(status = 404, description = "Mentor not found"), (status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "Internal server error") (status = 500, description = "[ADMIN] Internal server error")
), ),
tag = "Mentors - Admin", tag = "Mentors - Admin",
security( security(
@@ -123,18 +109,11 @@ pub async fn put_update_mentor(
headers: HeaderMap, headers: HeaderMap,
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
Json(dto): Json<MentorUpdateRequestDto>, ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
headers, MentorsService::update_mentor(&app_state, &id, dto).await
Extension(app_state), })
vec![PermissionsEnum::UpdateMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::update_mentor(&app_state, &id, dto).await,
Err(response) => response,
}
} }
#[utoipa::path( #[utoipa::path(
@@ -144,9 +123,9 @@ pub async fn put_update_mentor(
("id" = String, Path, description = "Mentor ID") ("id" = String, Path, description = "Mentor ID")
), ),
responses( responses(
(status = 200, description = "Mentor deleted successfully"), (status = 200, description = "[ADMIN] Mentor deleted successfully"),
(status = 404, description = "Mentor not found"), (status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "Internal server error") (status = 500, description = "[ADMIN] Internal server error")
), ),
tag = "Mentors - Admin", tag = "Mentors - Admin",
security( security(
@@ -158,16 +137,9 @@ pub async fn delete_mentor(
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
headers, MentorsService::delete_mentor(&app_state, &id).await
Extension(app_state), })
vec![PermissionsEnum::DeleteMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::delete_mentor(&app_state, &id).await,
Err(response) => response,
}
} }
#[utoipa::path( #[utoipa::path(
@@ -178,10 +150,10 @@ pub async fn delete_mentor(
), ),
request_body = MentorVerifyRequestDto, request_body = MentorVerifyRequestDto,
responses( responses(
(status = 200, description = "Mentor verified successfully", body = MentorDetailResponseDto), (status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"), (status = 400, description = "[ADMIN] Bad request - validation error"),
(status = 404, description = "Mentor not found"), (status = 404, description = "[ADMIN] Mentor not found"),
(status = 500, description = "Internal server error") (status = 500, description = "[ADMIN] Internal server error")
), ),
tag = "Mentors - Admin", tag = "Mentors - Admin",
security( security(
@@ -192,28 +164,21 @@ pub async fn put_verify_mentor(
headers: HeaderMap, headers: HeaderMap,
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
Json(dto): Json<MentorVerifyRequestDto>, ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
headers, MentorsService::verify_mentor(&app_state, &id, dto).await
Extension(app_state), })
vec![PermissionsEnum::VerifyMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::verify_mentor(&app_state, &id, dto).await,
Err(response) => response,
}
} }
#[utoipa::path( #[utoipa::path(
get, get,
path = "/v1/mentors/me", path = "/v1/mentors/me",
responses( responses(
(status = 200, description = "Current user's mentor profile", body = MentorDetailResponseDto), (status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
(status = 401, description = "Unauthorized - invalid token"), (status = 401, description = "[MENTOR] Unauthorized - invalid token"),
(status = 403, description = "Mentor profile not found for current user"), (status = 403, description = "[MENTOR] Mentor profile not found for current user"),
(status = 500, description = "Internal server error") (status = 500, description = "[MENTOR] Internal server error")
), ),
tag = "Mentors", tag = "Mentors",
security( security(
@@ -224,43 +189,30 @@ pub async fn get_mentor_me(
headers: HeaderMap, headers: HeaderMap,
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
headers.clone(),
Extension(app_state),
vec![PermissionsEnum::ReadOwnMentorProfile],
)
.await
{
Ok((_user, app_state)) => {
let email = match extract_email(&headers) { let email = match extract_email(&headers) {
Some(email) => email, Some(email) => email,
None => { None => {
return ( return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED, axum::http::StatusCode::UNAUTHORIZED,
Json(json!({ "Token tidak valid",
"error": "Unauthorized", );
"message": "Token tidak valid"
})),
)
.into_response();
} }
}; };
MentorsService::get_mentor_me(&app_state, &email).await MentorsService::get_mentor_me(&app_state, &email).await
} })
Err(response) => response,
}
} }
#[utoipa::path( #[utoipa::path(
put, put,
path = "/v1/mentors/update/me", path = "/v1/mentors/me/update",
request_body = MentorUpdateRequestDto, request_body = MentorUpdateRequestDto,
responses( responses(
(status = 200, description = "Mentor profile updated successfully", body = MentorDetailResponseDto), (status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"), (status = 400, description = "[MENTOR] Bad request - validation error"),
(status = 401, description = "Unauthorized - invalid token"), (status = 401, description = "[MENTOR] Unauthorized - invalid token"),
(status = 404, description = "Mentor profile not found"), (status = 404, description = "[MENTOR] Mentor profile not found"),
(status = 500, description = "Internal server error") (status = 500, description = "[MENTOR] Internal server error")
), ),
tag = "Mentors", tag = "Mentors",
security( security(
@@ -270,16 +222,9 @@ pub async fn get_mentor_me(
pub async fn put_update_mentor_me( pub async fn put_update_mentor_me(
headers: HeaderMap, headers: HeaderMap,
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
Json(dto): Json<MentorUpdateRequestDto>, ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
headers.clone(),
Extension(app_state),
vec![PermissionsEnum::UpdateOwnMentorProfile],
)
.await
{
Ok((_user, app_state)) => {
let email = match extract_email(&headers) { let email = match extract_email(&headers) {
Some(email) => email, Some(email) => email,
None => { None => {
@@ -290,16 +235,14 @@ pub async fn put_update_mentor_me(
} }
}; };
MentorsService::update_mentor_me(&app_state, &email, dto).await MentorsService::update_mentor_me(&app_state, &email, dto).await
} })
Err(response) => response,
}
} }
#[utoipa::path( #[utoipa::path(
put, put,
path = "/v1/mentors/update", path = "/v1/mentors/update",
request_body = MentorUpdateRequestDto, request_body = MentorUpdateRequestDto,
responses( responses(
(status = 400, description = "Bad request - Mentor ID is required for update"), (status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
), ),
tag = "Mentors - Admin" tag = "Mentors - Admin"
)] )]
@@ -312,12 +255,12 @@ pub async fn put_update_mentor_no_id() -> Response {
#[utoipa::path( #[utoipa::path(
get, get,
path = "/v1/mentors/status", path = "/v1/mentors/me/status",
responses( responses(
(status = 200, description = "Mentor application status", body = String), (status = 200, description = "[MENTOR] Mentor application status", body = String),
(status = 401, description = "Unauthorized - invalid token"), (status = 401, description = "[MENTOR] Unauthorized - invalid token"),
(status = 403, description = "No mentor application found for current user"), (status = 403, description = "[MENTOR] No mentor application found for current user"),
(status = 500, description = "Internal server error") (status = 500, description = "[MENTOR] Internal server error")
), ),
tag = "Mentors", tag = "Mentors",
security( security(
@@ -328,29 +271,16 @@ pub async fn get_mentor_status(
headers: HeaderMap, headers: HeaderMap,
Extension(app_state): Extension<AppState>, Extension(app_state): Extension<AppState>,
) -> Response { ) -> Response {
match permissions_guard( require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
headers.clone(),
Extension(app_state),
vec![PermissionsEnum::ReadOwnMentorStatus],
)
.await
{
Ok((_user, app_state)) => {
let email = match extract_email(&headers) { let email = match extract_email(&headers) {
Some(email) => email, Some(email) => email,
None => { None => {
return ( return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED, axum::http::StatusCode::UNAUTHORIZED,
Json(json!({ "Token tidak valid",
"error": "Unauthorized", );
"message": "Token tidak valid"
})),
)
.into_response();
} }
}; };
MentorsService::get_mentor_status(&app_state, &email).await MentorsService::get_mentor_status(&app_state, &email).await
} })
Err(response) => response,
}
} }
@@ -167,7 +167,7 @@ pub struct MentorUserRegisterRequestDto {
message = "Password must have at least 8 characters" message = "Password must have at least 8 characters"
))] ))]
#[validate(custom( #[validate(custom(
function = "imphnen_iam::auth_dto::validate_password_complexity", function = "imphnen_iam::v1::auth::auth_dto::validate_password_complexity",
message = "Password must include uppercase, lowercase, number, and special character" message = "Password must include uppercase, lowercase, number, and special character"
))] ))]
pub password: String, pub password: String,
@@ -2,7 +2,8 @@ use anyhow::{Result, bail};
use imphnen_iam::{get_id, make_thing}; use imphnen_iam::{get_id, make_thing};
use surrealdb::sql::Thing; use surrealdb::sql::Thing;
use crate::v1::mentors::{MentorDetailWithUserDto, MentorInsertDto, MentorSchema}; use crate::v1::mentors::mentors_dto::MentorDetailWithUserDto;
use crate::v1::mentors::{MentorInsertDto, MentorSchema};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date}; use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
@@ -122,8 +123,18 @@ impl<'a> MentorsRepository<'a> {
) -> Result<MentorDetailWithUserDto> { ) -> Result<MentorDetailWithUserDto> {
let now = Instant::now(); let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
.with_id(get_id(id)?.1) // Validate ID format first
let mentor_id = match get_id(id) {
Ok((_, id_str)) => id_str,
Err(_) => bail!("Invalid mentor ID format"),
};
let mentors_table = ResourceEnum::Mentors.to_string();
// Build query with proper ID binding
let mut builder = DetailQueryBuilder::new(mentors_table.clone())
.with_id(mentor_id) // Use the extracted ID string
.with_select_fields(vec![ .with_select_fields(vec![
"id", "id",
"user_id", "user_id",
@@ -151,20 +162,23 @@ impl<'a> MentorsRepository<'a> {
let sql = builder.build(); let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id"); info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
let mentor_opt: Option<MentorDetailWithUserDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; let mentor_opt: Option<MentorDetailWithUserDto> = builder
.apply_bindings(db.query(sql))
.await?
.take(0)?;
let elapsed = now.elapsed(); let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development" == "development"
{ {
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}"); println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
} }
let Some(mentor) = mentor_opt else { let Some(mentor) = mentor_opt else {
bail!("Mentor not found in database"); bail!("Mentor not found");
}; };
if mentor.is_deleted && !include_deleted {
bail!("Mentor has been deleted");
}
Ok(mentor) Ok(mentor)
} }
@@ -6,8 +6,9 @@ use crate::v1::mentors::{
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::Response; use axum::response::Response;
use imphnen_entities::{ use imphnen_entities::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
}; };
use imphnen_libs::AppState;
use imphnen_iam::{ use imphnen_iam::{
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema, AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
}; };
@@ -33,9 +34,9 @@ impl MentorsService {
let user_repo = UsersRepository::new(state); let user_repo = UsersRepository::new(state);
let mentor_repo = MentorsRepository::new(state); let mentor_repo = MentorsRepository::new(state);
let role_repo = RolesRepository::new(state); let role_repo = RolesRepository::new(state);
let auth_repo = AuthRepository::new(state); let auth_repo = AuthRepository::new(state.surrealdb_mem.clone());
let user_email = dto.email.clone(); let user_email = &dto.email;
let mut _user_to_update: Option<UsersSchema> = None; let mut _user_to_update: Option<UsersSchema> = None;
let existing_user_result = let existing_user_result =
@@ -56,7 +57,7 @@ impl MentorsService {
); );
} }
let mut user_schema = UsersSchema::from(user_detail_query_dto.clone()); let mut user_schema = UsersSchema::from(user_detail_query_dto);
user_schema.fullname = dto.fullname.clone(); user_schema.fullname = dto.fullname.clone();
user_schema.phone_number = dto.phone_number.clone(); user_schema.phone_number = dto.phone_number.clone();
@@ -99,7 +100,7 @@ impl MentorsService {
} }
}; };
user_schema.role = user_schema.role =
imphnen_utils::make_thing(&ResourceEnum::Roles.to_string(), &mentor_role.id); imphnen_utils::make_thing_from_enum(ResourceEnum::Roles, &mentor_role.id);
user_schema.is_active = false; user_schema.is_active = false;
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await { if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
@@ -139,14 +140,14 @@ impl MentorsService {
}; };
let new_user_schema = UsersSchema { let new_user_schema = UsersSchema {
id: imphnen_utils::make_thing( id: imphnen_utils::make_thing_from_enum(
&ResourceEnum::Users.to_string(), ResourceEnum::Users,
&Uuid::new_v4().to_string(), &Uuid::new_v4().to_string(),
), ),
email: dto.email.clone(), email: dto.email,
fullname: dto.fullname.clone(), fullname: dto.fullname,
password: hashed_password, password: hashed_password,
phone_number: dto.phone_number.clone(), phone_number: dto.phone_number,
// Store personal data from identity_and_verification in user // Store personal data from identity_and_verification in user
legal_name: Some(dto.identity_and_verification.legal_name.clone()), legal_name: Some(dto.identity_and_verification.legal_name.clone()),
gender: dto.identity_and_verification.gender.clone(), gender: dto.identity_and_verification.gender.clone(),
@@ -161,8 +162,8 @@ impl MentorsService {
portfolio_url: dto.professional_profile.portfolio_url.clone(), portfolio_url: dto.professional_profile.portfolio_url.clone(),
created_at: imphnen_utils::get_iso_date(), created_at: imphnen_utils::get_iso_date(),
updated_at: imphnen_utils::get_iso_date(), updated_at: imphnen_utils::get_iso_date(),
role: imphnen_utils::make_thing( role: imphnen_utils::make_thing_from_enum(
&ResourceEnum::Roles.to_string(), ResourceEnum::Roles,
&mentor_role.id, &mentor_role.id,
), ),
is_active: false, is_active: false,
@@ -186,11 +187,11 @@ impl MentorsService {
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp(); let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
match auth_repo match auth_repo
.query_store_otp(final_user_email.clone(), otp) .query_store_otp(final_user_email.clone(), otp.clone())
.await .await
{ {
Ok(_) => { Ok(_) => {
let message = format!("your otp code is {otp}"); let message = format!("your otp code is {}", otp.code);
if let Err(_err) = if let Err(_err) =
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message) imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
{ {
+39 -8
View File
@@ -9,19 +9,50 @@ pub mod mentors_repository;
pub mod mentors_schema; pub mod mentors_schema;
pub mod mentors_service; pub mod mentors_service;
pub use mentors_controller::*; // Explicitly export only public controller functions and key types
pub use mentors_dto::*; pub use mentors_controller::{
pub use mentors_repository::*; post_register_mentor,
pub use mentors_schema::*; get_mentor_list,
pub use mentors_service::*; get_mentor_by_id,
put_update_mentor,
delete_mentor,
put_verify_mentor,
get_mentor_me,
put_update_mentor_me,
put_update_mentor_no_id,
get_mentor_status,
};
// Export key DTO types used across the API
pub use mentors_dto::{
MentorListResponseDto,
MentorDetailResponseDto,
MentorRegisterResponseDto,
MentorUpdateRequestDto,
MentorUserRegisterRequestDto,
MentorVerifyRequestDto,
MentorDetailQueryDto,
ProfessionalProfile,
MentoringLogistics,
MentoringRate,
IdentityAndVerification,
MentorInsertDto,
};
// Export service and repository for internal use
pub use mentors_service::MentorsService;
pub use mentors_repository::MentorsRepository;
// Export schema types for database interactions
pub use mentors_schema::MentorSchema;
pub fn mentors_router() -> Router { pub fn mentors_router() -> Router {
Router::new() Router::new()
.route("/", get(get_mentor_list)) .route("/", get(get_mentor_list))
.route("/register", post(post_register_mentor)) .route("/create", post(post_register_mentor))
.route("/me", get(get_mentor_me)) .route("/me", get(get_mentor_me))
.route("/update/me", put(put_update_mentor_me)) .route("/me/update", put(put_update_mentor_me))
.route("/status", get(get_mentor_status)) .route("/me/status", get(get_mentor_status))
.route("/detail/{id}", get(get_mentor_by_id)) .route("/detail/{id}", get(get_mentor_by_id))
.route("/update/{id}", put(put_update_mentor)) .route("/update/{id}", put(put_update_mentor))
.route("/update", put(put_update_mentor_no_id)) .route("/update", put(put_update_mentor_no_id))
+20 -1
View File
@@ -1,7 +1,26 @@
use axum::Router; use axum::Router;
pub mod mentors; pub mod mentors;
pub mod sessions;
/// Creates the main Dimentorin router with all version 1 endpoints
/// Routes:
/// - /mentors -> mentors::mentors_router()
/// - /sessions -> sessions::sessions_router()
/// - /users/me/sessions -> sessions::get_my_sessions()
pub fn dimentorin_router() -> Router { pub fn dimentorin_router() -> Router {
Router::new().nest("/mentors", mentors::mentors_router()) Router::new()
.nest("/mentors", mentors::mentors_router())
.merge(sessions::sessions_router())
} }
// Explicitly re-export key items for easier consumption
pub use mentors::mentors_router;
pub use mentors::MentorsService;
pub use mentors::MentorsRepository;
pub use mentors::MentorSchema;
pub use sessions::sessions_router;
pub use sessions::SessionsService;
pub use sessions::SessionsRepository;
pub use sessions::SessionSchema;
+11
View File
@@ -0,0 +1,11 @@
pub mod sessions_controller;
pub mod sessions_dto;
pub mod sessions_repository;
pub mod sessions_schema;
pub mod sessions_service;
pub use sessions_controller::*;
pub use sessions_dto::*;
pub use sessions_repository::*;
pub use sessions_schema::*;
pub use sessions_service::*;
@@ -0,0 +1,297 @@
use super::{
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
SessionsService, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
};
use axum::{
extract::{Extension, Path, Query},
http::HeaderMap,
response::Response,
routing::{get, post, put},
Json, Router,
};
use imphnen_libs::AppState;
use imphnen_utils::extract_email;
use serde::Deserialize;
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
post_book_session,
get_mentor_sessions,
get_mentor_availability,
put_update_session_status,
post_submit_feedback,
get_my_sessions,
),
components(schemas(
BookSessionRequestDto,
BookSessionResponseDto,
SessionListResponseDto,
super::SessionListItemDto,
MentorAvailabilityDto,
super::AvailabilitySlotDto,
UpdateSessionStatusRequestDto,
UpdateSessionStatusResponseDto,
SessionFeedbackRequestDto,
SessionFeedbackResponseDto,
)),
tags(
(name = "sessions", description = "Mentoring Sessions Management API")
)
)]
pub struct SessionsApiDoc;
// ============================================
// Book Session
// ============================================
#[utoipa::path(
post,
path = "/v1/mentors/{id}/sessions/create",
tag = "sessions",
summary = "Book a mentoring session",
description = "Book a mentoring session with a specific mentor. Requires authentication.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Mentor ID"),
),
request_body = BookSessionRequestDto,
responses(
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
(status = 400, description = "Invalid request"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn post_book_session(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(mentor_id): Path<String>,
Json(dto): Json<BookSessionRequestDto>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::book_session(&state, mentor_id, user_email, dto).await
}
// ============================================
// Get Mentor's Sessions
// ============================================
#[derive(Deserialize)]
pub struct SessionStatusFilter {
status: Option<String>,
}
#[utoipa::path(
get,
path = "/v1/mentors/{id}/sessions",
tag = "sessions",
summary = "List mentor's sessions",
description = "Get all sessions for a specific mentor. Only accessible by the mentor themselves or admin.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Mentor ID"),
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
),
responses(
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn get_mentor_sessions(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(mentor_id): Path<String>,
Query(filter): Query<SessionStatusFilter>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::get_mentor_sessions(&state, mentor_id, user_email, filter.status).await
}
// ============================================
// Get Mentor Availability
// ============================================
#[utoipa::path(
get,
path = "/v1/mentors/{id}/availability",
tag = "sessions",
summary = "Get mentor availability",
description = "Get available time slots for booking with a mentor. Public endpoint.",
params(
("id" = String, Path, description = "Mentor ID"),
),
responses(
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
(status = 404, description = "Mentor not found"),
)
)]
pub async fn get_mentor_availability(
Extension(state): Extension<AppState>,
Path(mentor_id): Path<String>,
) -> Response {
SessionsService::get_mentor_availability(&state, mentor_id).await
}
// ============================================
// Update Session Status
// ============================================
#[utoipa::path(
put,
path = "/v1/sessions/update/{id}/status",
tag = "sessions",
summary = "Update session status",
description = "Update the status of a session (confirm, complete, cancel). Only accessible by the mentor.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Session ID"),
),
request_body = UpdateSessionStatusRequestDto,
responses(
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
(status = 400, description = "Invalid request"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Session not found"),
)
)]
pub async fn put_update_session_status(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(session_id): Path<String>,
Json(dto): Json<UpdateSessionStatusRequestDto>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::update_session_status(&state, session_id, user_email, dto).await
}
// ============================================
// Submit Feedback
// ============================================
#[utoipa::path(
post,
path = "/v1/sessions/{id}/feedback/create",
tag = "sessions",
summary = "Submit session feedback",
description = "Submit feedback and rating for a completed session. Only accessible by the mentee.",
security(("Bearer" = [])),
params(
("id" = String, Path, description = "Session ID"),
),
request_body = SessionFeedbackRequestDto,
responses(
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
(status = 400, description = "Invalid request or session not completed"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Session not found"),
)
)]
pub async fn post_submit_feedback(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(session_id): Path<String>,
Json(dto): Json<SessionFeedbackRequestDto>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::submit_feedback(&state, session_id, user_email, dto).await
}
// ============================================
// Get User's Sessions
// ============================================
#[utoipa::path(
get,
path = "/v1/users/me/sessions",
tag = "sessions",
summary = "Get my sessions",
description = "Get all sessions for the authenticated user (as mentee). Requires authentication.",
security(("Bearer" = [])),
params(
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
),
responses(
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
(status = 401, description = "Unauthorized"),
)
)]
pub async fn get_my_sessions(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Query(filter): Query<SessionStatusFilter>,
) -> Response {
let user_email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
SessionsService::get_user_sessions(&state, user_email, filter.status).await
}
// ============================================
// Router
// ============================================
pub fn sessions_router() -> Router {
Router::new()
// Book session (under mentors path)
.route("/mentors/{id}/sessions/create", post(post_book_session))
// Get mentor's sessions
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
// Get mentor availability (public - no auth)
.route("/mentors/{id}/availability", get(get_mentor_availability))
// Update session status
.route("/sessions/update/{id}/status", put(put_update_session_status))
// Submit feedback
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
// Get my sessions
.route("/users/me/sessions", get(get_my_sessions))
}
@@ -0,0 +1,197 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use validator::Validate;
// ============================================
// Book Session (POST /v1/mentors/{id}/sessions/book)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct BookSessionRequestDto {
#[validate(length(min = 3, max = 200, message = "Topic must be 3-200 characters"))]
pub topic: String,
#[validate(length(max = 1000, message = "Description must be max 1000 characters"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[validate(length(min = 1, message = "Scheduled time is required"))]
pub scheduled_at: String, // ISO 8601 datetime
#[validate(range(min = 15, max = 240, message = "Duration must be 15-240 minutes"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_minutes: Option<i32>,
#[validate(length(max = 50, message = "Session type must be max 50 characters"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub session_type: Option<String>, // "video_call", "phone_call", "chat"
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct BookSessionResponseDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String,
pub duration_minutes: i32,
pub session_type: String,
pub status: String,
pub created_at: String,
}
// ============================================
// List Sessions (GET /v1/mentors/{id}/sessions & /v1/users/me/sessions)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionListItemDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub mentee_fullname: Option<String>,
pub mentee_email: Option<String>,
pub topic: String,
pub scheduled_at: String,
pub duration_minutes: i32,
pub session_type: String,
pub status: String,
pub rating: Option<i32>,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionListResponseDto {
pub sessions: Vec<SessionListItemDto>,
pub total: usize,
}
// ============================================
// Session Detail
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionDetailDto {
pub id: String,
pub mentor_id: String,
pub mentor_fullname: Option<String>,
pub mentee_id: String,
pub mentee_fullname: Option<String>,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String,
pub duration_minutes: i32,
pub meeting_link: Option<String>,
pub session_type: String,
pub status: String,
pub feedback: Option<String>,
pub rating: Option<i32>,
pub feedback_submitted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
// ============================================
// Mentor Availability (GET /v1/mentors/{id}/availability)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct AvailabilitySlotDto {
pub date: String, // YYYY-MM-DD
pub time: String, // HH:MM
pub available: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct MentorAvailabilityDto {
pub mentor_id: String,
pub availability_commitment: String,
pub preferred_formats: Vec<String>,
pub slots: Vec<AvailabilitySlotDto>,
pub booked_dates: Vec<String>, // Dates with existing sessions
}
// ============================================
// Update Session Status (PUT /v1/sessions/{id}/status)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct UpdateSessionStatusRequestDto {
#[validate(length(min = 1, max = 50, message = "Status must be 1-50 characters"))]
pub status: String, // "confirmed", "completed", "cancelled", "no_show"
#[validate(url(message = "Meeting link must be a valid URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub meeting_link: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct UpdateSessionStatusResponseDto {
pub id: String,
pub status: String,
pub meeting_link: Option<String>,
pub updated_at: String,
}
// ============================================
// Submit Feedback (POST /v1/sessions/{id}/feedback)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct SessionFeedbackRequestDto {
#[validate(length(min = 10, max = 2000, message = "Feedback must be 10-2000 characters"))]
pub feedback: String,
#[validate(range(min = 1, max = 5, message = "Rating must be 1-5"))]
pub rating: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SessionFeedbackResponseDto {
pub id: String,
pub feedback: String,
pub rating: i32,
pub submitted_at: String,
}
// ============================================
// Query DTOs (internal use)
// ============================================
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionDetailQueryDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String,
pub duration_minutes: i32,
pub meeting_link: Option<String>,
pub session_type: String,
pub status: String,
pub feedback: Option<String>,
pub rating: Option<i32>,
pub feedback_submitted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
pub mentor_fullname: Option<String>,
pub mentee_fullname: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionListQueryDto {
pub id: String,
pub mentor_id: String,
pub mentee_id: String,
pub topic: String,
pub scheduled_at: String,
pub duration_minutes: i32,
pub session_type: String,
pub status: String,
pub rating: Option<i32>,
pub created_at: String,
pub mentee_fullname: Option<String>,
pub mentee_email: Option<String>,
}
@@ -0,0 +1,365 @@
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
use imphnen_libs::AppState;
use imphnen_utils::get_id;
use serde::Deserialize;
use surrealdb::sql::Thing;
pub struct SessionsRepository<'a> {
pub state: &'a AppState,
}
impl<'a> SessionsRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
// ============================================
// Create Session
// ============================================
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema, String> {
let db = &self.state.surrealdb_ws;
let created: Option<SessionSchema> = db
.create("sessions")
.content(schema)
.await
.map_err(|e| format!("Failed to create session: {}", e))?;
created.ok_or_else(|| "Session creation returned None".to_string())
}
// ============================================
// Get Session by ID
// ============================================
pub async fn query_session_by_id(&self, id: &Thing) -> Result<Option<SessionSchema>, String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let session: Option<SessionSchema> = db
.select(record_key)
.await
.map_err(|e| format!("Failed to fetch session: {}", e))?;
Ok(session)
}
// ============================================
// Get Session Detail with User Info
// ============================================
pub async fn query_session_detail(&self, id: &Thing) -> Result<Option<SessionDetailQueryDto>, String> {
let db = &self.state.surrealdb_ws;
let query = r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
description,
scheduled_at,
duration_minutes,
meeting_link,
session_type,
status,
feedback,
rating,
feedback_submitted_at,
created_at,
updated_at,
(SELECT fullname FROM $parent.mentor_id.user_id)[0].fullname AS mentor_fullname,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname
FROM type::thing($table, $id)
"#;
let mut result = db
.query(query)
.bind(("table", "sessions"))
.bind(("id", id.id.to_string()))
.await
.map_err(|e| format!("Failed to query session detail: {}", e))?;
let session: Option<SessionDetailQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse session detail: {}", e))?;
Ok(session)
}
// ============================================
// List Mentor's Sessions
// ============================================
pub async fn query_mentor_sessions(
&self,
mentor_id: &Thing,
status_filter: Option<String>,
) -> Result<Vec<SessionListQueryDto>, String> {
let query = if let Some(_status) = status_filter.as_ref() {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentor_id = $mentor_id AND status = $status
ORDER BY scheduled_at DESC
"#
} else {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentor_id = $mentor_id
ORDER BY scheduled_at DESC
"#
};
let db = &self.state.surrealdb_ws;
let mentor_id_clone = mentor_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.await
}
.map_err(|e| format!("Failed to query mentor sessions: {}", e))?;
let sessions: Vec<SessionListQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse mentor sessions: {}", e))?;
Ok(sessions)
}
// ============================================
// List User's Sessions (as mentee)
// ============================================
pub async fn query_user_sessions(
&self,
user_id: &Thing,
status_filter: Option<String>,
) -> Result<Vec<SessionListQueryDto>, String> {
let query = if let Some(_status) = status_filter.as_ref() {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentee_id = $user_id AND status = $status
ORDER BY scheduled_at DESC
"#
} else {
r#"
SELECT
id,
mentor_id,
mentee_id,
topic,
scheduled_at,
duration_minutes,
session_type,
status,
rating,
created_at,
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
FROM sessions
WHERE mentee_id = $user_id
ORDER BY scheduled_at DESC
"#
};
let db = &self.state.surrealdb_ws;
let user_id_clone = user_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("user_id", user_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("user_id", user_id_clone))
.await
}
.map_err(|e| format!("Failed to query user sessions: {}", e))?;
let sessions: Vec<SessionListQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse user sessions: {}", e))?;
Ok(sessions)
}
// ============================================
// Get Booked Dates for Mentor
// ============================================
pub async fn query_booked_dates(&self, mentor_id: &Thing) -> Result<Vec<String>, String> {
let query = r#"
SELECT scheduled_at FROM sessions
WHERE mentor_id = $mentor_id
AND status IN ['pending', 'confirmed']
ORDER BY scheduled_at ASC
"#;
let db = &self.state.surrealdb_ws;
let mentor_id_clone = mentor_id.clone();
let mut result = db
.query(query)
.bind(("mentor_id", mentor_id_clone))
.await
.map_err(|e| format!("Failed to query booked dates: {}", e))?;
#[derive(Deserialize)]
struct DateOnly {
scheduled_at: String,
}
let dates: Vec<DateOnly> = result
.take(0)
.map_err(|e| format!("Failed to parse booked dates: {}", e))?;
Ok(dates.into_iter().map(|d| d.scheduled_at).collect())
}
// ============================================
// Update Session
// ============================================
pub async fn update_session(&self, id: &Thing, schema: SessionSchema) -> Result<SessionSchema, String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let updated: Option<SessionSchema> = db
.update(record_key)
.content(schema)
.await
.map_err(|e| format!("Failed to update session: {}", e))?;
updated.ok_or_else(|| "Session update returned None".to_string())
}
// ============================================
// Count Mentor Sessions
// ============================================
pub async fn count_mentor_sessions(
&self,
mentor_id: &Thing,
status_filter: Option<String>,
) -> Result<usize, String> {
let query = if status_filter.is_some() {
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id AND status = $status GROUP ALL"
} else {
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id GROUP ALL"
};
let db = &self.state.surrealdb_ws;
let mentor_id_clone = mentor_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("mentor_id", mentor_id_clone))
.await
}
.map_err(|e| format!("Failed to count mentor sessions: {}", e))?;
#[derive(serde::Deserialize)]
struct CountResult {
count: usize,
}
let count_result: Option<CountResult> = result
.take(0)
.map_err(|e| format!("Failed to parse count: {}", e))?;
Ok(count_result.map(|r| r.count).unwrap_or(0))
}
// ============================================
// Count User Sessions
// ============================================
pub async fn count_user_sessions(
&self,
user_id: &Thing,
status_filter: Option<String>,
) -> Result<usize, String> {
let query = if status_filter.is_some() {
"SELECT count() FROM sessions WHERE mentee_id = $user_id AND status = $status GROUP ALL"
} else {
"SELECT count() FROM sessions WHERE mentee_id = $user_id GROUP ALL"
};
let db = &self.state.surrealdb_ws;
let user_id_clone = user_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("user_id", user_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("user_id", user_id_clone))
.await
}
.map_err(|e| format!("Failed to count user sessions: {}", e))?;
#[derive(serde::Deserialize)]
struct CountResult {
count: usize,
}
let count_result: Option<CountResult> = result
.take(0)
.map_err(|e| format!("Failed to parse count: {}", e))?;
Ok(count_result.map(|r| r.count).unwrap_or(0))
}
// ============================================
// Delete Session (soft delete)
// ============================================
// Delete Session (soft delete)
// ============================================
pub async fn delete_session(&self, id: &Thing) -> Result<(), String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let _: Option<SessionSchema> = db
.delete(record_key)
.await
.map_err(|e| format!("Failed to delete session: {}", e))?;
Ok(())
}
}
@@ -0,0 +1,89 @@
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
use imphnen_libs::ResourceEnum;
use imphnen_utils::{get_iso_date, make_thing};
use serde::{Deserialize, Serialize};
use surrealdb::{sql::Thing, Uuid};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionSchema {
pub id: Thing,
pub mentor_id: Thing,
pub mentee_id: Thing,
pub topic: String,
pub description: Option<String>,
pub scheduled_at: String, // ISO 8601 datetime
pub duration_minutes: i32,
pub meeting_link: Option<String>,
pub session_type: String, // "video_call", "phone_call", "chat"
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
pub feedback: Option<String>,
pub rating: Option<i32>, // 1-5
pub feedback_submitted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl Default for SessionSchema {
fn default() -> Self {
Self {
id: make_thing(
ResourceEnum::Sessions.to_string().as_str(),
&Uuid::new_v4().to_string(),
),
mentor_id: make_thing(
ResourceEnum::Mentors.to_string().as_str(),
&Uuid::new_v4().to_string(),
),
mentee_id: make_thing(
ResourceEnum::Users.to_string().as_str(),
&Uuid::new_v4().to_string(),
),
topic: String::new(),
description: None,
scheduled_at: get_iso_date(),
duration_minutes: 60,
meeting_link: None,
session_type: "video_call".to_string(),
status: "pending".to_string(),
feedback: None,
rating: None,
feedback_submitted_at: None,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
}
impl SessionSchema {
pub fn from_book_request(
mentor_id: Thing,
mentee_id: Thing,
request: BookSessionRequestDto,
) -> Self {
Self {
mentor_id,
mentee_id,
topic: request.topic,
description: request.description,
scheduled_at: request.scheduled_at,
duration_minutes: request.duration_minutes.unwrap_or(60),
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
..Default::default()
}
}
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
self.status = request.status;
if let Some(link) = request.meeting_link {
self.meeting_link = Some(link);
}
self.updated_at = get_iso_date();
}
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
self.feedback = Some(request.feedback);
self.rating = Some(request.rating);
self.feedback_submitted_at = Some(get_iso_date());
self.updated_at = get_iso_date();
}
}
@@ -0,0 +1,288 @@
use super::{
AvailabilitySlotDto, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
SessionListResponseDto, SessionSchema, SessionsRepository, UpdateSessionStatusRequestDto,
UpdateSessionStatusResponseDto,
};
use axum::{http::StatusCode, response::Response};
use chrono::{Duration, Utc};
use imphnen_entities::ResponseSuccessDto;
use imphnen_libs::AppState;
use imphnen_utils::{common_response, extract_id, get_iso_date, make_thing, success_response, validate_request};
pub struct SessionsService;
impl SessionsService {
// ============================================
// Book Session
// ============================================
pub async fn book_session(
state: &AppState,
mentor_id: String,
user_id: String,
dto: BookSessionRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&dto) {
return common_response(status, &message);
}
let mentor_thing = make_thing("mentors", &mentor_id);
let mentee_thing = make_thing("users", &user_id);
let schema = SessionSchema::from_book_request(mentor_thing.clone(), mentee_thing.clone(), dto);
let repo = SessionsRepository::new(state);
match repo.create_session(schema).await {
Ok(created) => {
let response = BookSessionResponseDto {
id: extract_id(&created.id),
mentor_id: extract_id(&created.mentor_id),
mentee_id: extract_id(&created.mentee_id),
topic: created.topic,
description: created.description,
scheduled_at: created.scheduled_at,
duration_minutes: created.duration_minutes,
session_type: created.session_type,
status: created.status,
created_at: created.created_at,
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
}
}
// ============================================
// Get Mentor's Sessions
// ============================================
pub async fn get_mentor_sessions(
state: &AppState,
mentor_id: String,
_user_email: String,
status_filter: Option<String>,
) -> Response {
let mentor_thing = make_thing("mentors", &mentor_id);
let repo = SessionsRepository::new(state);
// Get count and sessions
let count = match repo.count_mentor_sessions(&mentor_thing, status_filter.clone()).await {
Ok(c) => c,
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
};
match repo.query_mentor_sessions(&mentor_thing, status_filter).await {
Ok(sessions) => {
let session_items: Vec<SessionListItemDto> = sessions
.into_iter()
.map(|s| SessionListItemDto {
id: s.id,
mentor_id: s.mentor_id,
mentee_id: s.mentee_id,
mentee_fullname: s.mentee_fullname,
mentee_email: s.mentee_email,
topic: s.topic,
scheduled_at: s.scheduled_at,
duration_minutes: s.duration_minutes,
session_type: s.session_type,
status: s.status,
rating: s.rating,
created_at: s.created_at,
})
.collect();
let response = SessionListResponseDto {
sessions: session_items,
total: count,
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
}
}
// ============================================
// Get User's Sessions (as mentee)
// ============================================
pub async fn get_user_sessions(
state: &AppState,
user_id: String,
status_filter: Option<String>,
) -> Response {
let user_thing = make_thing("users", &user_id);
let repo = SessionsRepository::new(state);
// Get count and sessions
let count = match repo.count_user_sessions(&user_thing, status_filter.clone()).await {
Ok(c) => c,
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
};
match repo.query_user_sessions(&user_thing, status_filter).await {
Ok(sessions) => {
let session_items: Vec<SessionListItemDto> = sessions
.into_iter()
.map(|s| SessionListItemDto {
id: s.id,
mentor_id: s.mentor_id,
mentee_id: s.mentee_id,
mentee_fullname: s.mentee_fullname,
mentee_email: s.mentee_email,
topic: s.topic,
scheduled_at: s.scheduled_at,
duration_minutes: s.duration_minutes,
session_type: s.session_type,
status: s.status,
rating: s.rating,
created_at: s.created_at,
})
.collect();
let response = SessionListResponseDto {
sessions: session_items,
total: count,
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
}
}
// ============================================
// Get Mentor Availability
// ============================================
pub async fn get_mentor_availability(state: &AppState, mentor_id: String) -> Response {
let mentor_thing = make_thing("mentors", &mentor_id);
let repo = SessionsRepository::new(state);
match repo.query_booked_dates(&mentor_thing).await {
Ok(booked_dates) => {
// Generate sample availability slots (next 7 days)
let mut slots = Vec::new();
let today = Utc::now().date_naive();
for i in 0..7 {
let date = today + Duration::days(i);
let date_str = date.format("%Y-%m-%d").to_string();
// Generate time slots (9 AM to 5 PM, every hour)
for hour in 9..17 {
let time_str = format!("{:02}:00", hour);
let datetime_str = format!("{}T{}:00Z", date_str, time_str);
// Check if this slot is booked
let is_booked = booked_dates.iter().any(|d| d.starts_with(&datetime_str[..13]));
slots.push(AvailabilitySlotDto {
date: date_str.clone(),
time: time_str,
available: !is_booked,
});
}
}
let response = MentorAvailabilityDto {
mentor_id,
availability_commitment: "Available weekdays 9 AM - 5 PM".to_string(),
preferred_formats: vec!["video_call".to_string(), "phone_call".to_string()],
slots,
booked_dates,
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::NOT_FOUND, &e),
}
}
// ============================================
// Update Session Status
// ============================================
pub async fn update_session_status(
state: &AppState,
session_id: String,
_user_id: String,
dto: UpdateSessionStatusRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&dto) {
return common_response(status, &message);
}
let session_thing = make_thing("sessions", &session_id);
let repo = SessionsRepository::new(state);
match repo.query_session_by_id(&session_thing).await {
Ok(Some(mut session)) => {
session.update_status(dto.clone());
match repo.update_session(&session_thing, session).await {
Ok(updated) => {
let response = UpdateSessionStatusResponseDto {
id: extract_id(&updated.id),
status: updated.status,
meeting_link: updated.meeting_link,
updated_at: updated.updated_at,
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
}
}
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
}
}
// ============================================
// Submit Feedback
// ============================================
pub async fn submit_feedback(
state: &AppState,
session_id: String,
user_id: String,
dto: SessionFeedbackRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&dto) {
return common_response(status, &message);
}
let session_thing = make_thing("sessions", &session_id);
let repo = SessionsRepository::new(state);
match repo.query_session_by_id(&session_thing).await {
Ok(Some(mut session)) => {
// Authorization: Only mentee can submit feedback
let mentee_id = extract_id(&session.mentee_id);
if mentee_id != user_id {
return common_response(
StatusCode::FORBIDDEN,
"Unauthorized: Only the mentee can submit feedback",
);
}
// Validate session is completed
if session.status != "completed" {
return common_response(
StatusCode::BAD_REQUEST,
"Feedback can only be submitted for completed sessions",
);
}
session.add_feedback(dto.clone());
match repo.update_session(&session_thing, session).await {
Ok(updated) => {
let response = SessionFeedbackResponseDto {
id: extract_id(&updated.id),
feedback: dto.feedback,
rating: dto.rating,
submitted_at: updated.feedback_submitted_at.unwrap_or_else(get_iso_date),
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
}
}
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
}
}
}
+13
View File
@@ -1 +1,14 @@
/// Version 2 of the Dimentorin API - currently under development
/// This module will contain all version 2 endpoints following API versioning best practices
use axum::Router;
/// Placeholder for version 2 router
/// To be implemented when version 2 endpoints are ready
pub fn dimentorin_v2_router() -> Router {
Router::new()
// Version 2 endpoints will be added here following the same pattern as v1
}
// Re-export the v1 router for backward compatibility
pub use crate::v1::dimentorin_router;
+5
View File
@@ -6,7 +6,12 @@ edition = "2024"
[dependencies] [dependencies]
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true
utoipa.workspace = true utoipa.workspace = true
surrealdb.workspace = true surrealdb.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
uuid.workspace = true
strum.workspace = true
strum_macros.workspace = true
chrono.workspace = true
+88
View File
@@ -0,0 +1,88 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
/// Schema untuk audit log yang mencatat semua aksi admin
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AuditLogSchema {
/// ID unik dari log
pub id: Option<Thing>,
/// ID pengguna yang melakukan aksi
pub user_id: String,
/// Email pengguna
pub user_email: String,
/// Tipe aksi yang dilakukan (CREATE, UPDATE, DELETE, etc.)
pub action: String,
/// Resource yang terkena aksi
pub resource: String,
/// ID resource yang terkena aksi
pub resource_id: Option<String>,
/// Data sebelum perubahan (untuk UPDATE/DELETE)
pub old_data: Option<serde_json::Value>,
/// Data setelah perubahan (untuk CREATE/UPDATE)
pub new_data: Option<serde_json::Value>,
/// IP address pengguna
pub ip_address: String,
/// User agent pengguna
pub user_agent: Option<String>,
/// Timestamp ketika aksi dilakukan
pub timestamp: DateTime<Utc>,
}
/// Schema untuk rate limiting menggunakan SurrealDB memori
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RateLimitSchema {
/// ID unik (IP address)
pub id: Option<Thing>,
/// IP address klien
pub ip_address: String,
/// Jumlah request dalam window saat ini
pub request_count: u32,
/// Timestamp pertama request dalam window
pub first_request_time: DateTime<Utc>,
/// Timestamp terakhir request
pub last_request_time: DateTime<Utc>,
/// Window duration dalam detik
pub window_duration_secs: u64,
}
impl RateLimitSchema {
/// Buat instance baru RateLimitSchema
pub fn new(ip_address: String, window_duration_secs: u64) -> Self {
let now = Utc::now();
Self {
id: None,
ip_address,
request_count: 1,
first_request_time: now,
last_request_time: now,
window_duration_secs,
}
}
/// Periksa apakah rate limit sudah terlampaui
pub fn is_rate_limited(&self, max_requests: u32) -> bool {
self.request_count > max_requests
}
/// Perbarui counter dan timestamp
pub fn increment(&mut self) {
self.request_count += 1;
self.last_request_time = Utc::now();
}
/// Reset counter jika window sudah expired
pub fn reset_if_expired(&mut self) -> bool {
let now = Utc::now();
let duration = now - self.first_request_time;
if duration.num_seconds() >= self.window_duration_secs as i64 {
self.request_count = 1;
self.first_request_time = now;
self.last_request_time = now;
true
} else {
false
}
}
}

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