Compare commits
124
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c2fce7be | ||
|
|
98c46611fb | ||
|
|
fbfe3dcd51 | ||
|
|
5cc5a3dfe1 | ||
|
|
95ae55c9df | ||
|
|
f7ca67d720 | ||
|
|
02421e9bc2 | ||
|
|
5e2b0d3caf | ||
|
|
b9a51ce6cc | ||
|
|
d4a6c4c9ea | ||
|
|
ece6499e2b | ||
|
|
1caaa8404b | ||
|
|
cb6eef2054 | ||
|
|
e77797d7bb | ||
|
|
d5ccf4cf75 | ||
|
|
1a36698962 | ||
|
|
15a2ee1950 | ||
|
|
bef8482402 | ||
|
|
768e583f81 | ||
|
|
1ca6d8f47c | ||
|
|
3fcfb3709e | ||
|
|
6915a97d79 | ||
|
|
6f596efadd | ||
|
|
b6b5f48055 | ||
|
|
789b20c278 | ||
|
|
884b679ba8 | ||
|
|
6ef624c169 | ||
|
|
c10443f881 | ||
|
|
466ba3391a | ||
|
|
8c2527d61e | ||
|
|
018124f7b3 | ||
|
|
7749f6fdec | ||
|
|
b27e4a4404 | ||
|
|
3dde67a4ac | ||
|
|
49abcf28c3 | ||
|
|
9cecc8fa05 | ||
|
|
a32f54873b | ||
|
|
af41a98111 | ||
|
|
4c435708c0 | ||
|
|
ac462ea771 | ||
|
|
3e52223730 | ||
|
|
01e479350b | ||
|
|
b0cf9817d7 | ||
|
|
e530eba60d | ||
|
|
ef1d63e893 | ||
|
|
5859af5294 | ||
|
|
c12da948aa | ||
|
|
3cde18360c | ||
|
|
14b22328de | ||
|
|
96debc210a | ||
|
|
b80662188f | ||
|
|
7b9ce6cda1 | ||
|
|
1bb2d6a419 | ||
|
|
14690a378f | ||
|
|
17cc4ccdba | ||
|
|
1b7368a4b7 | ||
|
|
ff2e14d5f9 | ||
|
|
012aff60ef | ||
|
|
f3d88e2ac9 | ||
|
|
0e48b6755f | ||
|
|
7056f8c8a6 | ||
|
|
4ecdcd12d2 | ||
|
|
dd4a73e360 | ||
|
|
49e0a89ad1 | ||
|
|
82b85abe8f | ||
|
|
df11aa5eb8 | ||
|
|
ac44867ec1 | ||
|
|
7205afe43b | ||
|
|
b1c678c72b | ||
|
|
fa3687c6be | ||
|
|
ba46a19a5a | ||
|
|
8cdbca24ba | ||
|
|
8f981572cd | ||
|
|
39b75f410f | ||
|
|
6da1b2a7e7 | ||
|
|
acccae9f7c | ||
|
|
e7c17784cc | ||
|
|
042e0b4eff | ||
|
|
0b51a56dee | ||
|
|
cb98440b65 | ||
|
|
3c19d262f3 | ||
|
|
bdffe85544 | ||
|
|
9ed619e3fd | ||
|
|
00fd7e1907 | ||
|
|
b70126734e | ||
|
|
6af9461079 | ||
|
|
57b3662951 | ||
|
|
c0c08f2821 | ||
|
|
8402415649 | ||
|
|
01e20baa45 | ||
|
|
10dc869eaf | ||
|
|
199d0c885e | ||
|
|
89ef48a57e | ||
|
|
da1826ebba | ||
|
|
874986f1d6 | ||
|
|
bbff555a06 | ||
|
|
d3eb687a58 | ||
|
|
c5516d4006 | ||
|
|
5f51dd281d | ||
|
|
f6232832af | ||
|
|
277cf0770d | ||
|
|
650728a385 | ||
|
|
3cc3e2a7d8 | ||
|
|
39fc2f86ad | ||
|
|
7bd0ef2274 | ||
|
|
b40a430c49 | ||
|
|
44b1e09551 | ||
|
|
1b9251b2dd | ||
|
|
34b6cdabfb | ||
|
|
a25e896fa2 | ||
|
|
adf3ef8e28 | ||
|
|
ad46687f07 | ||
|
|
0ba04abee9 | ||
|
|
1e25a5b496 | ||
|
|
d7178c792d | ||
|
|
d2754de9ef | ||
|
|
cda950ed7e | ||
|
|
c6a95c1231 | ||
|
|
86116b8157 | ||
|
|
ac06224ed1 | ||
|
|
83e765f358 | ||
|
|
558e0ce568 | ||
|
|
148cf3a5cd | ||
|
|
ee9d23e01b |
+32
-7
@@ -1,4 +1,30 @@
|
||||
# RUST_ENV : Mode environment aplikasi (misal: development, production)
|
||||
# RUST_LOG : Level logging aplikasi Rust (misal: debug, info, warn, error)
|
||||
# PORT : Port yang digunakan backend service
|
||||
# SURREALDB_URL : URL koneksi ke SurrealDB (gunakan ws:// untuk WebSocket)
|
||||
# SURREALDB_USERNAME : Username untuk SurrealDB
|
||||
# SURREALDB_PASSWORD : Password untuk SurrealDB
|
||||
# SURREALDB_NAMESPACE : Namespace SurrealDB yang digunakan
|
||||
# SURREALDB_DBNAME : Nama database SurrealDB
|
||||
# ACCESS_TOKEN_SECRET : Secret key untuk JWT access token (ganti dengan nilai rahasia)
|
||||
# REFRESH_TOKEN_SECRET : Secret key untuk JWT refresh token (ganti dengan nilai rahasia)
|
||||
# SMTP_EMAIL : Email yang digunakan untuk mengirim email (SMTP)
|
||||
# SMTP_PASSWORD : Password email SMTP
|
||||
# SMTP_NAME : Nama pengirim email (misal: nama aplikasi)
|
||||
# SMTP_HOST : Host SMTP server (misal: smtp.gmail.com)
|
||||
# REDISDB_URL : URL/host Redis database
|
||||
# FE_URL : URL frontend aplikasi (misal: http://localhost)
|
||||
# MINIO_ENDPOINT : Endpoint MinIO object storage
|
||||
# MINIO_BUCKET_NAME : Nama bucket default MinIO
|
||||
# MINIO_ACCESS_KEY : Access key MinIO atau MINIO_ROOT_USER
|
||||
# MINIO_SECRET_KEY : Secret key MinIO atau MINIO_ROOT_PASSWORD
|
||||
# MINIO_ROOT_USER : Username root MinIO (default: minioadmin)
|
||||
# MINIO_ROOT_PASSWORD : Password root MinIO (default: minioadmin)
|
||||
# GOOGLE_CLIENT_ID : Client ID OAuth Google (untuk login Google)
|
||||
# GOOGLE_CLIENT_SECRET : Client Secret OAuth Google
|
||||
|
||||
RUST_ENV=development
|
||||
RUST_LOG=debug
|
||||
PORT=4099
|
||||
SURREALDB_URL=ws://localhost:8000/rpc
|
||||
SURREALDB_USERNAME=root
|
||||
@@ -15,10 +41,9 @@ REDISDB_URL=localhost
|
||||
FE_URL=http://localhost
|
||||
MINIO_ENDPOINT=http://localhost:9000
|
||||
MINIO_BUCKET_NAME=default_bucket
|
||||
MINIO_ACCESS_KEY=your-minio-access-key
|
||||
MINIO_SECRET_KEY=your-minio-secret-key
|
||||
MAIL_USER=your-email@example.com
|
||||
MAIL_PASSWORD=your-smtp-password
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=465
|
||||
MAIL_SECURE=true
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_SECURE=false
|
||||
|
||||
GOOGLE_CLIENT_ID="your_google_client_id"
|
||||
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
||||
@@ -13,3 +13,4 @@
|
||||
.env.development
|
||||
.env.staging
|
||||
.env.production
|
||||
**/**.log
|
||||
@@ -0,0 +1 @@
|
||||
/cache
|
||||
@@ -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
+575
-219
File diff suppressed because it is too large
Load Diff
+51
-37
@@ -2,61 +2,74 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"tests",
|
||||
"imphnen-iam",
|
||||
"imphnen-cms",
|
||||
"imphnen-libs",
|
||||
"imphnen-utils",
|
||||
"imphnen-gacha",
|
||||
"imphnen-gateway",
|
||||
"imphnen-backend",
|
||||
"imphnen-entities",
|
||||
"imphnen-dimentorin",
|
||||
"imphnen-middleware",
|
||||
"imphnen-entities", # Most basic - core data structures
|
||||
"imphnen-libs", # Depends on entities
|
||||
"imphnen-utils", # Depends on libs and entities
|
||||
"imphnen-middleware",# Utility for permissions
|
||||
"imphnen-iam", # Core auth service, depends on libs, utils, entities
|
||||
"imphnen-cms", # Content management, depends on core services
|
||||
"imphnen-gacha", # Game mechanics, depends on core services
|
||||
"imphnen-dimentorin",# Learning platform, depends on core services
|
||||
"imphnen-hackathon", # Hackathon service, depends on core services
|
||||
"imphnen-gateway", # API gateway, depends on all services
|
||||
"imphnen-backend", # Main application, depends on all services
|
||||
]
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.83"
|
||||
oauth2 = "5.0.0"
|
||||
reqwest = { version = "0.12.23", features = ["json"] }
|
||||
serde_json = "1.0.142"
|
||||
axum = { version = "0.8.4", features = ["multipart"] }
|
||||
log = "0.4.25"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.138"
|
||||
tokio = { version = "1.45.0", features = ["full"] }
|
||||
log = "0.4.27"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||
jsonwebtoken = "9.3.1"
|
||||
chrono = "0.4.41"
|
||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
||||
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] }
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
||||
thiserror = "2.0.12"
|
||||
anyhow = "1.0.98"
|
||||
rand = { version = "0.9.1", features = ["std", "alloc"] }
|
||||
thiserror = "2.0.14"
|
||||
anyhow = "1.0.99"
|
||||
rand = { version = "0.9.2", features = ["std", "alloc"] }
|
||||
rand_distr = "0.5.1"
|
||||
tower-http = { version = "0.6.4", features = ["cors", "trace"] }
|
||||
http-body-util = "0.1.1"
|
||||
|
||||
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
|
||||
http-body-util = "0.1.3"
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
lazy_static = "1.4.0"
|
||||
lazy_static = "1.5.0"
|
||||
regex = "1.11.1"
|
||||
axum-test = "17.2.0"
|
||||
fancy-regex = "0.15.0"
|
||||
axum-test = "17.3.0"
|
||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
fancy-regex = "0.16.1"
|
||||
futures = "0.3.31"
|
||||
tower = "0.5.2"
|
||||
env_logger = "0.11.8"
|
||||
dotenvy = "0.15.7"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
once_cell = "1.21.3"
|
||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] }
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
strum_macros = "0.27.1"
|
||||
tracing = "0.1.41"
|
||||
uuid = { version = "1.18.0", features = ["v4", "fast-rng", "serde"] }
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
strum_macros = "0.27.2"
|
||||
base64 = "0.22.1"
|
||||
sha2 = "0.10.9"
|
||||
hmac = "0.12"
|
||||
hex = "0.4"
|
||||
urlencoding = "2.1"
|
||||
hyper = "1.6.0"
|
||||
hyper-util = "0.1.0"
|
||||
async-trait = "0.1.75"
|
||||
tokio-tungstenite = "0.23"
|
||||
hyper-util = "0.1.16"
|
||||
minio = "0.3.0"
|
||||
|
||||
|
||||
tokio-test = "0.4.4"
|
||||
mockall = "0.13.1"
|
||||
once_cell = "1.21.3"
|
||||
dotenvy = "0.15.7"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tokio-tungstenite = "0.27"
|
||||
url = "2.5"
|
||||
futures-util = "0.3"
|
||||
|
||||
http = "1.3"
|
||||
tests = { path = "./tests" }
|
||||
imphnen-iam = { path = "./imphnen-iam" }
|
||||
imphnen-cms = { path = "./imphnen-cms" }
|
||||
@@ -67,6 +80,7 @@ imphnen-gateway = { path = "./imphnen-gateway" }
|
||||
imphnen-backend = { path = "./imphnen-backend" }
|
||||
imphnen-entities = { path = "./imphnen-entities" }
|
||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Script deploy semua binary hasil build Rust (untuk Linux)
|
||||
|
||||
set -e
|
||||
|
||||
REMOTE_USER="asephs"
|
||||
REMOTE_HOST="70.153.9.42"
|
||||
REMOTE_PATH="/home/asephs/imphnen-backend-service"
|
||||
|
||||
# Build release binary
|
||||
echo "🔧 Building Rust project..."
|
||||
cargo build --release
|
||||
|
||||
# Filter hanya file executable tanpa ekstensi .exe, .rlib, atau .d
|
||||
BINARIES=$(find target/release -maxdepth 1 -type f ! -name "*.exe" ! -name "*.rlib" ! -name "*.d")
|
||||
|
||||
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/
|
||||
|
||||
# Jalankan ulang service utama di VPS
|
||||
echo "♻️ Restart service utama di server..."
|
||||
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||
set -e
|
||||
cd /home/asephs/imphnen-backend-service
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
if [ -f ~/.bashrc ]; then
|
||||
source ~/.bashrc
|
||||
fi
|
||||
|
||||
# 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
|
||||
|
||||
echo "✅ Deploy semua binary Linux selesai ke $REMOTE_HOST:$REMOTE_PATH"
|
||||
@@ -14,3 +14,19 @@ services:
|
||||
command: start --log trace --user root --pass root
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: minio
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
||||
volumes:
|
||||
minio_data:
|
||||
|
||||
@@ -3,6 +3,54 @@ name = "imphnen-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "api"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "clear_db_test"
|
||||
path = "src/bin/clear_db_test.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seeder"
|
||||
path = "src/bin/seeder.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_events"
|
||||
path = "src/bin/seed_events.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_gacha_rolls"
|
||||
path = "src/bin/seed_gacha_rolls.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_mentor_user"
|
||||
path = "src/bin/seed_mentor_user.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_permissions"
|
||||
path = "src/bin/seed_permissions.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_roles"
|
||||
path = "src/bin/seed_roles.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_roles_permissions"
|
||||
path = "src/bin/seed_roles_permissions.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_teams"
|
||||
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]
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
@@ -10,7 +58,9 @@ imphnen-gateway.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-gacha.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
imphnen-hackathon.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
|
||||
|
||||
let (ws_stream, _) = connect_async(url).await?;
|
||||
let (ws_stream, _) = connect_async(url.as_str()).await?;
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Authenticate (signin)
|
||||
@@ -43,7 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"id": 1
|
||||
}).to_string();
|
||||
println!("DEBUG: Sending signin query: {}", signin_query);
|
||||
write.send(Message::Text(signin_query)).await?;
|
||||
write.send(Message::Text(signin_query.into())).await?;
|
||||
|
||||
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
|
||||
let signin_response_msg = signin_response?;
|
||||
@@ -60,7 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"id": 2
|
||||
}).to_string();
|
||||
println!("DEBUG: Sending use query: {}", use_query);
|
||||
write.send(Message::Text(use_query)).await?;
|
||||
write.send(Message::Text(use_query.into())).await?;
|
||||
|
||||
let use_response = read.next().await.ok_or("Failed to read use response")?;
|
||||
let use_response_msg = use_response?;
|
||||
@@ -82,7 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}).to_string();
|
||||
|
||||
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
|
||||
write.send(Message::Text(query_json)).await?;
|
||||
write.send(Message::Text(query_json.into())).await?;
|
||||
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}).to_string();
|
||||
|
||||
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
|
||||
write.send(Message::Text(delete_all_json)).await?;
|
||||
write.send(Message::Text(delete_all_json.into())).await?;
|
||||
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
|
||||
|
||||
match delete_response_result {
|
||||
@@ -137,7 +137,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"params": [select_query],
|
||||
"id": i + 1000
|
||||
}).to_string();
|
||||
write.send(Message::Text(select_json)).await?;
|
||||
write.send(Message::Text(select_json.into())).await?;
|
||||
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
|
||||
match select_response_result {
|
||||
Ok(select_msg) => {
|
||||
|
||||
@@ -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,21 +1,20 @@
|
||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use imphnen_utils::{get_iso_date};
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
let events = vec![
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
|
||||
use imphnen_utils::{get_iso_date};
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
@@ -16,18 +13,17 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
db.query("DELETE type::thing('app_gacha_items', $id)")
|
||||
.bind(("id", "gacha_item_test_id"))
|
||||
.bind(("id", "1"))
|
||||
.await?;
|
||||
db.query("DELETE type::thing('app_gacha_rolls', $id)")
|
||||
.bind(("id", "gacha_roll_test_id"))
|
||||
.bind(("id", "test-gacha-roll-001"))
|
||||
.await?;
|
||||
|
||||
let gacha_item_id = "gacha_item_test_id";
|
||||
let gacha_item_id = "1";
|
||||
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", gacha_item_id))
|
||||
.bind(("name", "Test Gacha Item"))
|
||||
@@ -38,7 +34,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
.await?;
|
||||
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")
|
||||
.bind(("id", gacha_roll_id))
|
||||
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,5 @@
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use imphnen_utils::{get_iso_date};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
@@ -8,17 +7,15 @@ use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
|
||||
let env = Env::new();
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
for permission in [
|
||||
@@ -54,8 +51,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::UpdateOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
PermissionsEnum::UpdateMentors,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
PermissionsEnum::Administrator,
|
||||
] {
|
||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||
.bind(("id", permission.id()))
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use imphnen_utils::{get_iso_date};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
let roles = vec![
|
||||
@@ -28,7 +26,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
(
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
"User",
|
||||
None,
|
||||
Some("2025-02-28T14:53:58.576688+00"),
|
||||
Some("2025-02-28T14:53:58.576688+00"),
|
||||
),
|
||||
(
|
||||
@@ -46,13 +44,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||
"Admin",
|
||||
None,
|
||||
Some("2025-02-22T15:38:39.868306+00"),
|
||||
Some("2025-02-22T15:38:39.868306+00"),
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
"Mentor",
|
||||
None,
|
||||
Some("2025-07-06T10:00:00.000000+00"),
|
||||
Some("2025-07-06T10:00:00.000000+00"),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,64 +1,41 @@
|
||||
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum};
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_iam::{get_iso_date, make_thing, PermissionsEnum};
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
db.query("DEFINE INDEX user_email_index ON TABLE users COLUMNS email UNIQUE;")
|
||||
|
||||
.await?;
|
||||
db.query("DEFINE INDEX role_name_idx ON TABLE roles COLUMNS name UNIQUE;")
|
||||
|
||||
.await?;
|
||||
|
||||
println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
|
||||
|
||||
let roles_permissions = vec![
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::CreateUsers,
|
||||
PermissionsEnum::DeleteUsers,
|
||||
PermissionsEnum::UpdateUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::CreateRoles,
|
||||
PermissionsEnum::DeleteRoles,
|
||||
PermissionsEnum::UpdateRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::CreatePermissions,
|
||||
PermissionsEnum::DeletePermissions,
|
||||
PermissionsEnum::UpdatePermissions,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::CreateGachaItems,
|
||||
PermissionsEnum::DeleteGachaItems,
|
||||
PermissionsEnum::UpdateGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::RegisterMentors,
|
||||
PermissionsEnum::UpdateMentors,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
// Only Administrator permission - grants access to everything
|
||||
PermissionsEnum::Administrator,
|
||||
],
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::UpdateOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
@@ -76,6 +53,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
vec![
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
@@ -91,13 +70,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
(
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||
vec![
|
||||
// Staff should be able to list roles and permissions in tests
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
@@ -106,6 +86,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ManageAllTeams,
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -118,7 +99,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
for (role_id, permissions) in roles_permissions {
|
||||
let permission_refs: Vec<_> = permissions
|
||||
.iter()
|
||||
.map(|perm| make_thing("app_permissions", perm.id()))
|
||||
.map(|perm| make_thing("app_permissions", &perm.id()))
|
||||
.collect();
|
||||
|
||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
use imphnen_iam::UsersSchema;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||
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>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
@@ -15,8 +13,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
let users = vec![
|
||||
@@ -38,6 +36,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"User",
|
||||
"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 {
|
||||
@@ -48,15 +64,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
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: None,
|
||||
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: None,
|
||||
birthdate: None,
|
||||
gender: Some("male".into()),
|
||||
birthdate: Some("1990-05-15".into()),
|
||||
domicile: Some("Jakarta, Indonesia".into()),
|
||||
// identity_document_url: None, // Sudah tidak dipakai, bisa dihapus dari schema jika tidak diperlukan
|
||||
bio: Some(format!("{} adalah user dengan data pribadi lengkap untuk testing.", fullname)),
|
||||
last_education: Some("S1 Teknik Informatika".into()),
|
||||
linkedin_url: Some("https://linkedin.com/in/user".into()),
|
||||
github_url: Some("https://github.com/user".into()),
|
||||
cv_url: Some("https://example.com/cv.pdf".into()),
|
||||
portfolio_url: Some("https://example.com/portfolio".into()),
|
||||
website_url: Some("https://example.com/website".into()),
|
||||
twitter_url: Some("https://twitter.com/user".into()),
|
||||
location: Some("Jakarta, Indonesia".into()),
|
||||
skills: Some(vec!["JavaScript".into(), "React".into(), "Node.js".into()]),
|
||||
experience: None,
|
||||
education: None,
|
||||
career_status: Some("Senior Developer".into()),
|
||||
role: Thing::from(("app_roles", role_id)),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
|
||||
@@ -20,8 +20,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
run_seed("seed_roles_permissions")?;
|
||||
run_seed("seed_users")?;
|
||||
run_seed("seed_events")?;
|
||||
run_seed("seed_hackathons")?;
|
||||
run_seed("seed_gacha_rolls")?;
|
||||
run_seed("seed_mentor_user")?;
|
||||
run_seed("seed_test_data")?;
|
||||
println!("\n✅ All seeding completed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
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::response::IntoResponse;
|
||||
use axum::{Extension, Json};
|
||||
use axum::{Extension, http::HeaderMap};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto,
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -26,7 +27,7 @@ use imphnen_libs::{
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||
(status = 200, description = "[PUBLIC] Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
@@ -44,7 +45,7 @@ pub async fn get_event_list(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
@@ -63,15 +64,18 @@ pub async fn get_event_by_id(
|
||||
path = "/v1/cms/landing/events/create",
|
||||
request_body = EventsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Create new event", body = MessageResponseDto)
|
||||
(status = 201, description = "[ADMIN] Create new event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn post_create_event(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<EventsCreateRequestDto>,
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::create_event(&state, payload).await
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::create_event(&state, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -85,16 +89,19 @@ pub async fn post_create_event(
|
||||
),
|
||||
request_body = EventsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Update event", body = MessageResponseDto)
|
||||
(status = 200, description = "[ADMIN] Update event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn patch_update_event(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<EventsUpdateRequestDto>,
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::update_event(&state, id, payload).await
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::update_event(&state, id, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -107,13 +114,16 @@ pub async fn patch_update_event(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Soft delete event", body = MessageResponseDto)
|
||||
(status = 200, description = "[ADMIN] Soft delete event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn delete_event(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::delete_event(&state, id).await
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::delete_event(&state, id).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
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)]
|
||||
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,
|
||||
|
||||
#[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,
|
||||
|
||||
#[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,
|
||||
|
||||
#[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,
|
||||
|
||||
#[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>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(length(max = 200, message = "Location name cannot exceed 200 characters"))]
|
||||
pub location: Option<String>,
|
||||
pub is_online: bool,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date, make_thing};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
|
||||
pub struct EventsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -25,6 +26,7 @@ impl<'a> EventsRepository<'a> {
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
info!(query = %query, "Executing SurrealDB query");
|
||||
let res: Vec<EventsQueryDto> =
|
||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -44,10 +46,20 @@ impl<'a> EventsRepository<'a> {
|
||||
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
// Attempt to parse the ID. If it's a full Thing (e.g., "events:some_id"), extract the ID part.
|
||||
// Otherwise, assume it's already the raw ID.
|
||||
let parsed_id = if id.contains(":") {
|
||||
let thing = make_thing(ResourceEnum::Events.to_string().as_str(), &id);
|
||||
get_id(&thing)?.1.to_string()
|
||||
} else {
|
||||
id.clone()
|
||||
};
|
||||
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||
.with_id(&id)
|
||||
.with_id(&parsed_id)
|
||||
.with_select_fields(vec!["*"]);
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query");
|
||||
let result: Option<EventsQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -72,6 +84,8 @@ impl<'a> EventsRepository<'a> {
|
||||
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events);
|
||||
info!(query = %query_str, "Executing SurrealDB query");
|
||||
let record: Option<EventsSchema> = db
|
||||
.create(ResourceEnum::Events.to_string())
|
||||
.content(data)
|
||||
@@ -106,6 +120,8 @@ impl<'a> EventsRepository<'a> {
|
||||
};
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
let query_str = format!("UPDATE {:?} MERGE ...", record_key);
|
||||
info!(query = %query_str, "Executing SurrealDB query");
|
||||
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
@@ -130,6 +146,8 @@ impl<'a> EventsRepository<'a> {
|
||||
}
|
||||
|
||||
let record_key = get_id(&event.id)?;
|
||||
let query_str = format!("UPDATE {:?} MERGE {{ is_deleted: true }}", record_key);
|
||||
info!(query = %query_str, "Executing SurrealDB query");
|
||||
let record: Option<EventsSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
|
||||
@@ -9,36 +9,30 @@ pub mod events_repository;
|
||||
pub mod events_schema;
|
||||
pub mod events_service;
|
||||
|
||||
pub use events_controller::*;
|
||||
pub use events_dto::*;
|
||||
pub use events_repository::*;
|
||||
pub use events_schema::*;
|
||||
pub use events_service::*;
|
||||
// Export only the necessary public items
|
||||
pub use events_dto::{
|
||||
EventsCreateRequestDto,
|
||||
EventsUpdateRequestDto,
|
||||
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 {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/events",
|
||||
get(events_controller::get_event_list),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/events/detail/{id}",
|
||||
get(events_controller::get_event_by_id),
|
||||
)
|
||||
.route("/cms/landing/events", get(get_event_list))
|
||||
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||
}
|
||||
|
||||
pub fn events_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/events/create",
|
||||
post(events_controller::post_create_event),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/events/update/{id}",
|
||||
patch(events_controller::patch_update_event),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/events/delete/{id}",
|
||||
delete(events_controller::delete_event),
|
||||
)
|
||||
.route("/cms/landing/events/create", post(post_create_event))
|
||||
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
|
||||
pub use events::*;
|
||||
pub use testimonials::*;
|
||||
pub use events::events_public_routes;
|
||||
pub use events::events_protected_routes;
|
||||
pub use testimonials::testimonials_public_routes;
|
||||
pub use testimonials::testimonials_protected_routes;
|
||||
|
||||
@@ -9,36 +9,30 @@ pub mod testimonials_repository;
|
||||
pub mod testimonials_schema;
|
||||
pub mod testimonials_service;
|
||||
|
||||
pub use testimonials_controller::*;
|
||||
pub use testimonials_dto::*;
|
||||
pub use testimonials_repository::*;
|
||||
pub use testimonials_schema::*;
|
||||
pub use testimonials_service::*;
|
||||
// Export only the necessary public items
|
||||
pub use testimonials_dto::{
|
||||
TestimonialsCreateRequestDto,
|
||||
TestimonialsUpdateRequestDto,
|
||||
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 {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/testimonials",
|
||||
get(testimonials_controller::get_testimonial_list),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/testimonials/detail/{id}",
|
||||
get(testimonials_controller::get_testimonial_by_id),
|
||||
)
|
||||
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
||||
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
||||
}
|
||||
|
||||
pub fn testimonials_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/testimonials/create",
|
||||
post(testimonials_controller::post_create_testimonial),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/testimonials/update/{id}",
|
||||
patch(testimonials_controller::patch_update_testimonial),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/testimonials/delete/{id}",
|
||||
delete(testimonials_controller::delete_testimonial),
|
||||
)
|
||||
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
||||
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
||||
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use super::{
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, Json};
|
||||
use imphnen_iam::UsersDetailQueryDto;
|
||||
use axum::{Extension, http::HeaderMap};
|
||||
use imphnen_iam::{UsersDetailQueryDto, require_auth};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto,
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -27,7 +27,7 @@ use imphnen_libs::{
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||
(status = 200, description = "[PUBLIC] Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
@@ -45,7 +45,7 @@ pub async fn get_testimonial_list(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
@@ -64,16 +64,19 @@ pub async fn get_testimonial_by_id(
|
||||
path = "/v1/cms/landing/testimonials/create",
|
||||
request_body = TestimonialsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Create new testimonial", body = MessageResponseDto)
|
||||
(status = 201, description = "[USER] Create new testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn post_create_testimonial(
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Json(payload): Json<TestimonialsCreateRequestDto>,
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -87,18 +90,20 @@ pub async fn post_create_testimonial(
|
||||
),
|
||||
request_body = TestimonialsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Update testimonial", body = MessageResponseDto)
|
||||
(status = 200, description = "[USER] Update testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn patch_update_testimonial(
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Json(payload): Json<TestimonialsUpdateRequestDto>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user)
|
||||
.await
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -111,14 +116,17 @@ pub async fn patch_update_testimonial(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Soft delete testimonial", body = MessageResponseDto)
|
||||
(status = 200, description = "[USER] Soft delete testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn delete_testimonial(
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Path(id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
|
||||
require_auth!(headers, state, {
|
||||
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 surrealdb::sql::Thing;
|
||||
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)]
|
||||
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,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 500,
|
||||
message = "Content must be between 1 and 500 characters"
|
||||
max = 1000,
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
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,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 500,
|
||||
message = "Content must be between 1 and 500 characters"
|
||||
max = 1000,
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||
use serde_json;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
|
||||
pub struct TestimonialsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -28,6 +29,7 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
info!(query = %query, "Executing SurrealDB query");
|
||||
let res: Vec<TestimonialsQueryDto> =
|
||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -50,11 +52,18 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
) -> Result<TestimonialsQueryDto> {
|
||||
let now = Instant::now();
|
||||
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())
|
||||
.with_id(&id)
|
||||
.with_id(&raw_id)
|
||||
.with_condition("is_deleted = false")
|
||||
.with_select_fields(vec!["*", "user.* as user"]);
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query");
|
||||
let result: Option<TestimonialsQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -79,9 +88,14 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
pub async fn query_create_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
) -> Result<TestimonialsSchema> { // Change return type from String to TestimonialsSchema
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
info!(
|
||||
resource = %ResourceEnum::Testimonials.to_string(),
|
||||
payload = ?data,
|
||||
"Executing SurrealDB create"
|
||||
);
|
||||
let record: Option<TestimonialsSchema> = db
|
||||
.create(ResourceEnum::Testimonials.to_string())
|
||||
.content(data)
|
||||
@@ -94,7 +108,7 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success create testimonial".into()),
|
||||
Some(created_testimonial) => Ok(created_testimonial), // Return the created testimonial
|
||||
None => bail!("Failed to create testimonial"),
|
||||
}
|
||||
}
|
||||
@@ -120,6 +134,11 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
};
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
info!(
|
||||
record_key = ?record_key,
|
||||
payload = ?merged,
|
||||
"Executing SurrealDB update"
|
||||
);
|
||||
let record: Option<TestimonialsSchema> =
|
||||
db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -145,6 +164,10 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
}
|
||||
|
||||
let record_key = get_id(&testimonial.id)?;
|
||||
info!(
|
||||
record_key = ?record_key,
|
||||
"Executing SurrealDB soft delete"
|
||||
);
|
||||
let record: Option<TestimonialsSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
|
||||
@@ -5,80 +5,87 @@ use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use super::testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
||||
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for TestimonialsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
role: String::new(),
|
||||
content: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
role: String::new(),
|
||||
content: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestimonialsSchema {
|
||||
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user: dto.user.id,
|
||||
role: dto.role,
|
||||
content: dto.content,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user: dto.user.id,
|
||||
role: dto.role,
|
||||
content: dto.content,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: user_id.clone(),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: user_id.clone(),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
id: String,
|
||||
user_id: &Thing,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: make_thing(&ResourceEnum::Testimonials.to_string(), &id),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
updated_at: get_iso_date(),
|
||||
user: user_id.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
id: String,
|
||||
user_id: &Thing,
|
||||
) -> 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 {
|
||||
id: make_thing(&ResourceEnum::Testimonials.to_string(), &raw_id),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
updated_at: get_iso_date(),
|
||||
user: user_id.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
common_response, success_list_response, success_response, success_created_response, validate_request,
|
||||
};
|
||||
|
||||
pub struct TestimonialsService;
|
||||
@@ -46,8 +46,8 @@ impl TestimonialsService {
|
||||
Ok(testimonial) if !testimonial.is_deleted => {
|
||||
success_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: testimonial.id.id.to_raw(),
|
||||
user_id: testimonial.user.id.id.to_raw(),
|
||||
id: testimonial.id.to_raw(),
|
||||
user_id: testimonial.user.id.to_raw(),
|
||||
user_fullname: testimonial.user.fullname,
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
@@ -72,7 +72,19 @@ impl TestimonialsService {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
||||
match repo.query_create_testimonial(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Ok(created_testimonial) => {
|
||||
success_created_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: created_testimonial.id.to_raw(),
|
||||
user_id: created_testimonial.user.id.to_raw(),
|
||||
user_fullname: authenticated_user.fullname.clone(),
|
||||
role: created_testimonial.role,
|
||||
content: created_testimonial.content,
|
||||
created_at: created_testimonial.created_at,
|
||||
updated_at: created_testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
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;
|
||||
|
||||
@@ -8,7 +8,9 @@ imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-middleware.workspace = true
|
||||
axum.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
|
||||
@@ -1,2 +1,12 @@
|
||||
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 ::axum::{
|
||||
extract::{Extension, Json, Path, Query},
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
response::Response,
|
||||
};
|
||||
use imphnen_entities::*;
|
||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
use imphnen_entities::MetaRequestDto;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::extract_email;
|
||||
use serde_json::json;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/register",
|
||||
path = "/v1/mentors/create",
|
||||
request_body = MentorUserRegisterRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 409, description = "Conflict - user already has mentor profile"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||
(status = 400, description = "[PUBLIC] Bad request - validation error"),
|
||||
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
|
||||
(status = 500, description = "[PUBLIC] Internal server error")
|
||||
),
|
||||
tag = "Mentors"
|
||||
)]
|
||||
pub async fn post_register_mentor(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Json(dto): Json<MentorUserRegisterRequestDto>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
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)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
@@ -56,16 +56,9 @@ pub async fn get_mentor_list(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadListMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::get_mentor_list(&app_state, meta).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
|
||||
MentorsService::get_mentor_list(&app_state, meta).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -75,9 +68,9 @@ pub async fn get_mentor_list(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get mentor by ID", body = MentorDetailResponseDto),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
@@ -89,16 +82,9 @@ pub async fn get_mentor_by_id(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadDetailMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::get_mentor_by_id(&app_state, &id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
|
||||
MentorsService::get_mentor_by_id(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -109,10 +95,10 @@ pub async fn get_mentor_by_id(
|
||||
),
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
@@ -123,18 +109,11 @@ pub async fn put_update_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<MentorUpdateRequestDto>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::UpdateMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::update_mentor(&app_state, &id, dto).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
|
||||
MentorsService::update_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -144,9 +123,9 @@ pub async fn put_update_mentor(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Mentor deleted successfully"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
@@ -158,16 +137,9 @@ pub async fn delete_mentor(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::DeleteMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::delete_mentor(&app_state, &id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
|
||||
MentorsService::delete_mentor(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -178,10 +150,10 @@ pub async fn delete_mentor(
|
||||
),
|
||||
request_body = MentorVerifyRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor verified successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(
|
||||
@@ -192,28 +164,21 @@ pub async fn put_verify_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<MentorVerifyRequestDto>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::VerifyMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MentorsService::verify_mentor(&app_state, &id, dto).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
|
||||
MentorsService::verify_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me",
|
||||
responses(
|
||||
(status = 200, description = "Current user's mentor profile", body = MentorDetailResponseDto),
|
||||
(status = 401, description = "Unauthorized - invalid token"),
|
||||
(status = 403, description = "Mentor profile not found for current user"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
@@ -221,46 +186,33 @@ pub async fn put_verify_mentor(
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_me(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadOwnMentorProfile],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return (
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "Unauthorized",
|
||||
"message": "Token tidak valid"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_me(&app_state, &email).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_me(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update/me",
|
||||
path = "/v1/mentors/me/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "Bad request - validation error"),
|
||||
(status = 401, description = "Unauthorized - invalid token"),
|
||||
(status = 404, description = "Mentor profile not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[MENTOR] Bad request - validation error"),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 404, description = "[MENTOR] Mentor profile not found"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
@@ -268,38 +220,29 @@ pub async fn get_mentor_me(
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_mentor_me(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<MentorUpdateRequestDto>,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::UpdateOwnMentorProfile],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||
})
|
||||
}
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
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"
|
||||
)]
|
||||
@@ -312,12 +255,12 @@ pub async fn put_update_mentor_no_id() -> Response {
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/status",
|
||||
path = "/v1/mentors/me/status",
|
||||
responses(
|
||||
(status = 200, description = "Mentor application status", body = String),
|
||||
(status = 401, description = "Unauthorized - invalid token"),
|
||||
(status = 403, description = "No mentor application found for current user"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 200, description = "[MENTOR] Mentor application status", body = String),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] No mentor application found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(
|
||||
@@ -325,32 +268,19 @@ pub async fn put_update_mentor_no_id() -> Response {
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_status(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Extension(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadOwnMentorStatus],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return (
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "Unauthorized",
|
||||
"message": "Token tidak valid"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_status(&app_state, &email).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_status(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,15 +18,9 @@ pub struct MentorListResponseDto {
|
||||
pub struct MentorDetailWithUserDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
// Personal data is now in UsersSchema, access via user_id
|
||||
// Removed: fullname, email, legal_name, identity_document_url,
|
||||
// phone_for_verification, bio, linkedin_url, github_url, cv_url
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
@@ -47,15 +41,20 @@ pub struct MentorDetailWithUserDto {
|
||||
pub struct MentorDetailResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
// Personal data fields from UsersSchema
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub legal_name: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
// Professional data from MentorSchema
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
@@ -94,9 +93,6 @@ pub struct MentorUpdateRequestDto {
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[validate(url(message = "Invalid identity document URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub identity_document_url: Option<String>,
|
||||
#[validate(length(
|
||||
min = 10,
|
||||
max = 15,
|
||||
@@ -118,6 +114,7 @@ pub struct MentorUpdateRequestDto {
|
||||
#[validate(url(message = "Invalid CV URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[validate(url(message = "Invalid portfolio URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[validate(length(min = 1, message = "At least 1 industry required"))]
|
||||
@@ -170,7 +167,7 @@ pub struct MentorUserRegisterRequestDto {
|
||||
message = "Password must have at least 8 characters"
|
||||
))]
|
||||
#[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"
|
||||
))]
|
||||
pub password: String,
|
||||
@@ -279,18 +276,7 @@ pub struct MentoringRate {
|
||||
pub struct MentorInsertDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Option<Thing>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
// Personal data removed - now stored in UsersSchema
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
@@ -313,18 +299,7 @@ impl From<MentorSchema> for MentorInsertDto {
|
||||
MentorInsertDto {
|
||||
id: schema.id,
|
||||
user_id: schema.user_id,
|
||||
email: schema.email,
|
||||
legal_name: schema.legal_name,
|
||||
gender: schema.gender,
|
||||
domicile: schema.domicile,
|
||||
identity_document_url: schema.identity_document_url,
|
||||
phone_for_verification: schema.phone_for_verification,
|
||||
bio: schema.bio,
|
||||
last_education: schema.last_education,
|
||||
linkedin_url: schema.linkedin_url,
|
||||
github_url: schema.github_url,
|
||||
cv_url: schema.cv_url,
|
||||
portfolio_url: schema.portfolio_url,
|
||||
// Personal data removed from MentorSchema
|
||||
industries: schema.industries,
|
||||
expertise: schema.expertise,
|
||||
languages: schema.languages,
|
||||
@@ -354,19 +329,10 @@ pub struct MentorVerifyRequestDto {
|
||||
pub struct MentorDetailQueryDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
// Personal data has been moved to UsersSchema
|
||||
// Use user_id to get: fullname, email, legal_name, gender, domicile,
|
||||
// identity_document_url, phone_for_verification, bio, last_education,
|
||||
// linkedin_url, github_url, cv_url, portfolio_url
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
@@ -388,8 +354,8 @@ impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
fullname: dto.fullname,
|
||||
email: dto.email,
|
||||
fullname: None, // now in user table, must be populated from service layer
|
||||
email: None, // now in user table, must be populated from service layer
|
||||
status: dto.status,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
@@ -402,15 +368,20 @@ impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
user_id: extract_id(&dto.user_id),
|
||||
fullname: dto.fullname,
|
||||
email: dto.email,
|
||||
legal_name: dto.legal_name,
|
||||
identity_document_url: dto.identity_document_url,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
// Personal data fields are populated in service layer from UsersSchema
|
||||
fullname: None, // populated from user table in service layer
|
||||
email: None, // populated from user table in service layer
|
||||
legal_name: None, // populated from user table in service layer
|
||||
gender: None, // populated from user table in service layer
|
||||
domicile: None, // populated from user table in service layer
|
||||
phone_for_verification: None, // populated from user table in service layer
|
||||
bio: None, // populated from user table in service layer
|
||||
last_education: None, // populated from user table in service layer
|
||||
linkedin_url: None, // populated from user table in service layer
|
||||
github_url: None, // populated from user table in service layer
|
||||
cv_url: None, // populated from user table in service layer
|
||||
portfolio_url: None, // populated from user table in service layer
|
||||
// Professional data from mentor
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
@@ -433,7 +404,7 @@ impl From<MentorSchema> for MentorRegisterResponseDto {
|
||||
Self {
|
||||
id: schema.id.to_string(),
|
||||
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
|
||||
email: schema.email,
|
||||
email: None, // schema.email - now in user table
|
||||
status: schema.status,
|
||||
created_at: schema.created_at,
|
||||
updated_at: schema.updated_at,
|
||||
@@ -446,19 +417,7 @@ impl From<MentorDetailWithUserDto> for MentorDetailQueryDto {
|
||||
MentorDetailQueryDto {
|
||||
id: dto.id,
|
||||
user_id: dto.user_id,
|
||||
fullname: dto.fullname,
|
||||
email: dto.email,
|
||||
legal_name: dto.legal_name,
|
||||
gender: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
domicile: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
identity_document_url: dto.identity_document_url,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
last_education: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
portfolio_url: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
// Personal data removed from MentorDetailWithUserDto - now in UsersSchema
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
|
||||
@@ -2,12 +2,14 @@ use anyhow::{Result, bail};
|
||||
use imphnen_iam::{get_id, make_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_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
|
||||
use serde_json::{Map, Value};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
|
||||
pub struct MentorsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
@@ -27,19 +29,11 @@ impl<'a> MentorsRepository<'a> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
|
||||
.search_field("legal_name")
|
||||
.search_field("user_id.legal_name") // Search in user data instead
|
||||
.select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
@@ -79,19 +73,11 @@ impl<'a> MentorsRepository<'a> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
||||
.with_where("email", Some(email.clone()))
|
||||
.with_where("user_id.email", Some(email.clone())) // Search in user table
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
@@ -114,6 +100,7 @@ impl<'a> MentorsRepository<'a> {
|
||||
}
|
||||
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_email");
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -136,20 +123,22 @@ impl<'a> MentorsRepository<'a> {
|
||||
) -> Result<MentorDetailWithUserDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
||||
.with_id(get_id(id)?.1)
|
||||
|
||||
// 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![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
@@ -172,20 +161,24 @@ impl<'a> MentorsRepository<'a> {
|
||||
}
|
||||
|
||||
let sql = builder.build();
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
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 elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let Some(mentor) = mentor_opt else {
|
||||
bail!("Mentor not found in database");
|
||||
bail!("Mentor not found");
|
||||
};
|
||||
if mentor.is_deleted && !include_deleted {
|
||||
bail!("Mentor has been deleted");
|
||||
}
|
||||
|
||||
Ok(mentor)
|
||||
}
|
||||
|
||||
@@ -194,8 +187,10 @@ impl<'a> MentorsRepository<'a> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let dto: MentorInsertDto = data.into();
|
||||
let resource = ResourceEnum::Mentors.to_string();
|
||||
info!(query = %resource, "Executing SurrealDB create in query_create_mentor");
|
||||
let record: Option<MentorSchema> = db
|
||||
.create(ResourceEnum::Mentors.to_string())
|
||||
.create(resource)
|
||||
.content(dto.clone())
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -237,6 +232,7 @@ impl<'a> MentorsRepository<'a> {
|
||||
|
||||
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
info!(query = ?record_key, "Executing SurrealDB update in query_update_mentor");
|
||||
let record: Option<MentorSchema> =
|
||||
db.update(record_key).merge(merged_data_json).await?;
|
||||
let elapsed = now.elapsed();
|
||||
@@ -282,6 +278,7 @@ impl<'a> MentorsRepository<'a> {
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
info!(query = ?record_key, "Executing SurrealDB soft delete in query_delete_mentor");
|
||||
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
IdentityAndVerification, MentorDetailQueryDto, MentorUpdateRequestDto,
|
||||
MentorDetailQueryDto, MentorUpdateRequestDto,
|
||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
@@ -12,18 +12,9 @@ pub struct MentorSchema {
|
||||
pub id: Thing,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<Thing>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
// Personal data has been moved to UsersSchema - use user_id to reference
|
||||
// phone_for_verification, bio, last_education, linkedin_url, github_url,
|
||||
// cv_url, portfolio_url
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
@@ -52,18 +43,6 @@ impl Default for MentorSchema {
|
||||
ResourceEnum::Users.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
)),
|
||||
email: None,
|
||||
legal_name: String::new(),
|
||||
gender: None,
|
||||
domicile: None,
|
||||
identity_document_url: String::new(),
|
||||
phone_for_verification: String::new(),
|
||||
bio: String::new(),
|
||||
last_education: None,
|
||||
linkedin_url: None,
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: None,
|
||||
industries: Vec::new(),
|
||||
expertise: Vec::new(),
|
||||
languages: Vec::new(),
|
||||
@@ -89,11 +68,9 @@ impl Default for MentorSchema {
|
||||
|
||||
impl MentorSchema {
|
||||
pub fn create(
|
||||
identity_and_verification: IdentityAndVerification,
|
||||
professional_profile: ProfessionalProfile,
|
||||
mentoring_logistics: MentoringLogistics,
|
||||
user_id_raw: String,
|
||||
email_str: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
@@ -101,18 +78,7 @@ impl MentorSchema {
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)),
|
||||
email: Some(email_str),
|
||||
legal_name: identity_and_verification.legal_name,
|
||||
gender: identity_and_verification.gender,
|
||||
domicile: identity_and_verification.domicile,
|
||||
identity_document_url: identity_and_verification.identity_document_url,
|
||||
phone_for_verification: identity_and_verification.phone_for_verification,
|
||||
bio: professional_profile.bio,
|
||||
last_education: professional_profile.last_education,
|
||||
linkedin_url: professional_profile.linkedin_url,
|
||||
github_url: professional_profile.github_url,
|
||||
cv_url: professional_profile.cv_url,
|
||||
portfolio_url: professional_profile.portfolio_url,
|
||||
// Personal data now stored in UsersSchema, not here
|
||||
industries: professional_profile.industries,
|
||||
expertise: professional_profile.expertise,
|
||||
languages: professional_profile.languages,
|
||||
@@ -139,18 +105,7 @@ impl MentorSchema {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user_id: Some(dto.user_id),
|
||||
email: dto.email,
|
||||
legal_name: dto.legal_name,
|
||||
gender: dto.gender,
|
||||
domicile: dto.domicile,
|
||||
identity_document_url: dto.identity_document_url,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
last_education: dto.last_education,
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
portfolio_url: dto.portfolio_url,
|
||||
// Personal data now comes from UsersSchema via user_id
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
@@ -170,40 +125,10 @@ impl MentorSchema {
|
||||
}
|
||||
|
||||
pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self {
|
||||
// Update fields only if they are Some(value), otherwise preserve current value
|
||||
if let Some(val) = dto.legal_name {
|
||||
self.legal_name = val;
|
||||
}
|
||||
if let Some(val) = dto.gender {
|
||||
self.gender = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.domicile {
|
||||
self.domicile = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.identity_document_url {
|
||||
self.identity_document_url = val;
|
||||
}
|
||||
if let Some(val) = dto.phone_for_verification {
|
||||
self.phone_for_verification = val;
|
||||
}
|
||||
if let Some(val) = dto.bio {
|
||||
self.bio = val;
|
||||
}
|
||||
if let Some(val) = dto.last_education {
|
||||
self.last_education = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.linkedin_url {
|
||||
self.linkedin_url = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.github_url {
|
||||
self.github_url = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.cv_url {
|
||||
self.cv_url = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.portfolio_url {
|
||||
self.portfolio_url = Some(val);
|
||||
}
|
||||
// phone_for_verification, bio, last_education, linkedin_url, github_url,
|
||||
// cv_url, portfolio_url) are now updated in UsersSchema, not here
|
||||
|
||||
// Only update professional fields that are still in MentorSchema
|
||||
if let Some(val) = dto.industries {
|
||||
self.industries = val;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ use crate::v1::mentors::{
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
use imphnen_entities::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_iam::{
|
||||
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||
};
|
||||
@@ -33,9 +34,9 @@ impl MentorsService {
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let role_repo = RolesRepository::new(state);
|
||||
let auth_repo = AuthRepository::new(state);
|
||||
let 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 existing_user_result =
|
||||
@@ -56,10 +57,22 @@ 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.phone_number = dto.phone_number.clone();
|
||||
// Update personal data from identity_and_verification
|
||||
user_schema.legal_name = Some(dto.identity_and_verification.legal_name.clone());
|
||||
user_schema.gender = dto.identity_and_verification.gender.clone();
|
||||
user_schema.domicile = dto.identity_and_verification.domicile.clone();
|
||||
user_schema.phone_for_verification = Some(dto.identity_and_verification.phone_for_verification.clone());
|
||||
// Update personal data from professional_profile
|
||||
user_schema.bio = Some(dto.professional_profile.bio.clone());
|
||||
user_schema.last_education = dto.professional_profile.last_education.clone();
|
||||
user_schema.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
user_schema.github_url = dto.professional_profile.github_url.clone();
|
||||
user_schema.cv_url = dto.professional_profile.cv_url.clone();
|
||||
user_schema.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
user_schema.updated_at = imphnen_utils::get_iso_date();
|
||||
|
||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||
@@ -87,7 +100,7 @@ impl MentorsService {
|
||||
}
|
||||
};
|
||||
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;
|
||||
|
||||
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
|
||||
@@ -127,18 +140,30 @@ impl MentorsService {
|
||||
};
|
||||
|
||||
let new_user_schema = UsersSchema {
|
||||
id: imphnen_utils::make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
id: imphnen_utils::make_thing_from_enum(
|
||||
ResourceEnum::Users,
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
email: dto.email.clone(),
|
||||
fullname: dto.fullname.clone(),
|
||||
email: dto.email,
|
||||
fullname: dto.fullname,
|
||||
password: hashed_password,
|
||||
phone_number: dto.phone_number.clone(),
|
||||
phone_number: dto.phone_number,
|
||||
// Store personal data from identity_and_verification in user
|
||||
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||
gender: dto.identity_and_verification.gender.clone(),
|
||||
domicile: dto.identity_and_verification.domicile.clone(),
|
||||
phone_for_verification: Some(dto.identity_and_verification.phone_for_verification.clone()),
|
||||
// Store personal data from professional_profile in user
|
||||
bio: Some(dto.professional_profile.bio.clone()),
|
||||
last_education: dto.professional_profile.last_education.clone(),
|
||||
linkedin_url: dto.professional_profile.linkedin_url.clone(),
|
||||
github_url: dto.professional_profile.github_url.clone(),
|
||||
cv_url: dto.professional_profile.cv_url.clone(),
|
||||
portfolio_url: dto.professional_profile.portfolio_url.clone(),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
role: imphnen_utils::make_thing(
|
||||
&ResourceEnum::Roles.to_string(),
|
||||
role: imphnen_utils::make_thing_from_enum(
|
||||
ResourceEnum::Roles,
|
||||
&mentor_role.id,
|
||||
),
|
||||
is_active: false,
|
||||
@@ -162,11 +187,11 @@ impl MentorsService {
|
||||
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
|
||||
|
||||
match auth_repo
|
||||
.query_store_otp(final_user_email.clone(), otp)
|
||||
.query_store_otp(final_user_email.clone(), otp.clone())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let message = format!("your otp code is {otp}");
|
||||
let message = format!("your otp code is {}", otp.code);
|
||||
if let Err(_err) =
|
||||
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
|
||||
{
|
||||
@@ -187,11 +212,9 @@ impl MentorsService {
|
||||
}
|
||||
|
||||
let mentor_schema = MentorSchema::create(
|
||||
dto.identity_and_verification,
|
||||
dto.professional_profile,
|
||||
dto.mentoring_logistics,
|
||||
user_id.to_raw(),
|
||||
final_user_email.clone(),
|
||||
);
|
||||
|
||||
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
||||
@@ -233,16 +256,27 @@ impl MentorsService {
|
||||
|
||||
pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let user_repo = UsersRepository::new(state);
|
||||
|
||||
match repo.query_mentor_list(meta).await {
|
||||
Ok(result) => {
|
||||
let data: Vec<MentorListResponseDto> = result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(MentorDetailQueryDto::from)
|
||||
.map(MentorListResponseDto::from)
|
||||
.collect();
|
||||
let mut mentor_list_data: Vec<MentorListResponseDto> = Vec::new();
|
||||
|
||||
for mentor_with_user in result.data {
|
||||
let mentor_dto = MentorDetailQueryDto::from(mentor_with_user);
|
||||
let mut list_item = MentorListResponseDto::from(mentor_dto.clone());
|
||||
|
||||
// Get user data to populate personal fields
|
||||
if let Ok(user) = user_repo.query_user_by_id(&mentor_dto.user_id).await {
|
||||
list_item.fullname = Some(user.fullname);
|
||||
list_item.email = Some(user.email);
|
||||
}
|
||||
|
||||
mentor_list_data.push(list_item);
|
||||
}
|
||||
|
||||
success_list_response(ResponseListSuccessDto {
|
||||
data,
|
||||
data: mentor_list_data,
|
||||
meta: result.meta,
|
||||
})
|
||||
}
|
||||
@@ -251,12 +285,56 @@ impl MentorsService {
|
||||
}
|
||||
|
||||
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
|
||||
match mentor_repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => {
|
||||
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
// Get user data separately
|
||||
let user_result = user_repo.query_user_by_id(&mentor.user_id).await;
|
||||
match user_result {
|
||||
Ok(user) => {
|
||||
// Combine mentor and user data
|
||||
let dto = MentorDetailResponseDto {
|
||||
id: mentor.id.to_raw(),
|
||||
user_id: mentor.user_id.to_raw(),
|
||||
// Personal data from user
|
||||
fullname: Some(user.fullname),
|
||||
email: Some(user.email),
|
||||
legal_name: user.legal_name,
|
||||
gender: user.gender,
|
||||
domicile: user.domicile,
|
||||
phone_for_verification: user.phone_for_verification,
|
||||
bio: user.bio,
|
||||
last_education: user.last_education,
|
||||
linkedin_url: user.linkedin_url,
|
||||
github_url: user.github_url,
|
||||
cv_url: user.cv_url,
|
||||
portfolio_url: user.portfolio_url,
|
||||
// Professional data from mentor
|
||||
industries: mentor.industries,
|
||||
expertise: mentor.expertise,
|
||||
languages: mentor.languages,
|
||||
current_company: mentor.current_company,
|
||||
current_role: mentor.current_role,
|
||||
years_of_experience: mentor.years_of_experience,
|
||||
topics_of_interest: mentor.topics_of_interest,
|
||||
preferred_mentee_level: mentor.preferred_mentee_level,
|
||||
preferred_mentoring_formats: mentor.preferred_mentoring_formats,
|
||||
availability_commitment: mentor.availability_commitment,
|
||||
mentoring_rate: mentor.mentoring_rate,
|
||||
status: mentor.status,
|
||||
created_at: mentor.created_at,
|
||||
updated_at: mentor.updated_at,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(_e) => {
|
||||
error!("Failed to get user data for mentor {}: {}", id, _e);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get mentor user data")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
}
|
||||
|
||||
@@ -9,22 +9,53 @@ pub mod mentors_repository;
|
||||
pub mod mentors_schema;
|
||||
pub mod mentors_service;
|
||||
|
||||
pub use mentors_controller::*;
|
||||
pub use mentors_dto::*;
|
||||
pub use mentors_repository::*;
|
||||
pub use mentors_schema::*;
|
||||
pub use mentors_service::*;
|
||||
// Explicitly export only public controller functions and key types
|
||||
pub use mentors_controller::{
|
||||
post_register_mentor,
|
||||
get_mentor_list,
|
||||
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 {
|
||||
Router::new()
|
||||
.route("/", get(get_mentor_list))
|
||||
.route("/register", post(post_register_mentor))
|
||||
.route("/create", post(post_register_mentor))
|
||||
.route("/me", get(get_mentor_me))
|
||||
.route("/update/me", put(put_update_mentor_me))
|
||||
.route("/status", get(get_mentor_status))
|
||||
.route("/me/update", put(put_update_mentor_me))
|
||||
.route("/me/status", get(get_mentor_status))
|
||||
.route("/detail/{id}", get(get_mentor_by_id))
|
||||
.route("/update/{id}", put(put_update_mentor))
|
||||
.route("/update", put(put_update_mentor_no_id))
|
||||
.route("/delete/{id}", delete(delete_mentor))
|
||||
.route("/verify/{id}", put(put(put_verify_mentor)))
|
||||
.route("/verify/{id}", put(put_verify_mentor))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
use axum::Router;
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user