Compare commits
78
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 |
@@ -13,3 +13,4 @@
|
|||||||
.env.development
|
.env.development
|
||||||
.env.staging
|
.env.staging
|
||||||
.env.production
|
.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
+61
-1
@@ -2038,7 +2038,9 @@ dependencies = [
|
|||||||
"imphnen-cms",
|
"imphnen-cms",
|
||||||
"imphnen-dimentorin",
|
"imphnen-dimentorin",
|
||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
|
"imphnen-gacha",
|
||||||
"imphnen-gateway",
|
"imphnen-gateway",
|
||||||
|
"imphnen-hackathon",
|
||||||
"imphnen-iam",
|
"imphnen-iam",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"imphnen-utils",
|
"imphnen-utils",
|
||||||
@@ -2092,6 +2094,7 @@ name = "imphnen-dimentorin"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-test",
|
"axum-test",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2100,6 +2103,7 @@ dependencies = [
|
|||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
"imphnen-iam",
|
"imphnen-iam",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
|
"imphnen-middleware",
|
||||||
"imphnen-utils",
|
"imphnen-utils",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
@@ -2121,10 +2125,15 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum 0.27.2",
|
||||||
|
"strum_macros 0.27.2",
|
||||||
"surrealdb",
|
"surrealdb",
|
||||||
"thiserror 2.0.14",
|
"thiserror 2.0.14",
|
||||||
"utoipa",
|
"utoipa",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2167,6 +2176,7 @@ dependencies = [
|
|||||||
"imphnen-dimentorin",
|
"imphnen-dimentorin",
|
||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
"imphnen-gacha",
|
"imphnen-gacha",
|
||||||
|
"imphnen-hackathon",
|
||||||
"imphnen-iam",
|
"imphnen-iam",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"imphnen-middleware",
|
"imphnen-middleware",
|
||||||
@@ -2184,11 +2194,53 @@ dependencies = [
|
|||||||
"validator",
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imphnen-hackathon"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
|
"axum-test",
|
||||||
|
"chrono",
|
||||||
|
"dotenvy",
|
||||||
|
"futures",
|
||||||
|
"http-body-util",
|
||||||
|
"imphnen-entities",
|
||||||
|
"imphnen-iam",
|
||||||
|
"imphnen-libs",
|
||||||
|
"imphnen-utils",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"mockall",
|
||||||
|
"oauth2",
|
||||||
|
"once_cell",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"regex",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum 0.27.2",
|
||||||
|
"strum_macros 0.27.2",
|
||||||
|
"surrealdb",
|
||||||
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-swagger-ui",
|
||||||
|
"uuid",
|
||||||
|
"validator",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imphnen-iam"
|
name = "imphnen-iam"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-test",
|
"axum-test",
|
||||||
@@ -2227,6 +2279,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2240,11 +2293,13 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"surrealdb",
|
"surrealdb",
|
||||||
"tokio",
|
"tokio",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2255,13 +2310,14 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-test",
|
"axum-test",
|
||||||
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
"imphnen-iam",
|
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"imphnen-utils",
|
"imphnen-utils",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"log",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2288,6 +2344,7 @@ dependencies = [
|
|||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4844,6 +4901,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"imphnen-dimentorin",
|
"imphnen-dimentorin",
|
||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
|
"imphnen-hackathon",
|
||||||
"imphnen-iam",
|
"imphnen-iam",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"imphnen-utils",
|
"imphnen-utils",
|
||||||
@@ -4856,6 +4914,8 @@ dependencies = [
|
|||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"yoke",
|
||||||
|
"yoke-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+14
-11
@@ -1,21 +1,23 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"tests",
|
"tests",
|
||||||
"imphnen-iam",
|
"imphnen-entities", # Most basic - core data structures
|
||||||
"imphnen-cms",
|
"imphnen-libs", # Depends on entities
|
||||||
"imphnen-libs",
|
"imphnen-utils", # Depends on libs and entities
|
||||||
"imphnen-utils",
|
"imphnen-middleware",# Utility for permissions
|
||||||
"imphnen-gacha",
|
"imphnen-iam", # Core auth service, depends on libs, utils, entities
|
||||||
"imphnen-gateway",
|
"imphnen-cms", # Content management, depends on core services
|
||||||
"imphnen-backend",
|
"imphnen-gacha", # Game mechanics, depends on core services
|
||||||
"imphnen-entities",
|
"imphnen-dimentorin",# Learning platform, depends on core services
|
||||||
"imphnen-dimentorin",
|
"imphnen-hackathon", # Hackathon service, depends on core services
|
||||||
"imphnen-middleware",
|
"imphnen-gateway", # API gateway, depends on all services
|
||||||
|
"imphnen-backend", # Main application, depends on all services
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
async-trait = "0.1.83"
|
||||||
oauth2 = "5.0.0"
|
oauth2 = "5.0.0"
|
||||||
reqwest = { version = "0.12.23", features = ["json"] }
|
reqwest = { version = "0.12.23", features = ["json"] }
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
@@ -78,6 +80,7 @@ imphnen-gateway = { path = "./imphnen-gateway" }
|
|||||||
imphnen-backend = { path = "./imphnen-backend" }
|
imphnen-backend = { path = "./imphnen-backend" }
|
||||||
imphnen-entities = { path = "./imphnen-entities" }
|
imphnen-entities = { path = "./imphnen-entities" }
|
||||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||||
|
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
||||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
+31
-23
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Script deploy ke VPS
|
# Script deploy semua binary hasil build Rust (untuk Linux)
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -7,35 +7,43 @@ REMOTE_USER="asephs"
|
|||||||
REMOTE_HOST="70.153.9.42"
|
REMOTE_HOST="70.153.9.42"
|
||||||
REMOTE_PATH="/home/asephs/imphnen-backend-service"
|
REMOTE_PATH="/home/asephs/imphnen-backend-service"
|
||||||
|
|
||||||
# Build project
|
# Build release binary
|
||||||
taskset -c 0,1 cargo build --release -j 2
|
echo "🔧 Building Rust project..."
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
# Rsync hasil build dan file yang diperlukan
|
# Filter hanya file executable tanpa ekstensi .exe, .rlib, atau .d
|
||||||
rsync -avz --delete \
|
BINARIES=$(find target/release -maxdepth 1 -type f ! -name "*.exe" ! -name "*.rlib" ! -name "*.d")
|
||||||
target/release/ \
|
|
||||||
|
if [ -z "$BINARIES" ]; then
|
||||||
|
echo "❌ Tidak ada binary Linux (.exe/.rlib/.d diabaikan)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload semua binary yang valid ke server
|
||||||
|
echo "🚀 Mengirim binary ke server..."
|
||||||
|
rsync -avz --compress-level=9 --progress \
|
||||||
|
$BINARIES \
|
||||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
|
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
|
||||||
|
|
||||||
# Sync file konfigurasi dan source code (jika perlu)
|
# Jalankan ulang service utama di VPS
|
||||||
rsync -avz --delete \
|
echo "♻️ Restart service utama di server..."
|
||||||
imphnen-backend/ \
|
|
||||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/imphnen-backend/
|
|
||||||
|
|
||||||
rsync -avz --delete \
|
|
||||||
docker-compose.yml Dockerfile \
|
|
||||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/
|
|
||||||
|
|
||||||
# Tambahkan file lain jika diperlukan
|
|
||||||
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||||
|
set -e
|
||||||
cd /home/asephs/imphnen-backend-service
|
cd /home/asephs/imphnen-backend-service
|
||||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
|
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
export PATH=$PATH:/root/.local/share/pnpm
|
|
||||||
export PATH=$PATH:/home/asephs/.nvm/versions/node/v22.17.1/bin
|
|
||||||
export PATH=$PATH:/home/asephs/.bun/bin/bun
|
|
||||||
|
|
||||||
if [ -f ~/.bashrc ]; then
|
if [ -f ~/.bashrc ]; then
|
||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
fi
|
fi
|
||||||
pm2 restart 4 --update-env
|
|
||||||
|
# Pastikan semua binary bisa dieksekusi
|
||||||
|
chmod +x target/release/*
|
||||||
|
|
||||||
|
# Restart service utama (misalnya api.d)
|
||||||
|
if pm2 list | grep -q api; then
|
||||||
|
pm2 restart api --update-env
|
||||||
|
else
|
||||||
|
pm2 start target/release/api.d --name api
|
||||||
|
fi
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Deploy selesai ke $REMOTE_HOST:$REMOTE_PATH"
|
echo "✅ Deploy semua binary Linux selesai ke $REMOTE_HOST:$REMOTE_PATH"
|
||||||
|
|||||||
@@ -40,8 +40,16 @@ name = "seed_roles_permissions"
|
|||||||
path = "src/bin/seed_roles_permissions.rs"
|
path = "src/bin/seed_roles_permissions.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "seed_users"
|
name = "seed_teams"
|
||||||
path = "src/bin/seed_users.rs"
|
path = "src/bin/seed_teams.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_hackathons"
|
||||||
|
path = "src/bin/seed_hackathons.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_test_data"
|
||||||
|
path = "src/bin/seed_test_data.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
imphnen-libs.workspace = true
|
imphnen-libs.workspace = true
|
||||||
@@ -50,7 +58,9 @@ imphnen-gateway.workspace = true
|
|||||||
imphnen-entities.workspace = true
|
imphnen-entities.workspace = true
|
||||||
imphnen-iam.workspace = true
|
imphnen-iam.workspace = true
|
||||||
imphnen-cms.workspace = true
|
imphnen-cms.workspace = true
|
||||||
|
imphnen-gacha.workspace = true
|
||||||
imphnen-dimentorin.workspace = true
|
imphnen-dimentorin.workspace = true
|
||||||
|
imphnen-hackathon.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use axum::Router;
|
|
||||||
use imphnen_gateway::gateway_service;
|
use imphnen_gateway::gateway_service;
|
||||||
use imphnen_libs::axum_init;
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
@@ -6,10 +5,7 @@ use imphnen_libs::axum_init;
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||||
let app = gateway_service(surrealdb_ws, surrealdb_mem).await;
|
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||||
let mut router = Router::new();
|
|
||||||
router = router.nest("/api/v1/auth", imphnen_iam::v1::auth::auth_router());
|
|
||||||
app.merge(router)
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::enviroment::ENV;
|
let env = &imphnen_libs::environment::ENV;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use surrealdb::sql::Thing;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::enviroment::ENV;
|
let env = &imphnen_libs::environment::ENV;
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
@@ -18,13 +18,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_gacha_items', $id)")
|
db.query("DELETE type::thing('app_gacha_items', $id)")
|
||||||
.bind(("id", "gacha_item_test_id"))
|
.bind(("id", "1"))
|
||||||
.await?;
|
.await?;
|
||||||
db.query("DELETE type::thing('app_gacha_rolls', $id)")
|
db.query("DELETE type::thing('app_gacha_rolls', $id)")
|
||||||
.bind(("id", "gacha_roll_test_id"))
|
.bind(("id", "test-gacha-roll-001"))
|
||||||
.await?;
|
.await?;
|
||||||
|
let gacha_item_id = "1";
|
||||||
let gacha_item_id = "gacha_item_test_id";
|
|
||||||
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||||
.bind(("id", gacha_item_id))
|
.bind(("id", gacha_item_id))
|
||||||
.bind(("name", "Test Gacha Item"))
|
.bind(("name", "Test Gacha Item"))
|
||||||
@@ -35,7 +34,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
.await?;
|
.await?;
|
||||||
println!("Gacha Item seeded successfully!");
|
println!("Gacha Item seeded successfully!");
|
||||||
|
|
||||||
let gacha_roll_id = "gacha_roll_test_id";
|
let gacha_roll_id = "test-gacha-roll-001";
|
||||||
db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||||
.bind(("id", gacha_roll_id))
|
.bind(("id", gacha_roll_id))
|
||||||
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
|
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ use surrealdb::opt::auth::Root;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::enviroment::ENV;
|
let env = &imphnen_libs::environment::ENV;
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use surrealdb::opt::auth::Root;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::enviroment::ENV;
|
let env = &imphnen_libs::environment::ENV;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
@@ -54,6 +54,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
PermissionsEnum::UpdateMentors,
|
PermissionsEnum::UpdateMentors,
|
||||||
PermissionsEnum::VerifyMentors,
|
PermissionsEnum::VerifyMentors,
|
||||||
PermissionsEnum::DeleteMentors,
|
PermissionsEnum::DeleteMentors,
|
||||||
|
PermissionsEnum::Administrator,
|
||||||
] {
|
] {
|
||||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||||
.bind(("id", permission.id()))
|
.bind(("id", permission.id()))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use surrealdb::engine::any;
|
|||||||
use surrealdb::opt::auth::Root;
|
use surrealdb::opt::auth::Root;
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::enviroment::ENV;
|
let env = &imphnen_libs::environment::ENV;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
@@ -26,7 +26,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
(
|
(
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"User",
|
"User",
|
||||||
None,
|
Some("2025-02-28T14:53:58.576688+00"),
|
||||||
Some("2025-02-28T14:53:58.576688+00"),
|
Some("2025-02-28T14:53:58.576688+00"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -44,13 +44,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
(
|
(
|
||||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
"Admin",
|
"Admin",
|
||||||
None,
|
Some("2025-02-22T15:38:39.868306+00"),
|
||||||
Some("2025-02-22T15:38:39.868306+00"),
|
Some("2025-02-22T15:38:39.868306+00"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
"Mentor",
|
"Mentor",
|
||||||
None,
|
Some("2025-07-06T10:00:00.000000+00"),
|
||||||
Some("2025-07-06T10:00:00.000000+00"),
|
Some("2025-07-06T10:00:00.000000+00"),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use surrealdb::opt::auth::Root;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::enviroment::ENV;
|
let env = &imphnen_libs::environment::ENV;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
username: &env.surrealdb_username,
|
username: &env.surrealdb_username,
|
||||||
@@ -28,38 +28,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
(
|
(
|
||||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
vec![
|
vec![
|
||||||
PermissionsEnum::ReadListUsers,
|
// Only Administrator permission - grants access to everything
|
||||||
PermissionsEnum::ReadDetailUsers,
|
PermissionsEnum::Administrator,
|
||||||
PermissionsEnum::CreateUsers,
|
|
||||||
PermissionsEnum::DeleteUsers,
|
|
||||||
PermissionsEnum::UpdateUsers,
|
|
||||||
PermissionsEnum::ActivateUsers,
|
|
||||||
PermissionsEnum::ReadListRoles,
|
|
||||||
PermissionsEnum::ReadDetailRoles,
|
|
||||||
PermissionsEnum::CreateRoles,
|
|
||||||
PermissionsEnum::DeleteRoles,
|
|
||||||
PermissionsEnum::UpdateRoles,
|
|
||||||
PermissionsEnum::ReadListPermissions,
|
|
||||||
PermissionsEnum::ReadDetailPermissions,
|
|
||||||
PermissionsEnum::CreatePermissions,
|
|
||||||
PermissionsEnum::DeletePermissions,
|
|
||||||
PermissionsEnum::UpdatePermissions,
|
|
||||||
PermissionsEnum::CreateGachaClaims,
|
|
||||||
PermissionsEnum::ReadDetailGachaClaims,
|
|
||||||
PermissionsEnum::ReadListGachaItems,
|
|
||||||
PermissionsEnum::ReadDetailGachaItems,
|
|
||||||
PermissionsEnum::CreateGachaItems,
|
|
||||||
PermissionsEnum::DeleteGachaItems,
|
|
||||||
PermissionsEnum::UpdateGachaItems,
|
|
||||||
PermissionsEnum::ReadDetailGachaRolls,
|
|
||||||
PermissionsEnum::CreateGachaRolls,
|
|
||||||
PermissionsEnum::ExecuteGachaRolls,
|
|
||||||
PermissionsEnum::ReadListMentors,
|
|
||||||
PermissionsEnum::ReadDetailMentors,
|
|
||||||
PermissionsEnum::RegisterMentors,
|
|
||||||
PermissionsEnum::UpdateMentors,
|
|
||||||
PermissionsEnum::VerifyMentors,
|
|
||||||
PermissionsEnum::DeleteMentors,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -100,13 +70,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
(
|
(
|
||||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
vec![
|
vec![
|
||||||
|
// Staff should be able to list roles and permissions in tests
|
||||||
|
PermissionsEnum::ReadListRoles,
|
||||||
|
PermissionsEnum::ReadListPermissions,
|
||||||
PermissionsEnum::ReadListUsers,
|
PermissionsEnum::ReadListUsers,
|
||||||
PermissionsEnum::ReadListMentors,
|
PermissionsEnum::ReadListMentors,
|
||||||
PermissionsEnum::ReadDetailUsers,
|
PermissionsEnum::ReadDetailUsers,
|
||||||
PermissionsEnum::ActivateUsers,
|
PermissionsEnum::ActivateUsers,
|
||||||
PermissionsEnum::ReadListRoles,
|
|
||||||
PermissionsEnum::ReadDetailRoles,
|
PermissionsEnum::ReadDetailRoles,
|
||||||
PermissionsEnum::ReadListPermissions,
|
|
||||||
PermissionsEnum::ReadDetailPermissions,
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
PermissionsEnum::ReadListGachaItems,
|
PermissionsEnum::ReadListGachaItems,
|
||||||
PermissionsEnum::ReadDetailGachaItems,
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
@@ -115,6 +86,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
PermissionsEnum::ReadDetailGachaRolls,
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
PermissionsEnum::CreateGachaRolls,
|
PermissionsEnum::CreateGachaRolls,
|
||||||
PermissionsEnum::ExecuteGachaRolls,
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
PermissionsEnum::ManageAllTeams,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -127,7 +99,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
for (role_id, permissions) in roles_permissions {
|
for (role_id, permissions) in roles_permissions {
|
||||||
let permission_refs: Vec<_> = permissions
|
let permission_refs: Vec<_> = permissions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|perm| make_thing("app_permissions", perm.id()))
|
.map(|perm| make_thing("app_permissions", &perm.id()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ use std::error::Error;
|
|||||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::enviroment::ENV;
|
let env = &imphnen_libs::environment::ENV;
|
||||||
use surrealdb::engine::any;
|
use surrealdb::engine::any;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
@@ -36,6 +36,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
"User",
|
"User",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"testuser1-id",
|
||||||
|
"testuser1@example.com",
|
||||||
|
"Test User 1",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"testuser2-id",
|
||||||
|
"testuser2@example.com",
|
||||||
|
"Test User 2",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"testuser3-id",
|
||||||
|
"testuser3@example.com",
|
||||||
|
"Test User 3",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (id, email, fullname, role_id) in users {
|
for (id, email, fullname, role_id) in users {
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
run_seed("seed_roles_permissions")?;
|
run_seed("seed_roles_permissions")?;
|
||||||
run_seed("seed_users")?;
|
run_seed("seed_users")?;
|
||||||
run_seed("seed_events")?;
|
run_seed("seed_events")?;
|
||||||
|
run_seed("seed_hackathons")?;
|
||||||
run_seed("seed_gacha_rolls")?;
|
run_seed("seed_gacha_rolls")?;
|
||||||
run_seed("seed_mentor_user")?;
|
run_seed("seed_mentor_user")?;
|
||||||
|
run_seed("seed_test_data")?;
|
||||||
println!("\n✅ All seeding completed successfully.");
|
println!("\n✅ All seeding completed successfully.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
pub mod v1;
|
pub mod v1;
|
||||||
pub use v1::*;
|
|
||||||
|
pub use v1::landing;
|
||||||
|
pub use v1::landing::events;
|
||||||
|
pub use v1::landing::testimonials;
|
||||||
|
pub use v1::landing::events::events_public_routes;
|
||||||
|
pub use v1::landing::events::events_protected_routes;
|
||||||
|
pub use v1::landing::testimonials::testimonials_public_routes;
|
||||||
|
pub use v1::landing::testimonials::testimonials_protected_routes;
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ use super::{
|
|||||||
};
|
};
|
||||||
use axum::extract::{Path, Query};
|
use axum::extract::{Path, Query};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Extension, Json};
|
use axum::{Extension, http::HeaderMap};
|
||||||
use imphnen_libs::{
|
use imphnen_libs::{
|
||||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
ResponseSuccessDto,
|
ResponseSuccessDto, ValidatedJson,
|
||||||
};
|
};
|
||||||
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -26,7 +27,7 @@ use imphnen_libs::{
|
|||||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
(status = 200, description = "[PUBLIC] Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||||
),
|
),
|
||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
@@ -44,7 +45,7 @@ pub async fn get_event_list(
|
|||||||
("id" = String, Path, description = "Event ID")
|
("id" = String, Path, description = "Event ID")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
@@ -63,15 +64,18 @@ pub async fn get_event_by_id(
|
|||||||
path = "/v1/cms/landing/events/create",
|
path = "/v1/cms/landing/events/create",
|
||||||
request_body = EventsCreateRequestDto,
|
request_body = EventsCreateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "Create new event", body = MessageResponseDto)
|
(status = 201, description = "[ADMIN] Create new event", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn post_create_event(
|
pub async fn post_create_event(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Json(payload): Json<EventsCreateRequestDto>,
|
Extension(state): Extension<AppState>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
EventsService::create_event(&state, payload).await
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
EventsService::create_event(&state, payload).await
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -85,16 +89,19 @@ pub async fn post_create_event(
|
|||||||
),
|
),
|
||||||
request_body = EventsUpdateRequestDto,
|
request_body = EventsUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Update event", body = MessageResponseDto)
|
(status = 200, description = "[ADMIN] Update event", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn patch_update_event(
|
pub async fn patch_update_event(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<EventsUpdateRequestDto>,
|
Path(id): Path<String>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
EventsService::update_event(&state, id, payload).await
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
EventsService::update_event(&state, id, payload).await
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -107,13 +114,16 @@ pub async fn patch_update_event(
|
|||||||
("id" = String, Path, description = "Event ID")
|
("id" = String, Path, description = "Event ID")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Soft delete event", body = MessageResponseDto)
|
(status = 200, description = "[ADMIN] Soft delete event", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn delete_event(
|
pub async fn delete_event(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Path(id): Path<String>,
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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 chrono::{DateTime, Utc};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use validator::Validate;
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
// Custom URL validator that ensures valid HTTP/HTTPS URLs
|
||||||
|
pub fn validate_url(url: &str) -> Result<(), ValidationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap();
|
||||||
|
}
|
||||||
|
if VALID_URL_REGEX.is_match(url) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("invalid_url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validator for future dates
|
||||||
|
pub fn validate_future_date(end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||||
|
let now = Utc::now();
|
||||||
|
if end_date > &now {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("future_date"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validator for event date ranges (for combined validation)
|
||||||
|
pub fn validate_date_range(start_date: &DateTime<Utc>, end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||||
|
if start_date <= end_date {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("date_range"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct EventsCreateRequestDto {
|
pub struct EventsCreateRequestDto {
|
||||||
#[validate(length(min = 1, message = "Name is required"))]
|
#[validate(length(min = 1, max = 100, message = "Name must be between 1 and 100 characters"))]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
#[validate(length(min = 1, message = "Description is required"))]
|
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
|
||||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
#[validate(custom(
|
||||||
|
function = "validate_url",
|
||||||
|
message = "Detail link must be a valid HTTP/HTTPS URL"
|
||||||
|
))]
|
||||||
pub detail_link: String,
|
pub detail_link: String,
|
||||||
|
|
||||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
#[validate(range(min = 0.0, max = 1_000_000.0, message = "Price must be between 0 and 1,000,000"))]
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
|
|
||||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_future_date",
|
||||||
|
message = "End date must be in the future"
|
||||||
|
))]
|
||||||
pub end_date: DateTime<Utc>,
|
pub end_date: DateTime<Utc>,
|
||||||
|
|
||||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
pub start_date: DateTime<Utc>,
|
pub start_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
#[validate(length(max = 200, message = "Location name cannot exceed 200 characters"))]
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
pub is_online: bool,
|
pub is_online: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ impl<'a> EventsRepository<'a> {
|
|||||||
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events.to_string());
|
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events);
|
||||||
info!(query = %query_str, "Executing SurrealDB query");
|
info!(query = %query_str, "Executing SurrealDB query");
|
||||||
let record: Option<EventsSchema> = db
|
let record: Option<EventsSchema> = db
|
||||||
.create(ResourceEnum::Events.to_string())
|
.create(ResourceEnum::Events.to_string())
|
||||||
|
|||||||
@@ -9,36 +9,30 @@ pub mod events_repository;
|
|||||||
pub mod events_schema;
|
pub mod events_schema;
|
||||||
pub mod events_service;
|
pub mod events_service;
|
||||||
|
|
||||||
pub use events_controller::*;
|
// Export only the necessary public items
|
||||||
pub use events_dto::*;
|
pub use events_dto::{
|
||||||
pub use events_repository::*;
|
EventsCreateRequestDto,
|
||||||
pub use events_schema::*;
|
EventsUpdateRequestDto,
|
||||||
pub use events_service::*;
|
EventsListItemDto,
|
||||||
|
EventsDetailItemDto,
|
||||||
|
};
|
||||||
|
pub use events_controller::{
|
||||||
|
get_event_list,
|
||||||
|
get_event_by_id,
|
||||||
|
post_create_event,
|
||||||
|
patch_update_event,
|
||||||
|
delete_event,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn events_public_routes() -> Router {
|
pub fn events_public_routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route("/cms/landing/events", get(get_event_list))
|
||||||
"/cms/landing/events",
|
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||||
get(events_controller::get_event_list),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/cms/landing/events/detail/{id}",
|
|
||||||
get(events_controller::get_event_by_id),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn events_protected_routes() -> Router {
|
pub fn events_protected_routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route("/cms/landing/events/create", post(post_create_event))
|
||||||
"/cms/landing/events/create",
|
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||||
post(events_controller::post_create_event),
|
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/cms/landing/events/update/{id}",
|
|
||||||
patch(events_controller::patch_update_event),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/cms/landing/events/delete/{id}",
|
|
||||||
delete(events_controller::delete_event),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod testimonials;
|
pub mod testimonials;
|
||||||
|
|
||||||
pub use events::*;
|
pub use events::events_public_routes;
|
||||||
pub use testimonials::*;
|
pub use events::events_protected_routes;
|
||||||
|
pub use testimonials::testimonials_public_routes;
|
||||||
|
pub use testimonials::testimonials_protected_routes;
|
||||||
|
|||||||
@@ -9,36 +9,30 @@ pub mod testimonials_repository;
|
|||||||
pub mod testimonials_schema;
|
pub mod testimonials_schema;
|
||||||
pub mod testimonials_service;
|
pub mod testimonials_service;
|
||||||
|
|
||||||
pub use testimonials_controller::*;
|
// Export only the necessary public items
|
||||||
pub use testimonials_dto::*;
|
pub use testimonials_dto::{
|
||||||
pub use testimonials_repository::*;
|
TestimonialsCreateRequestDto,
|
||||||
pub use testimonials_schema::*;
|
TestimonialsUpdateRequestDto,
|
||||||
pub use testimonials_service::*;
|
TestimonialsListItemDto,
|
||||||
|
TestimonialsDetailItemDto,
|
||||||
|
};
|
||||||
|
pub use testimonials_controller::{
|
||||||
|
get_testimonial_list,
|
||||||
|
get_testimonial_by_id,
|
||||||
|
post_create_testimonial,
|
||||||
|
patch_update_testimonial,
|
||||||
|
delete_testimonial,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn testimonials_public_routes() -> Router {
|
pub fn testimonials_public_routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
||||||
"/cms/landing/testimonials",
|
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
||||||
get(testimonials_controller::get_testimonial_list),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/cms/landing/testimonials/detail/{id}",
|
|
||||||
get(testimonials_controller::get_testimonial_by_id),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn testimonials_protected_routes() -> Router {
|
pub fn testimonials_protected_routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
||||||
"/cms/landing/testimonials/create",
|
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
||||||
post(testimonials_controller::post_create_testimonial),
|
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/cms/landing/testimonials/update/{id}",
|
|
||||||
patch(testimonials_controller::patch_update_testimonial),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/cms/landing/testimonials/delete/{id}",
|
|
||||||
delete(testimonials_controller::delete_testimonial),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ use super::{
|
|||||||
};
|
};
|
||||||
use axum::extract::{Path, Query};
|
use axum::extract::{Path, Query};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Extension, Json};
|
use axum::{Extension, http::HeaderMap};
|
||||||
use imphnen_iam::UsersDetailQueryDto;
|
use imphnen_iam::{UsersDetailQueryDto, require_auth};
|
||||||
use imphnen_libs::{
|
use imphnen_libs::{
|
||||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
ResponseSuccessDto,
|
ResponseSuccessDto, ValidatedJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -27,7 +27,7 @@ use imphnen_libs::{
|
|||||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
(status = 200, description = "[PUBLIC] Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||||
),
|
),
|
||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
@@ -45,7 +45,7 @@ pub async fn get_testimonial_list(
|
|||||||
("id" = String, Path, description = "Testimonial ID")
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
@@ -64,16 +64,19 @@ pub async fn get_testimonial_by_id(
|
|||||||
path = "/v1/cms/landing/testimonials/create",
|
path = "/v1/cms/landing/testimonials/create",
|
||||||
request_body = TestimonialsCreateRequestDto,
|
request_body = TestimonialsCreateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "Create new testimonial", body = MessageResponseDto)
|
(status = 201, description = "[USER] Create new testimonial", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn post_create_testimonial(
|
pub async fn post_create_testimonial(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<TestimonialsCreateRequestDto>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
require_auth!(headers, state, {
|
||||||
|
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -87,18 +90,20 @@ pub async fn post_create_testimonial(
|
|||||||
),
|
),
|
||||||
request_body = TestimonialsUpdateRequestDto,
|
request_body = TestimonialsUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Update testimonial", body = MessageResponseDto)
|
(status = 200, description = "[USER] Update testimonial", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn patch_update_testimonial(
|
pub async fn patch_update_testimonial(
|
||||||
Path(id): Path<String>,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Path(id): Path<String>,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<TestimonialsUpdateRequestDto>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user)
|
require_auth!(headers, state, {
|
||||||
.await
|
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -111,14 +116,17 @@ pub async fn patch_update_testimonial(
|
|||||||
("id" = String, Path, description = "Testimonial ID")
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Soft delete testimonial", body = MessageResponseDto)
|
(status = 200, description = "[USER] Soft delete testimonial", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn delete_testimonial(
|
pub async fn delete_testimonial(
|
||||||
Extension(state): Extension<AppState>,
|
headers: HeaderMap,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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 serde::{Deserialize, Serialize};
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use validator::Validate;
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
// Custom validator for content length and format
|
||||||
|
pub fn validate_testimonial_content(content: &str) -> Result<(), ValidationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref CONTENT_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9\s.,!?'-]+$").unwrap();
|
||||||
|
}
|
||||||
|
if CONTENT_REGEX.is_match(content) && content.len() <= 1000 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("invalid_content"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct TestimonialsCreateRequestDto {
|
pub struct TestimonialsCreateRequestDto {
|
||||||
#[validate(length(min = 1, message = "Role is required"))]
|
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||||
pub role: String,
|
pub role: String,
|
||||||
|
|
||||||
#[validate(length(
|
#[validate(length(
|
||||||
min = 1,
|
min = 1,
|
||||||
max = 500,
|
max = 1000,
|
||||||
message = "Content must be between 1 and 500 characters"
|
message = "Content must be between 1 and 1000 characters"
|
||||||
|
))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_testimonial_content",
|
||||||
|
message = "Content contains invalid characters or is too long"
|
||||||
))]
|
))]
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct TestimonialsUpdateRequestDto {
|
pub struct TestimonialsUpdateRequestDto {
|
||||||
#[validate(length(min = 1, message = "Role is required"))]
|
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||||
pub role: String,
|
pub role: String,
|
||||||
|
|
||||||
#[validate(length(
|
#[validate(length(
|
||||||
min = 1,
|
min = 1,
|
||||||
max = 500,
|
max = 1000,
|
||||||
message = "Content must be between 1 and 500 characters"
|
message = "Content must be between 1 and 1000 characters"
|
||||||
|
))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_testimonial_content",
|
||||||
|
message = "Content contains invalid characters or is too long"
|
||||||
))]
|
))]
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,14 @@ impl<'a> TestimonialsRepository<'a> {
|
|||||||
) -> Result<TestimonialsQueryDto> {
|
) -> Result<TestimonialsQueryDto> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
|
// Extract raw id if id is a thing string
|
||||||
|
let raw_id = if id.contains(':') {
|
||||||
|
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
|
||||||
|
} else {
|
||||||
|
id
|
||||||
|
};
|
||||||
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||||
.with_id(&id)
|
.with_id(&raw_id)
|
||||||
.with_condition("is_deleted = false")
|
.with_condition("is_deleted = false")
|
||||||
.with_select_fields(vec!["*", "user.* as user"]);
|
.with_select_fields(vec!["*", "user.* as user"]);
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
|
|||||||
@@ -5,80 +5,87 @@ use surrealdb::Uuid;
|
|||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
use super::testimonials_dto::{
|
use super::testimonials_dto::{
|
||||||
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct TestimonialsSchema {
|
pub struct TestimonialsSchema {
|
||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub user: Thing,
|
pub user: Thing,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TestimonialsSchema {
|
impl Default for TestimonialsSchema {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: make_thing(
|
id: make_thing(
|
||||||
&ResourceEnum::Testimonials.to_string(),
|
&ResourceEnum::Testimonials.to_string(),
|
||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
user: make_thing(
|
user: make_thing(
|
||||||
&ResourceEnum::Users.to_string(),
|
&ResourceEnum::Users.to_string(),
|
||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
role: String::new(),
|
role: String::new(),
|
||||||
content: String::new(),
|
content: String::new(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
updated_at: get_iso_date(),
|
updated_at: get_iso_date(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestimonialsSchema {
|
impl TestimonialsSchema {
|
||||||
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
user: dto.user.id,
|
user: dto.user.id,
|
||||||
role: dto.role,
|
role: dto.role,
|
||||||
content: dto.content,
|
content: dto.content,
|
||||||
is_deleted: dto.is_deleted,
|
is_deleted: dto.is_deleted,
|
||||||
created_at: dto.created_at,
|
created_at: dto.created_at,
|
||||||
updated_at: dto.updated_at,
|
updated_at: dto.updated_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
|
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: make_thing(
|
id: make_thing(
|
||||||
&ResourceEnum::Testimonials.to_string(),
|
&ResourceEnum::Testimonials.to_string(),
|
||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
user: user_id.clone(),
|
user: user_id.clone(),
|
||||||
role: payload.role,
|
role: payload.role,
|
||||||
content: payload.content,
|
content: payload.content,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
created_at: get_iso_date(),
|
created_at: get_iso_date(),
|
||||||
updated_at: get_iso_date(),
|
updated_at: get_iso_date(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(
|
||||||
payload: TestimonialsUpdateRequestDto,
|
payload: TestimonialsUpdateRequestDto,
|
||||||
id: String,
|
id: String,
|
||||||
user_id: &Thing,
|
user_id: &Thing,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
// Normalize id: accept either raw id (uuid) or Thing-formatted id like "table:⟨id⟩"
|
||||||
id: make_thing(&ResourceEnum::Testimonials.to_string(), &id),
|
let raw_id = if id.contains(':') {
|
||||||
role: payload.role,
|
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
|
||||||
content: payload.content,
|
} else {
|
||||||
updated_at: get_iso_date(),
|
id
|
||||||
user: user_id.clone(),
|
};
|
||||||
..Default::default()
|
|
||||||
}
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
pub mod landing;
|
pub mod landing;
|
||||||
|
|
||||||
pub use landing::*;
|
pub use landing::events;
|
||||||
|
pub use landing::testimonials;
|
||||||
|
pub use landing::events::events_public_routes;
|
||||||
|
pub use landing::events::events_protected_routes;
|
||||||
|
pub use landing::testimonials::testimonials_public_routes;
|
||||||
|
pub use landing::testimonials::testimonials_protected_routes;
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ imphnen-libs.workspace = true
|
|||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
imphnen-entities.workspace = true
|
imphnen-entities.workspace = true
|
||||||
imphnen-iam.workspace = true
|
imphnen-iam.workspace = true
|
||||||
|
imphnen-middleware.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
|
|||||||
@@ -1,2 +1,12 @@
|
|||||||
pub mod v1;
|
pub mod v1;
|
||||||
pub use v1::*;
|
|
||||||
|
// Explicitly export only what's needed from v1
|
||||||
|
pub use v1::dimentorin_router;
|
||||||
|
pub use v1::mentors::mentors_router;
|
||||||
|
pub use v1::sessions::{
|
||||||
|
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||||
|
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||||
|
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||||
|
AvailabilitySlotDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,30 +4,30 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
||||||
use ::axum::{
|
use ::axum::{
|
||||||
extract::{Extension, Json, Path, Query},
|
extract::{Extension, Path, Query},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::{IntoResponse, Response},
|
response::Response,
|
||||||
};
|
};
|
||||||
use imphnen_entities::*;
|
use imphnen_entities::MetaRequestDto;
|
||||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
use imphnen_utils::extract_email;
|
use imphnen_utils::extract_email;
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/v1/mentors/register",
|
path = "/v1/mentors/create",
|
||||||
request_body = MentorUserRegisterRequestDto,
|
request_body = MentorUserRegisterRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Mentor registered successfully", body = MentorRegisterResponseDto),
|
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||||
(status = 400, description = "Bad request - validation error"),
|
(status = 400, description = "[PUBLIC] Bad request - validation error"),
|
||||||
(status = 409, description = "Conflict - user already has mentor profile"),
|
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[PUBLIC] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors"
|
tag = "Mentors"
|
||||||
)]
|
)]
|
||||||
pub async fn post_register_mentor(
|
pub async fn post_register_mentor(
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Json(dto): Json<MentorUserRegisterRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
MentorsService::register_mentor(&app_state, dto).await
|
MentorsService::register_mentor(&app_state, dto).await
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ pub async fn post_register_mentor(
|
|||||||
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get list of mentors", body = Vec<MentorListResponseDto>),
|
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors",
|
tag = "Mentors",
|
||||||
security(
|
security(
|
||||||
@@ -56,16 +56,9 @@ pub async fn get_mentor_list(
|
|||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
|
||||||
headers,
|
MentorsService::get_mentor_list(&app_state, meta).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::ReadListMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::get_mentor_list(&app_state, meta).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -75,9 +68,9 @@ pub async fn get_mentor_list(
|
|||||||
("id" = String, Path, description = "Mentor ID")
|
("id" = String, Path, description = "Mentor ID")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get mentor by ID", body = MentorDetailResponseDto),
|
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
|
||||||
(status = 404, description = "Mentor not found"),
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors",
|
tag = "Mentors",
|
||||||
security(
|
security(
|
||||||
@@ -89,16 +82,9 @@ pub async fn get_mentor_by_id(
|
|||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
|
||||||
headers,
|
MentorsService::get_mentor_by_id(&app_state, &id).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::ReadDetailMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::get_mentor_by_id(&app_state, &id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -109,10 +95,10 @@ pub async fn get_mentor_by_id(
|
|||||||
),
|
),
|
||||||
request_body = MentorUpdateRequestDto,
|
request_body = MentorUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Mentor updated successfully", body = MentorDetailResponseDto),
|
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
|
||||||
(status = 400, description = "Bad request - validation error"),
|
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||||
(status = 404, description = "Mentor not found"),
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors - Admin",
|
tag = "Mentors - Admin",
|
||||||
security(
|
security(
|
||||||
@@ -123,18 +109,11 @@ pub async fn put_update_mentor(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<MentorUpdateRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
|
||||||
headers,
|
MentorsService::update_mentor(&app_state, &id, dto).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::UpdateMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::update_mentor(&app_state, &id, dto).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -144,9 +123,9 @@ pub async fn put_update_mentor(
|
|||||||
("id" = String, Path, description = "Mentor ID")
|
("id" = String, Path, description = "Mentor ID")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Mentor deleted successfully"),
|
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
|
||||||
(status = 404, description = "Mentor not found"),
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors - Admin",
|
tag = "Mentors - Admin",
|
||||||
security(
|
security(
|
||||||
@@ -158,16 +137,9 @@ pub async fn delete_mentor(
|
|||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
|
||||||
headers,
|
MentorsService::delete_mentor(&app_state, &id).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::DeleteMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::delete_mentor(&app_state, &id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -178,10 +150,10 @@ pub async fn delete_mentor(
|
|||||||
),
|
),
|
||||||
request_body = MentorVerifyRequestDto,
|
request_body = MentorVerifyRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Mentor verified successfully", body = MentorDetailResponseDto),
|
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
|
||||||
(status = 400, description = "Bad request - validation error"),
|
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||||
(status = 404, description = "Mentor not found"),
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors - Admin",
|
tag = "Mentors - Admin",
|
||||||
security(
|
security(
|
||||||
@@ -192,28 +164,21 @@ pub async fn put_verify_mentor(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<MentorVerifyRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
|
||||||
headers,
|
MentorsService::verify_mentor(&app_state, &id, dto).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::VerifyMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::verify_mentor(&app_state, &id, dto).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/v1/mentors/me",
|
path = "/v1/mentors/me",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Current user's mentor profile", body = MentorDetailResponseDto),
|
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
|
||||||
(status = 401, description = "Unauthorized - invalid token"),
|
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||||
(status = 403, description = "Mentor profile not found for current user"),
|
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[MENTOR] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors",
|
tag = "Mentors",
|
||||||
security(
|
security(
|
||||||
@@ -224,43 +189,30 @@ pub async fn get_mentor_me(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||||
headers.clone(),
|
let email = match extract_email(&headers) {
|
||||||
Extension(app_state),
|
Some(email) => email,
|
||||||
vec![PermissionsEnum::ReadOwnMentorProfile],
|
None => {
|
||||||
)
|
return imphnen_utils::common_response(
|
||||||
.await
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
{
|
"Token tidak valid",
|
||||||
Ok((_user, app_state)) => {
|
);
|
||||||
let email = match extract_email(&headers) {
|
}
|
||||||
Some(email) => email,
|
};
|
||||||
None => {
|
MentorsService::get_mentor_me(&app_state, &email).await
|
||||||
return (
|
})
|
||||||
axum::http::StatusCode::UNAUTHORIZED,
|
|
||||||
Json(json!({
|
|
||||||
"error": "Unauthorized",
|
|
||||||
"message": "Token tidak valid"
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MentorsService::get_mentor_me(&app_state, &email).await
|
|
||||||
}
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
put,
|
put,
|
||||||
path = "/v1/mentors/update/me",
|
path = "/v1/mentors/me/update",
|
||||||
request_body = MentorUpdateRequestDto,
|
request_body = MentorUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Mentor profile updated successfully", body = MentorDetailResponseDto),
|
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||||
(status = 400, description = "Bad request - validation error"),
|
(status = 400, description = "[MENTOR] Bad request - validation error"),
|
||||||
(status = 401, description = "Unauthorized - invalid token"),
|
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||||
(status = 404, description = "Mentor profile not found"),
|
(status = 404, description = "[MENTOR] Mentor profile not found"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[MENTOR] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors",
|
tag = "Mentors",
|
||||||
security(
|
security(
|
||||||
@@ -270,36 +222,27 @@ pub async fn get_mentor_me(
|
|||||||
pub async fn put_update_mentor_me(
|
pub async fn put_update_mentor_me(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Json(dto): Json<MentorUpdateRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||||
headers.clone(),
|
let email = match extract_email(&headers) {
|
||||||
Extension(app_state),
|
Some(email) => email,
|
||||||
vec![PermissionsEnum::UpdateOwnMentorProfile],
|
None => {
|
||||||
)
|
return imphnen_utils::common_response(
|
||||||
.await
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
{
|
"Token tidak valid",
|
||||||
Ok((_user, app_state)) => {
|
);
|
||||||
let email = match extract_email(&headers) {
|
}
|
||||||
Some(email) => email,
|
};
|
||||||
None => {
|
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||||
return imphnen_utils::common_response(
|
})
|
||||||
axum::http::StatusCode::UNAUTHORIZED,
|
|
||||||
"Token tidak valid",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
|
||||||
}
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
put,
|
put,
|
||||||
path = "/v1/mentors/update",
|
path = "/v1/mentors/update",
|
||||||
request_body = MentorUpdateRequestDto,
|
request_body = MentorUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 400, description = "Bad request - Mentor ID is required for update"),
|
(status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
|
||||||
),
|
),
|
||||||
tag = "Mentors - Admin"
|
tag = "Mentors - Admin"
|
||||||
)]
|
)]
|
||||||
@@ -312,12 +255,12 @@ pub async fn put_update_mentor_no_id() -> Response {
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/v1/mentors/status",
|
path = "/v1/mentors/me/status",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Mentor application status", body = String),
|
(status = 200, description = "[MENTOR] Mentor application status", body = String),
|
||||||
(status = 401, description = "Unauthorized - invalid token"),
|
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||||
(status = 403, description = "No mentor application found for current user"),
|
(status = 403, description = "[MENTOR] No mentor application found for current user"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "[MENTOR] Internal server error")
|
||||||
),
|
),
|
||||||
tag = "Mentors",
|
tag = "Mentors",
|
||||||
security(
|
security(
|
||||||
@@ -328,29 +271,16 @@ pub async fn get_mentor_status(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||||
headers.clone(),
|
let email = match extract_email(&headers) {
|
||||||
Extension(app_state),
|
Some(email) => email,
|
||||||
vec![PermissionsEnum::ReadOwnMentorStatus],
|
None => {
|
||||||
)
|
return imphnen_utils::common_response(
|
||||||
.await
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
{
|
"Token tidak valid",
|
||||||
Ok((_user, app_state)) => {
|
);
|
||||||
let email = match extract_email(&headers) {
|
}
|
||||||
Some(email) => email,
|
};
|
||||||
None => {
|
MentorsService::get_mentor_status(&app_state, &email).await
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ pub struct MentorUserRegisterRequestDto {
|
|||||||
message = "Password must have at least 8 characters"
|
message = "Password must have at least 8 characters"
|
||||||
))]
|
))]
|
||||||
#[validate(custom(
|
#[validate(custom(
|
||||||
function = "imphnen_iam::auth_dto::validate_password_complexity",
|
function = "imphnen_iam::v1::auth::auth_dto::validate_password_complexity",
|
||||||
message = "Password must include uppercase, lowercase, number, and special character"
|
message = "Password must include uppercase, lowercase, number, and special character"
|
||||||
))]
|
))]
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use anyhow::{Result, bail};
|
|||||||
use imphnen_iam::{get_id, make_thing};
|
use imphnen_iam::{get_id, make_thing};
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
use crate::v1::mentors::{MentorDetailWithUserDto, MentorInsertDto, MentorSchema};
|
use crate::v1::mentors::mentors_dto::MentorDetailWithUserDto;
|
||||||
|
use crate::v1::mentors::{MentorInsertDto, MentorSchema};
|
||||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||||
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
|
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
@@ -122,8 +123,18 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
) -> Result<MentorDetailWithUserDto> {
|
) -> Result<MentorDetailWithUserDto> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
|
||||||
.with_id(get_id(id)?.1)
|
// Validate ID format first
|
||||||
|
let mentor_id = match get_id(id) {
|
||||||
|
Ok((_, id_str)) => id_str,
|
||||||
|
Err(_) => bail!("Invalid mentor ID format"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||||
|
|
||||||
|
// Build query with proper ID binding
|
||||||
|
let mut builder = DetailQueryBuilder::new(mentors_table.clone())
|
||||||
|
.with_id(mentor_id) // Use the extracted ID string
|
||||||
.with_select_fields(vec![
|
.with_select_fields(vec![
|
||||||
"id",
|
"id",
|
||||||
"user_id",
|
"user_id",
|
||||||
@@ -151,20 +162,23 @@ impl<'a> MentorsRepository<'a> {
|
|||||||
|
|
||||||
let sql = builder.build();
|
let sql = builder.build();
|
||||||
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
|
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
|
||||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
|
||||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
let mentor_opt: Option<MentorDetailWithUserDto> = builder
|
||||||
|
.apply_bindings(db.query(sql))
|
||||||
|
.await?
|
||||||
|
.take(0)?;
|
||||||
|
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
== "development"
|
== "development"
|
||||||
{
|
{
|
||||||
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(mentor) = mentor_opt else {
|
let Some(mentor) = mentor_opt else {
|
||||||
bail!("Mentor not found in database");
|
bail!("Mentor not found");
|
||||||
};
|
};
|
||||||
if mentor.is_deleted && !include_deleted {
|
|
||||||
bail!("Mentor has been deleted");
|
|
||||||
}
|
|
||||||
Ok(mentor)
|
Ok(mentor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ use crate::v1::mentors::{
|
|||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use imphnen_entities::{
|
use imphnen_entities::{
|
||||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
};
|
};
|
||||||
|
use imphnen_libs::AppState;
|
||||||
use imphnen_iam::{
|
use imphnen_iam::{
|
||||||
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||||
};
|
};
|
||||||
@@ -33,9 +34,9 @@ impl MentorsService {
|
|||||||
let user_repo = UsersRepository::new(state);
|
let user_repo = UsersRepository::new(state);
|
||||||
let mentor_repo = MentorsRepository::new(state);
|
let mentor_repo = MentorsRepository::new(state);
|
||||||
let role_repo = RolesRepository::new(state);
|
let role_repo = RolesRepository::new(state);
|
||||||
let auth_repo = AuthRepository::new(state);
|
let auth_repo = AuthRepository::new(state.surrealdb_mem.clone());
|
||||||
|
|
||||||
let user_email = dto.email.clone();
|
let user_email = &dto.email;
|
||||||
let mut _user_to_update: Option<UsersSchema> = None;
|
let mut _user_to_update: Option<UsersSchema> = None;
|
||||||
|
|
||||||
let existing_user_result =
|
let existing_user_result =
|
||||||
@@ -56,7 +57,7 @@ impl MentorsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut user_schema = UsersSchema::from(user_detail_query_dto.clone());
|
let mut user_schema = UsersSchema::from(user_detail_query_dto);
|
||||||
|
|
||||||
user_schema.fullname = dto.fullname.clone();
|
user_schema.fullname = dto.fullname.clone();
|
||||||
user_schema.phone_number = dto.phone_number.clone();
|
user_schema.phone_number = dto.phone_number.clone();
|
||||||
@@ -99,7 +100,7 @@ impl MentorsService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
user_schema.role =
|
user_schema.role =
|
||||||
imphnen_utils::make_thing(&ResourceEnum::Roles.to_string(), &mentor_role.id);
|
imphnen_utils::make_thing_from_enum(ResourceEnum::Roles, &mentor_role.id);
|
||||||
user_schema.is_active = false;
|
user_schema.is_active = false;
|
||||||
|
|
||||||
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
|
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
|
||||||
@@ -139,14 +140,14 @@ impl MentorsService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let new_user_schema = UsersSchema {
|
let new_user_schema = UsersSchema {
|
||||||
id: imphnen_utils::make_thing(
|
id: imphnen_utils::make_thing_from_enum(
|
||||||
&ResourceEnum::Users.to_string(),
|
ResourceEnum::Users,
|
||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
email: dto.email.clone(),
|
email: dto.email,
|
||||||
fullname: dto.fullname.clone(),
|
fullname: dto.fullname,
|
||||||
password: hashed_password,
|
password: hashed_password,
|
||||||
phone_number: dto.phone_number.clone(),
|
phone_number: dto.phone_number,
|
||||||
// Store personal data from identity_and_verification in user
|
// Store personal data from identity_and_verification in user
|
||||||
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||||
gender: dto.identity_and_verification.gender.clone(),
|
gender: dto.identity_and_verification.gender.clone(),
|
||||||
@@ -161,8 +162,8 @@ impl MentorsService {
|
|||||||
portfolio_url: dto.professional_profile.portfolio_url.clone(),
|
portfolio_url: dto.professional_profile.portfolio_url.clone(),
|
||||||
created_at: imphnen_utils::get_iso_date(),
|
created_at: imphnen_utils::get_iso_date(),
|
||||||
updated_at: imphnen_utils::get_iso_date(),
|
updated_at: imphnen_utils::get_iso_date(),
|
||||||
role: imphnen_utils::make_thing(
|
role: imphnen_utils::make_thing_from_enum(
|
||||||
&ResourceEnum::Roles.to_string(),
|
ResourceEnum::Roles,
|
||||||
&mentor_role.id,
|
&mentor_role.id,
|
||||||
),
|
),
|
||||||
is_active: false,
|
is_active: false,
|
||||||
@@ -186,11 +187,11 @@ impl MentorsService {
|
|||||||
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
|
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
|
||||||
|
|
||||||
match auth_repo
|
match auth_repo
|
||||||
.query_store_otp(final_user_email.clone(), otp)
|
.query_store_otp(final_user_email.clone(), otp.clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let message = format!("your otp code is {otp}");
|
let message = format!("your otp code is {}", otp.code);
|
||||||
if let Err(_err) =
|
if let Err(_err) =
|
||||||
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
|
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,19 +9,50 @@ pub mod mentors_repository;
|
|||||||
pub mod mentors_schema;
|
pub mod mentors_schema;
|
||||||
pub mod mentors_service;
|
pub mod mentors_service;
|
||||||
|
|
||||||
pub use mentors_controller::*;
|
// Explicitly export only public controller functions and key types
|
||||||
pub use mentors_dto::*;
|
pub use mentors_controller::{
|
||||||
pub use mentors_repository::*;
|
post_register_mentor,
|
||||||
pub use mentors_schema::*;
|
get_mentor_list,
|
||||||
pub use mentors_service::*;
|
get_mentor_by_id,
|
||||||
|
put_update_mentor,
|
||||||
|
delete_mentor,
|
||||||
|
put_verify_mentor,
|
||||||
|
get_mentor_me,
|
||||||
|
put_update_mentor_me,
|
||||||
|
put_update_mentor_no_id,
|
||||||
|
get_mentor_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export key DTO types used across the API
|
||||||
|
pub use mentors_dto::{
|
||||||
|
MentorListResponseDto,
|
||||||
|
MentorDetailResponseDto,
|
||||||
|
MentorRegisterResponseDto,
|
||||||
|
MentorUpdateRequestDto,
|
||||||
|
MentorUserRegisterRequestDto,
|
||||||
|
MentorVerifyRequestDto,
|
||||||
|
MentorDetailQueryDto,
|
||||||
|
ProfessionalProfile,
|
||||||
|
MentoringLogistics,
|
||||||
|
MentoringRate,
|
||||||
|
IdentityAndVerification,
|
||||||
|
MentorInsertDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export service and repository for internal use
|
||||||
|
pub use mentors_service::MentorsService;
|
||||||
|
pub use mentors_repository::MentorsRepository;
|
||||||
|
|
||||||
|
// Export schema types for database interactions
|
||||||
|
pub use mentors_schema::MentorSchema;
|
||||||
|
|
||||||
pub fn mentors_router() -> Router {
|
pub fn mentors_router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(get_mentor_list))
|
.route("/", get(get_mentor_list))
|
||||||
.route("/register", post(post_register_mentor))
|
.route("/create", post(post_register_mentor))
|
||||||
.route("/me", get(get_mentor_me))
|
.route("/me", get(get_mentor_me))
|
||||||
.route("/update/me", put(put_update_mentor_me))
|
.route("/me/update", put(put_update_mentor_me))
|
||||||
.route("/status", get(get_mentor_status))
|
.route("/me/status", get(get_mentor_status))
|
||||||
.route("/detail/{id}", get(get_mentor_by_id))
|
.route("/detail/{id}", get(get_mentor_by_id))
|
||||||
.route("/update/{id}", put(put_update_mentor))
|
.route("/update/{id}", put(put_update_mentor))
|
||||||
.route("/update", put(put_update_mentor_no_id))
|
.route("/update", put(put_update_mentor_no_id))
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
|
||||||
pub mod mentors;
|
pub mod mentors;
|
||||||
|
pub mod sessions;
|
||||||
|
|
||||||
|
/// Creates the main Dimentorin router with all version 1 endpoints
|
||||||
|
/// Routes:
|
||||||
|
/// - /mentors -> mentors::mentors_router()
|
||||||
|
/// - /sessions -> sessions::sessions_router()
|
||||||
|
/// - /users/me/sessions -> sessions::get_my_sessions()
|
||||||
pub fn dimentorin_router() -> Router {
|
pub fn dimentorin_router() -> Router {
|
||||||
Router::new().nest("/mentors", mentors::mentors_router())
|
Router::new()
|
||||||
|
.nest("/mentors", mentors::mentors_router())
|
||||||
|
.merge(sessions::sessions_router())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicitly re-export key items for easier consumption
|
||||||
|
pub use mentors::mentors_router;
|
||||||
|
pub use mentors::MentorsService;
|
||||||
|
pub use mentors::MentorsRepository;
|
||||||
|
pub use mentors::MentorSchema;
|
||||||
|
|
||||||
|
pub use sessions::sessions_router;
|
||||||
|
pub use sessions::SessionsService;
|
||||||
|
pub use sessions::SessionsRepository;
|
||||||
|
pub use sessions::SessionSchema;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod sessions_controller;
|
||||||
|
pub mod sessions_dto;
|
||||||
|
pub mod sessions_repository;
|
||||||
|
pub mod sessions_schema;
|
||||||
|
pub mod sessions_service;
|
||||||
|
|
||||||
|
pub use sessions_controller::*;
|
||||||
|
pub use sessions_dto::*;
|
||||||
|
pub use sessions_repository::*;
|
||||||
|
pub use sessions_schema::*;
|
||||||
|
pub use sessions_service::*;
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
use super::{
|
||||||
|
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||||
|
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
|
||||||
|
SessionsService, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
extract::{Extension, Path, Query},
|
||||||
|
http::HeaderMap,
|
||||||
|
response::Response,
|
||||||
|
routing::{get, post, put},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use imphnen_utils::extract_email;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
post_book_session,
|
||||||
|
get_mentor_sessions,
|
||||||
|
get_mentor_availability,
|
||||||
|
put_update_session_status,
|
||||||
|
post_submit_feedback,
|
||||||
|
get_my_sessions,
|
||||||
|
),
|
||||||
|
components(schemas(
|
||||||
|
BookSessionRequestDto,
|
||||||
|
BookSessionResponseDto,
|
||||||
|
SessionListResponseDto,
|
||||||
|
super::SessionListItemDto,
|
||||||
|
MentorAvailabilityDto,
|
||||||
|
super::AvailabilitySlotDto,
|
||||||
|
UpdateSessionStatusRequestDto,
|
||||||
|
UpdateSessionStatusResponseDto,
|
||||||
|
SessionFeedbackRequestDto,
|
||||||
|
SessionFeedbackResponseDto,
|
||||||
|
)),
|
||||||
|
tags(
|
||||||
|
(name = "sessions", description = "Mentoring Sessions Management API")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct SessionsApiDoc;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Book Session
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/mentors/{id}/sessions/create",
|
||||||
|
tag = "sessions",
|
||||||
|
summary = "Book a mentoring session",
|
||||||
|
description = "Book a mentoring session with a specific mentor. Requires authentication.",
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID"),
|
||||||
|
),
|
||||||
|
request_body = BookSessionRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
|
||||||
|
(status = 400, description = "Invalid request"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 404, description = "Mentor not found"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn post_book_session(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(mentor_id): Path<String>,
|
||||||
|
Json(dto): Json<BookSessionRequestDto>,
|
||||||
|
) -> Response {
|
||||||
|
let user_email = match extract_email(&headers) {
|
||||||
|
Some(email) => email,
|
||||||
|
None => {
|
||||||
|
return imphnen_utils::common_response(
|
||||||
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
|
"Token tidak valid",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SessionsService::book_session(&state, mentor_id, user_email, dto).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Mentor's Sessions
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SessionStatusFilter {
|
||||||
|
status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors/{id}/sessions",
|
||||||
|
tag = "sessions",
|
||||||
|
summary = "List mentor's sessions",
|
||||||
|
description = "Get all sessions for a specific mentor. Only accessible by the mentor themselves or admin.",
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID"),
|
||||||
|
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden"),
|
||||||
|
(status = 404, description = "Mentor not found"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_mentor_sessions(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(mentor_id): Path<String>,
|
||||||
|
Query(filter): Query<SessionStatusFilter>,
|
||||||
|
) -> Response {
|
||||||
|
let user_email = match extract_email(&headers) {
|
||||||
|
Some(email) => email,
|
||||||
|
None => {
|
||||||
|
return imphnen_utils::common_response(
|
||||||
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
|
"Token tidak valid",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SessionsService::get_mentor_sessions(&state, mentor_id, user_email, filter.status).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Mentor Availability
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors/{id}/availability",
|
||||||
|
tag = "sessions",
|
||||||
|
summary = "Get mentor availability",
|
||||||
|
description = "Get available time slots for booking with a mentor. Public endpoint.",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
|
||||||
|
(status = 404, description = "Mentor not found"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_mentor_availability(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(mentor_id): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
SessionsService::get_mentor_availability(&state, mentor_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Update Session Status
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/sessions/update/{id}/status",
|
||||||
|
tag = "sessions",
|
||||||
|
summary = "Update session status",
|
||||||
|
description = "Update the status of a session (confirm, complete, cancel). Only accessible by the mentor.",
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Session ID"),
|
||||||
|
),
|
||||||
|
request_body = UpdateSessionStatusRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
|
||||||
|
(status = 400, description = "Invalid request"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden"),
|
||||||
|
(status = 404, description = "Session not found"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn put_update_session_status(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(session_id): Path<String>,
|
||||||
|
Json(dto): Json<UpdateSessionStatusRequestDto>,
|
||||||
|
) -> Response {
|
||||||
|
let user_email = match extract_email(&headers) {
|
||||||
|
Some(email) => email,
|
||||||
|
None => {
|
||||||
|
return imphnen_utils::common_response(
|
||||||
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
|
"Token tidak valid",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SessionsService::update_session_status(&state, session_id, user_email, dto).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Submit Feedback
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/sessions/{id}/feedback/create",
|
||||||
|
tag = "sessions",
|
||||||
|
summary = "Submit session feedback",
|
||||||
|
description = "Submit feedback and rating for a completed session. Only accessible by the mentee.",
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Session ID"),
|
||||||
|
),
|
||||||
|
request_body = SessionFeedbackRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
|
||||||
|
(status = 400, description = "Invalid request or session not completed"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden"),
|
||||||
|
(status = 404, description = "Session not found"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn post_submit_feedback(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(session_id): Path<String>,
|
||||||
|
Json(dto): Json<SessionFeedbackRequestDto>,
|
||||||
|
) -> Response {
|
||||||
|
let user_email = match extract_email(&headers) {
|
||||||
|
Some(email) => email,
|
||||||
|
None => {
|
||||||
|
return imphnen_utils::common_response(
|
||||||
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
|
"Token tidak valid",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SessionsService::submit_feedback(&state, session_id, user_email, dto).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get User's Sessions
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/users/me/sessions",
|
||||||
|
tag = "sessions",
|
||||||
|
summary = "Get my sessions",
|
||||||
|
description = "Get all sessions for the authenticated user (as mentee). Requires authentication.",
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
params(
|
||||||
|
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_my_sessions(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(filter): Query<SessionStatusFilter>,
|
||||||
|
) -> Response {
|
||||||
|
let user_email = match extract_email(&headers) {
|
||||||
|
Some(email) => email,
|
||||||
|
None => {
|
||||||
|
return imphnen_utils::common_response(
|
||||||
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
|
"Token tidak valid",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SessionsService::get_user_sessions(&state, user_email, filter.status).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Router
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
pub fn sessions_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
// Book session (under mentors path)
|
||||||
|
.route("/mentors/{id}/sessions/create", post(post_book_session))
|
||||||
|
// Get mentor's sessions
|
||||||
|
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
|
||||||
|
// Get mentor availability (public - no auth)
|
||||||
|
.route("/mentors/{id}/availability", get(get_mentor_availability))
|
||||||
|
// Update session status
|
||||||
|
.route("/sessions/update/{id}/status", put(put_update_session_status))
|
||||||
|
// Submit feedback
|
||||||
|
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
|
||||||
|
// Get my sessions
|
||||||
|
.route("/users/me/sessions", get(get_my_sessions))
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Book Session (POST /v1/mentors/{id}/sessions/book)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct BookSessionRequestDto {
|
||||||
|
#[validate(length(min = 3, max = 200, message = "Topic must be 3-200 characters"))]
|
||||||
|
pub topic: String,
|
||||||
|
|
||||||
|
#[validate(length(max = 1000, message = "Description must be max 1000 characters"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
#[validate(length(min = 1, message = "Scheduled time is required"))]
|
||||||
|
pub scheduled_at: String, // ISO 8601 datetime
|
||||||
|
|
||||||
|
#[validate(range(min = 15, max = 240, message = "Duration must be 15-240 minutes"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub duration_minutes: Option<i32>,
|
||||||
|
|
||||||
|
#[validate(length(max = 50, message = "Session type must be max 50 characters"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session_type: Option<String>, // "video_call", "phone_call", "chat"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct BookSessionResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub mentor_id: String,
|
||||||
|
pub mentee_id: String,
|
||||||
|
pub topic: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub scheduled_at: String,
|
||||||
|
pub duration_minutes: i32,
|
||||||
|
pub session_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// List Sessions (GET /v1/mentors/{id}/sessions & /v1/users/me/sessions)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SessionListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub mentor_id: String,
|
||||||
|
pub mentee_id: String,
|
||||||
|
pub mentee_fullname: Option<String>,
|
||||||
|
pub mentee_email: Option<String>,
|
||||||
|
pub topic: String,
|
||||||
|
pub scheduled_at: String,
|
||||||
|
pub duration_minutes: i32,
|
||||||
|
pub session_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub rating: Option<i32>,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SessionListResponseDto {
|
||||||
|
pub sessions: Vec<SessionListItemDto>,
|
||||||
|
pub total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Session Detail
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SessionDetailDto {
|
||||||
|
pub id: String,
|
||||||
|
pub mentor_id: String,
|
||||||
|
pub mentor_fullname: Option<String>,
|
||||||
|
pub mentee_id: String,
|
||||||
|
pub mentee_fullname: Option<String>,
|
||||||
|
pub topic: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub scheduled_at: String,
|
||||||
|
pub duration_minutes: i32,
|
||||||
|
pub meeting_link: Option<String>,
|
||||||
|
pub session_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub feedback: Option<String>,
|
||||||
|
pub rating: Option<i32>,
|
||||||
|
pub feedback_submitted_at: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Mentor Availability (GET /v1/mentors/{id}/availability)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AvailabilitySlotDto {
|
||||||
|
pub date: String, // YYYY-MM-DD
|
||||||
|
pub time: String, // HH:MM
|
||||||
|
pub available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct MentorAvailabilityDto {
|
||||||
|
pub mentor_id: String,
|
||||||
|
pub availability_commitment: String,
|
||||||
|
pub preferred_formats: Vec<String>,
|
||||||
|
pub slots: Vec<AvailabilitySlotDto>,
|
||||||
|
pub booked_dates: Vec<String>, // Dates with existing sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Update Session Status (PUT /v1/sessions/{id}/status)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct UpdateSessionStatusRequestDto {
|
||||||
|
#[validate(length(min = 1, max = 50, message = "Status must be 1-50 characters"))]
|
||||||
|
pub status: String, // "confirmed", "completed", "cancelled", "no_show"
|
||||||
|
|
||||||
|
#[validate(url(message = "Meeting link must be a valid URL"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub meeting_link: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateSessionStatusResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub status: String,
|
||||||
|
pub meeting_link: Option<String>,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Submit Feedback (POST /v1/sessions/{id}/feedback)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct SessionFeedbackRequestDto {
|
||||||
|
#[validate(length(min = 10, max = 2000, message = "Feedback must be 10-2000 characters"))]
|
||||||
|
pub feedback: String,
|
||||||
|
|
||||||
|
#[validate(range(min = 1, max = 5, message = "Rating must be 1-5"))]
|
||||||
|
pub rating: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SessionFeedbackResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub feedback: String,
|
||||||
|
pub rating: i32,
|
||||||
|
pub submitted_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Query DTOs (internal use)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SessionDetailQueryDto {
|
||||||
|
pub id: String,
|
||||||
|
pub mentor_id: String,
|
||||||
|
pub mentee_id: String,
|
||||||
|
pub topic: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub scheduled_at: String,
|
||||||
|
pub duration_minutes: i32,
|
||||||
|
pub meeting_link: Option<String>,
|
||||||
|
pub session_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub feedback: Option<String>,
|
||||||
|
pub rating: Option<i32>,
|
||||||
|
pub feedback_submitted_at: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub mentor_fullname: Option<String>,
|
||||||
|
pub mentee_fullname: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SessionListQueryDto {
|
||||||
|
pub id: String,
|
||||||
|
pub mentor_id: String,
|
||||||
|
pub mentee_id: String,
|
||||||
|
pub topic: String,
|
||||||
|
pub scheduled_at: String,
|
||||||
|
pub duration_minutes: i32,
|
||||||
|
pub session_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub rating: Option<i32>,
|
||||||
|
pub created_at: String,
|
||||||
|
pub mentee_fullname: Option<String>,
|
||||||
|
pub mentee_email: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use imphnen_utils::get_id;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
pub struct SessionsRepository<'a> {
|
||||||
|
pub state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SessionsRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Create Session
|
||||||
|
// ============================================
|
||||||
|
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let created: Option<SessionSchema> = db
|
||||||
|
.create("sessions")
|
||||||
|
.content(schema)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to create session: {}", e))?;
|
||||||
|
|
||||||
|
created.ok_or_else(|| "Session creation returned None".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Session by ID
|
||||||
|
// ============================================
|
||||||
|
pub async fn query_session_by_id(&self, id: &Thing) -> Result<Option<SessionSchema>, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||||
|
let session: Option<SessionSchema> = db
|
||||||
|
.select(record_key)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to fetch session: {}", e))?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Session Detail with User Info
|
||||||
|
// ============================================
|
||||||
|
pub async fn query_session_detail(&self, id: &Thing) -> Result<Option<SessionDetailQueryDto>, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let query = r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
mentor_id,
|
||||||
|
mentee_id,
|
||||||
|
topic,
|
||||||
|
description,
|
||||||
|
scheduled_at,
|
||||||
|
duration_minutes,
|
||||||
|
meeting_link,
|
||||||
|
session_type,
|
||||||
|
status,
|
||||||
|
feedback,
|
||||||
|
rating,
|
||||||
|
feedback_submitted_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
(SELECT fullname FROM $parent.mentor_id.user_id)[0].fullname AS mentor_fullname,
|
||||||
|
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname
|
||||||
|
FROM type::thing($table, $id)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut result = db
|
||||||
|
.query(query)
|
||||||
|
.bind(("table", "sessions"))
|
||||||
|
.bind(("id", id.id.to_string()))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to query session detail: {}", e))?;
|
||||||
|
|
||||||
|
let session: Option<SessionDetailQueryDto> = result
|
||||||
|
.take(0)
|
||||||
|
.map_err(|e| format!("Failed to parse session detail: {}", e))?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// List Mentor's Sessions
|
||||||
|
// ============================================
|
||||||
|
pub async fn query_mentor_sessions(
|
||||||
|
&self,
|
||||||
|
mentor_id: &Thing,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||||
|
let query = if let Some(_status) = status_filter.as_ref() {
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
mentor_id,
|
||||||
|
mentee_id,
|
||||||
|
topic,
|
||||||
|
scheduled_at,
|
||||||
|
duration_minutes,
|
||||||
|
session_type,
|
||||||
|
status,
|
||||||
|
rating,
|
||||||
|
created_at,
|
||||||
|
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||||
|
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||||
|
FROM sessions
|
||||||
|
WHERE mentor_id = $mentor_id AND status = $status
|
||||||
|
ORDER BY scheduled_at DESC
|
||||||
|
"#
|
||||||
|
} else {
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
mentor_id,
|
||||||
|
mentee_id,
|
||||||
|
topic,
|
||||||
|
scheduled_at,
|
||||||
|
duration_minutes,
|
||||||
|
session_type,
|
||||||
|
status,
|
||||||
|
rating,
|
||||||
|
created_at,
|
||||||
|
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||||
|
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||||
|
FROM sessions
|
||||||
|
WHERE mentor_id = $mentor_id
|
||||||
|
ORDER BY scheduled_at DESC
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let mentor_id_clone = mentor_id.clone();
|
||||||
|
let mut result = if let Some(status_val) = status_filter {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("mentor_id", mentor_id_clone))
|
||||||
|
.bind(("status", status_val))
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("mentor_id", mentor_id_clone))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.map_err(|e| format!("Failed to query mentor sessions: {}", e))?;
|
||||||
|
|
||||||
|
let sessions: Vec<SessionListQueryDto> = result
|
||||||
|
.take(0)
|
||||||
|
.map_err(|e| format!("Failed to parse mentor sessions: {}", e))?;
|
||||||
|
|
||||||
|
Ok(sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// List User's Sessions (as mentee)
|
||||||
|
// ============================================
|
||||||
|
pub async fn query_user_sessions(
|
||||||
|
&self,
|
||||||
|
user_id: &Thing,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||||
|
let query = if let Some(_status) = status_filter.as_ref() {
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
mentor_id,
|
||||||
|
mentee_id,
|
||||||
|
topic,
|
||||||
|
scheduled_at,
|
||||||
|
duration_minutes,
|
||||||
|
session_type,
|
||||||
|
status,
|
||||||
|
rating,
|
||||||
|
created_at,
|
||||||
|
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||||
|
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||||
|
FROM sessions
|
||||||
|
WHERE mentee_id = $user_id AND status = $status
|
||||||
|
ORDER BY scheduled_at DESC
|
||||||
|
"#
|
||||||
|
} else {
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
mentor_id,
|
||||||
|
mentee_id,
|
||||||
|
topic,
|
||||||
|
scheduled_at,
|
||||||
|
duration_minutes,
|
||||||
|
session_type,
|
||||||
|
status,
|
||||||
|
rating,
|
||||||
|
created_at,
|
||||||
|
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||||
|
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||||
|
FROM sessions
|
||||||
|
WHERE mentee_id = $user_id
|
||||||
|
ORDER BY scheduled_at DESC
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let user_id_clone = user_id.clone();
|
||||||
|
let mut result = if let Some(status_val) = status_filter {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.bind(("status", status_val))
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.map_err(|e| format!("Failed to query user sessions: {}", e))?;
|
||||||
|
|
||||||
|
let sessions: Vec<SessionListQueryDto> = result
|
||||||
|
.take(0)
|
||||||
|
.map_err(|e| format!("Failed to parse user sessions: {}", e))?;
|
||||||
|
|
||||||
|
Ok(sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Booked Dates for Mentor
|
||||||
|
// ============================================
|
||||||
|
pub async fn query_booked_dates(&self, mentor_id: &Thing) -> Result<Vec<String>, String> {
|
||||||
|
let query = r#"
|
||||||
|
SELECT scheduled_at FROM sessions
|
||||||
|
WHERE mentor_id = $mentor_id
|
||||||
|
AND status IN ['pending', 'confirmed']
|
||||||
|
ORDER BY scheduled_at ASC
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let mentor_id_clone = mentor_id.clone();
|
||||||
|
let mut result = db
|
||||||
|
.query(query)
|
||||||
|
.bind(("mentor_id", mentor_id_clone))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to query booked dates: {}", e))?;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DateOnly {
|
||||||
|
scheduled_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let dates: Vec<DateOnly> = result
|
||||||
|
.take(0)
|
||||||
|
.map_err(|e| format!("Failed to parse booked dates: {}", e))?;
|
||||||
|
|
||||||
|
Ok(dates.into_iter().map(|d| d.scheduled_at).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Update Session
|
||||||
|
// ============================================
|
||||||
|
pub async fn update_session(&self, id: &Thing, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||||
|
let updated: Option<SessionSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.content(schema)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to update session: {}", e))?;
|
||||||
|
|
||||||
|
updated.ok_or_else(|| "Session update returned None".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Count Mentor Sessions
|
||||||
|
// ============================================
|
||||||
|
pub async fn count_mentor_sessions(
|
||||||
|
&self,
|
||||||
|
mentor_id: &Thing,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<usize, String> {
|
||||||
|
let query = if status_filter.is_some() {
|
||||||
|
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id AND status = $status GROUP ALL"
|
||||||
|
} else {
|
||||||
|
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id GROUP ALL"
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let mentor_id_clone = mentor_id.clone();
|
||||||
|
let mut result = if let Some(status_val) = status_filter {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("mentor_id", mentor_id_clone))
|
||||||
|
.bind(("status", status_val))
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("mentor_id", mentor_id_clone))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.map_err(|e| format!("Failed to count mentor sessions: {}", e))?;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct CountResult {
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
let count_result: Option<CountResult> = result
|
||||||
|
.take(0)
|
||||||
|
.map_err(|e| format!("Failed to parse count: {}", e))?;
|
||||||
|
|
||||||
|
Ok(count_result.map(|r| r.count).unwrap_or(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Count User Sessions
|
||||||
|
// ============================================
|
||||||
|
pub async fn count_user_sessions(
|
||||||
|
&self,
|
||||||
|
user_id: &Thing,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<usize, String> {
|
||||||
|
let query = if status_filter.is_some() {
|
||||||
|
"SELECT count() FROM sessions WHERE mentee_id = $user_id AND status = $status GROUP ALL"
|
||||||
|
} else {
|
||||||
|
"SELECT count() FROM sessions WHERE mentee_id = $user_id GROUP ALL"
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let user_id_clone = user_id.clone();
|
||||||
|
let mut result = if let Some(status_val) = status_filter {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.bind(("status", status_val))
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
db.query(query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.map_err(|e| format!("Failed to count user sessions: {}", e))?;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct CountResult {
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
let count_result: Option<CountResult> = result
|
||||||
|
.take(0)
|
||||||
|
.map_err(|e| format!("Failed to parse count: {}", e))?;
|
||||||
|
|
||||||
|
Ok(count_result.map(|r| r.count).unwrap_or(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Delete Session (soft delete)
|
||||||
|
// ============================================
|
||||||
|
// Delete Session (soft delete)
|
||||||
|
// ============================================
|
||||||
|
pub async fn delete_session(&self, id: &Thing) -> Result<(), String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||||
|
let _: Option<SessionSchema> = db
|
||||||
|
.delete(record_key)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to delete session: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
|
use imphnen_utils::{get_iso_date, make_thing};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{sql::Thing, Uuid};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SessionSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub mentor_id: Thing,
|
||||||
|
pub mentee_id: Thing,
|
||||||
|
pub topic: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub scheduled_at: String, // ISO 8601 datetime
|
||||||
|
pub duration_minutes: i32,
|
||||||
|
pub meeting_link: Option<String>,
|
||||||
|
pub session_type: String, // "video_call", "phone_call", "chat"
|
||||||
|
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
|
||||||
|
pub feedback: Option<String>,
|
||||||
|
pub rating: Option<i32>, // 1-5
|
||||||
|
pub feedback_submitted_at: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SessionSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
ResourceEnum::Sessions.to_string().as_str(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
mentor_id: make_thing(
|
||||||
|
ResourceEnum::Mentors.to_string().as_str(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
mentee_id: make_thing(
|
||||||
|
ResourceEnum::Users.to_string().as_str(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
topic: String::new(),
|
||||||
|
description: None,
|
||||||
|
scheduled_at: get_iso_date(),
|
||||||
|
duration_minutes: 60,
|
||||||
|
meeting_link: None,
|
||||||
|
session_type: "video_call".to_string(),
|
||||||
|
status: "pending".to_string(),
|
||||||
|
feedback: None,
|
||||||
|
rating: None,
|
||||||
|
feedback_submitted_at: None,
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionSchema {
|
||||||
|
pub fn from_book_request(
|
||||||
|
mentor_id: Thing,
|
||||||
|
mentee_id: Thing,
|
||||||
|
request: BookSessionRequestDto,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
mentor_id,
|
||||||
|
mentee_id,
|
||||||
|
topic: request.topic,
|
||||||
|
description: request.description,
|
||||||
|
scheduled_at: request.scheduled_at,
|
||||||
|
duration_minutes: request.duration_minutes.unwrap_or(60),
|
||||||
|
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
|
||||||
|
self.status = request.status;
|
||||||
|
if let Some(link) = request.meeting_link {
|
||||||
|
self.meeting_link = Some(link);
|
||||||
|
}
|
||||||
|
self.updated_at = get_iso_date();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
|
||||||
|
self.feedback = Some(request.feedback);
|
||||||
|
self.rating = Some(request.rating);
|
||||||
|
self.feedback_submitted_at = Some(get_iso_date());
|
||||||
|
self.updated_at = get_iso_date();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
use super::{
|
||||||
|
AvailabilitySlotDto, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||||
|
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||||
|
SessionListResponseDto, SessionSchema, SessionsRepository, UpdateSessionStatusRequestDto,
|
||||||
|
UpdateSessionStatusResponseDto,
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use imphnen_utils::{common_response, extract_id, get_iso_date, make_thing, success_response, validate_request};
|
||||||
|
|
||||||
|
pub struct SessionsService;
|
||||||
|
|
||||||
|
impl SessionsService {
|
||||||
|
// ============================================
|
||||||
|
// Book Session
|
||||||
|
// ============================================
|
||||||
|
pub async fn book_session(
|
||||||
|
state: &AppState,
|
||||||
|
mentor_id: String,
|
||||||
|
user_id: String,
|
||||||
|
dto: BookSessionRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&dto) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||||
|
let mentee_thing = make_thing("users", &user_id);
|
||||||
|
|
||||||
|
let schema = SessionSchema::from_book_request(mentor_thing.clone(), mentee_thing.clone(), dto);
|
||||||
|
|
||||||
|
let repo = SessionsRepository::new(state);
|
||||||
|
match repo.create_session(schema).await {
|
||||||
|
Ok(created) => {
|
||||||
|
let response = BookSessionResponseDto {
|
||||||
|
id: extract_id(&created.id),
|
||||||
|
mentor_id: extract_id(&created.mentor_id),
|
||||||
|
mentee_id: extract_id(&created.mentee_id),
|
||||||
|
topic: created.topic,
|
||||||
|
description: created.description,
|
||||||
|
scheduled_at: created.scheduled_at,
|
||||||
|
duration_minutes: created.duration_minutes,
|
||||||
|
session_type: created.session_type,
|
||||||
|
status: created.status,
|
||||||
|
created_at: created.created_at,
|
||||||
|
};
|
||||||
|
success_response(ResponseSuccessDto { data: response })
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Mentor's Sessions
|
||||||
|
// ============================================
|
||||||
|
pub async fn get_mentor_sessions(
|
||||||
|
state: &AppState,
|
||||||
|
mentor_id: String,
|
||||||
|
_user_email: String,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Response {
|
||||||
|
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||||
|
|
||||||
|
let repo = SessionsRepository::new(state);
|
||||||
|
|
||||||
|
// Get count and sessions
|
||||||
|
let count = match repo.count_mentor_sessions(&mentor_thing, status_filter.clone()).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match repo.query_mentor_sessions(&mentor_thing, status_filter).await {
|
||||||
|
Ok(sessions) => {
|
||||||
|
let session_items: Vec<SessionListItemDto> = sessions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| SessionListItemDto {
|
||||||
|
id: s.id,
|
||||||
|
mentor_id: s.mentor_id,
|
||||||
|
mentee_id: s.mentee_id,
|
||||||
|
mentee_fullname: s.mentee_fullname,
|
||||||
|
mentee_email: s.mentee_email,
|
||||||
|
topic: s.topic,
|
||||||
|
scheduled_at: s.scheduled_at,
|
||||||
|
duration_minutes: s.duration_minutes,
|
||||||
|
session_type: s.session_type,
|
||||||
|
status: s.status,
|
||||||
|
rating: s.rating,
|
||||||
|
created_at: s.created_at,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let response = SessionListResponseDto {
|
||||||
|
sessions: session_items,
|
||||||
|
total: count,
|
||||||
|
};
|
||||||
|
success_response(ResponseSuccessDto { data: response })
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get User's Sessions (as mentee)
|
||||||
|
// ============================================
|
||||||
|
pub async fn get_user_sessions(
|
||||||
|
state: &AppState,
|
||||||
|
user_id: String,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Response {
|
||||||
|
let user_thing = make_thing("users", &user_id);
|
||||||
|
|
||||||
|
let repo = SessionsRepository::new(state);
|
||||||
|
|
||||||
|
// Get count and sessions
|
||||||
|
let count = match repo.count_user_sessions(&user_thing, status_filter.clone()).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match repo.query_user_sessions(&user_thing, status_filter).await {
|
||||||
|
Ok(sessions) => {
|
||||||
|
let session_items: Vec<SessionListItemDto> = sessions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| SessionListItemDto {
|
||||||
|
id: s.id,
|
||||||
|
mentor_id: s.mentor_id,
|
||||||
|
mentee_id: s.mentee_id,
|
||||||
|
mentee_fullname: s.mentee_fullname,
|
||||||
|
mentee_email: s.mentee_email,
|
||||||
|
topic: s.topic,
|
||||||
|
scheduled_at: s.scheduled_at,
|
||||||
|
duration_minutes: s.duration_minutes,
|
||||||
|
session_type: s.session_type,
|
||||||
|
status: s.status,
|
||||||
|
rating: s.rating,
|
||||||
|
created_at: s.created_at,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let response = SessionListResponseDto {
|
||||||
|
sessions: session_items,
|
||||||
|
total: count,
|
||||||
|
};
|
||||||
|
success_response(ResponseSuccessDto { data: response })
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Mentor Availability
|
||||||
|
// ============================================
|
||||||
|
pub async fn get_mentor_availability(state: &AppState, mentor_id: String) -> Response {
|
||||||
|
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||||
|
|
||||||
|
let repo = SessionsRepository::new(state);
|
||||||
|
match repo.query_booked_dates(&mentor_thing).await {
|
||||||
|
Ok(booked_dates) => {
|
||||||
|
// Generate sample availability slots (next 7 days)
|
||||||
|
let mut slots = Vec::new();
|
||||||
|
let today = Utc::now().date_naive();
|
||||||
|
|
||||||
|
for i in 0..7 {
|
||||||
|
let date = today + Duration::days(i);
|
||||||
|
let date_str = date.format("%Y-%m-%d").to_string();
|
||||||
|
|
||||||
|
// Generate time slots (9 AM to 5 PM, every hour)
|
||||||
|
for hour in 9..17 {
|
||||||
|
let time_str = format!("{:02}:00", hour);
|
||||||
|
let datetime_str = format!("{}T{}:00Z", date_str, time_str);
|
||||||
|
|
||||||
|
// Check if this slot is booked
|
||||||
|
let is_booked = booked_dates.iter().any(|d| d.starts_with(&datetime_str[..13]));
|
||||||
|
|
||||||
|
slots.push(AvailabilitySlotDto {
|
||||||
|
date: date_str.clone(),
|
||||||
|
time: time_str,
|
||||||
|
available: !is_booked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = MentorAvailabilityDto {
|
||||||
|
mentor_id,
|
||||||
|
availability_commitment: "Available weekdays 9 AM - 5 PM".to_string(),
|
||||||
|
preferred_formats: vec!["video_call".to_string(), "phone_call".to_string()],
|
||||||
|
slots,
|
||||||
|
booked_dates,
|
||||||
|
};
|
||||||
|
success_response(ResponseSuccessDto { data: response })
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Update Session Status
|
||||||
|
// ============================================
|
||||||
|
pub async fn update_session_status(
|
||||||
|
state: &AppState,
|
||||||
|
session_id: String,
|
||||||
|
_user_id: String,
|
||||||
|
dto: UpdateSessionStatusRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&dto) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_thing = make_thing("sessions", &session_id);
|
||||||
|
|
||||||
|
let repo = SessionsRepository::new(state);
|
||||||
|
match repo.query_session_by_id(&session_thing).await {
|
||||||
|
Ok(Some(mut session)) => {
|
||||||
|
session.update_status(dto.clone());
|
||||||
|
match repo.update_session(&session_thing, session).await {
|
||||||
|
Ok(updated) => {
|
||||||
|
let response = UpdateSessionStatusResponseDto {
|
||||||
|
id: extract_id(&updated.id),
|
||||||
|
status: updated.status,
|
||||||
|
meeting_link: updated.meeting_link,
|
||||||
|
updated_at: updated.updated_at,
|
||||||
|
};
|
||||||
|
success_response(ResponseSuccessDto { data: response })
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Submit Feedback
|
||||||
|
// ============================================
|
||||||
|
pub async fn submit_feedback(
|
||||||
|
state: &AppState,
|
||||||
|
session_id: String,
|
||||||
|
user_id: String,
|
||||||
|
dto: SessionFeedbackRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&dto) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_thing = make_thing("sessions", &session_id);
|
||||||
|
|
||||||
|
let repo = SessionsRepository::new(state);
|
||||||
|
match repo.query_session_by_id(&session_thing).await {
|
||||||
|
Ok(Some(mut session)) => {
|
||||||
|
// Authorization: Only mentee can submit feedback
|
||||||
|
let mentee_id = extract_id(&session.mentee_id);
|
||||||
|
if mentee_id != user_id {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"Unauthorized: Only the mentee can submit feedback",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate session is completed
|
||||||
|
if session.status != "completed" {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Feedback can only be submitted for completed sessions",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.add_feedback(dto.clone());
|
||||||
|
match repo.update_session(&session_thing, session).await {
|
||||||
|
Ok(updated) => {
|
||||||
|
let response = SessionFeedbackResponseDto {
|
||||||
|
id: extract_id(&updated.id),
|
||||||
|
feedback: dto.feedback,
|
||||||
|
rating: dto.rating,
|
||||||
|
submitted_at: updated.feedback_submitted_at.unwrap_or_else(get_iso_date),
|
||||||
|
};
|
||||||
|
success_response(ResponseSuccessDto { data: response })
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,14 @@
|
|||||||
|
/// Version 2 of the Dimentorin API - currently under development
|
||||||
|
/// This module will contain all version 2 endpoints following API versioning best practices
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
/// Placeholder for version 2 router
|
||||||
|
/// To be implemented when version 2 endpoints are ready
|
||||||
|
pub fn dimentorin_v2_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
// Version 2 endpoints will be added here following the same pattern as v1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export the v1 router for backward compatibility
|
||||||
|
pub use crate::v1::dimentorin_router;
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
surrealdb.workspace = true
|
surrealdb.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
strum_macros.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
/// Schema untuk audit log yang mencatat semua aksi admin
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AuditLogSchema {
|
||||||
|
/// ID unik dari log
|
||||||
|
pub id: Option<Thing>,
|
||||||
|
/// ID pengguna yang melakukan aksi
|
||||||
|
pub user_id: String,
|
||||||
|
/// Email pengguna
|
||||||
|
pub user_email: String,
|
||||||
|
/// Tipe aksi yang dilakukan (CREATE, UPDATE, DELETE, etc.)
|
||||||
|
pub action: String,
|
||||||
|
/// Resource yang terkena aksi
|
||||||
|
pub resource: String,
|
||||||
|
/// ID resource yang terkena aksi
|
||||||
|
pub resource_id: Option<String>,
|
||||||
|
/// Data sebelum perubahan (untuk UPDATE/DELETE)
|
||||||
|
pub old_data: Option<serde_json::Value>,
|
||||||
|
/// Data setelah perubahan (untuk CREATE/UPDATE)
|
||||||
|
pub new_data: Option<serde_json::Value>,
|
||||||
|
/// IP address pengguna
|
||||||
|
pub ip_address: String,
|
||||||
|
/// User agent pengguna
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
/// Timestamp ketika aksi dilakukan
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schema untuk rate limiting menggunakan SurrealDB memori
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct RateLimitSchema {
|
||||||
|
/// ID unik (IP address)
|
||||||
|
pub id: Option<Thing>,
|
||||||
|
/// IP address klien
|
||||||
|
pub ip_address: String,
|
||||||
|
/// Jumlah request dalam window saat ini
|
||||||
|
pub request_count: u32,
|
||||||
|
/// Timestamp pertama request dalam window
|
||||||
|
pub first_request_time: DateTime<Utc>,
|
||||||
|
/// Timestamp terakhir request
|
||||||
|
pub last_request_time: DateTime<Utc>,
|
||||||
|
/// Window duration dalam detik
|
||||||
|
pub window_duration_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RateLimitSchema {
|
||||||
|
/// Buat instance baru RateLimitSchema
|
||||||
|
pub fn new(ip_address: String, window_duration_secs: u64) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
ip_address,
|
||||||
|
request_count: 1,
|
||||||
|
first_request_time: now,
|
||||||
|
last_request_time: now,
|
||||||
|
window_duration_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Periksa apakah rate limit sudah terlampaui
|
||||||
|
pub fn is_rate_limited(&self, max_requests: u32) -> bool {
|
||||||
|
self.request_count > max_requests
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perbarui counter dan timestamp
|
||||||
|
pub fn increment(&mut self) {
|
||||||
|
self.request_count += 1;
|
||||||
|
self.last_request_time = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset counter jika window sudah expired
|
||||||
|
pub fn reset_if_expired(&mut self) -> bool {
|
||||||
|
let now = Utc::now();
|
||||||
|
let duration = now - self.first_request_time;
|
||||||
|
|
||||||
|
if duration.num_seconds() >= self.window_duration_secs as i64 {
|
||||||
|
self.request_count = 1;
|
||||||
|
self.first_request_time = now;
|
||||||
|
self.last_request_time = now;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::{Surreal, engine::any::Any, engine::local::Db};
|
|
||||||
use utoipa::{IntoParams, ToSchema};
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
@@ -51,13 +50,12 @@ pub struct ResponseListSuccessDto<T: Serialize> {
|
|||||||
pub meta: Option<MetaResponseDto>,
|
pub meta: Option<MetaResponseDto>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SurrealWsClient = Surreal<Any>;
|
|
||||||
pub type SurrealMemClient = Surreal<Db>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct AppState {
|
pub struct ErrorDto {
|
||||||
pub surrealdb_ws: SurrealWsClient,
|
pub status: u16,
|
||||||
pub surrealdb_mem: SurrealMemClient,
|
pub message: String,
|
||||||
|
pub details: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
|||||||
@@ -1,4 +1,32 @@
|
|||||||
pub mod common_dto;
|
pub mod common_dto;
|
||||||
pub mod error_dto;
|
pub mod error_dto;
|
||||||
pub use common_dto::*;
|
pub mod users;
|
||||||
pub use error_dto::*;
|
pub mod permissions;
|
||||||
|
pub mod audit_log;
|
||||||
|
|
||||||
|
// Re-export error type at root level for convenience
|
||||||
|
pub use error_dto::error::Error;
|
||||||
|
|
||||||
|
// Explicit common_dto exports
|
||||||
|
pub use common_dto::CountResult;
|
||||||
|
pub use common_dto::ErrorDto;
|
||||||
|
pub use common_dto::MessageResponseDto;
|
||||||
|
pub use common_dto::MetaRequestDto;
|
||||||
|
pub use common_dto::MetaResponseDto;
|
||||||
|
pub use common_dto::ResponseListSuccessDto;
|
||||||
|
pub use common_dto::ResponseSuccessDto;
|
||||||
|
|
||||||
|
// Explicit users exports
|
||||||
|
pub use users::EducationDto;
|
||||||
|
pub use users::ExperienceDto;
|
||||||
|
pub use users::RolesDetailItemDto;
|
||||||
|
pub use users::RolesDetailQueryDto;
|
||||||
|
pub use users::UsersDetailQueryDto;
|
||||||
|
|
||||||
|
// Explicit permissions exports
|
||||||
|
pub use permissions::PermissionsEnum;
|
||||||
|
pub use permissions::PermissionsItemDto;
|
||||||
|
pub use permissions::PermissionsQueryDto;
|
||||||
|
|
||||||
|
// Explicit audit_log exports
|
||||||
|
pub use audit_log::AuditLogSchema;
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use strum_macros::EnumIter;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, EnumIter)]
|
||||||
|
pub enum PermissionsEnum {
|
||||||
|
// User permissions
|
||||||
|
ReadListUsers,
|
||||||
|
ReadDetailUsers,
|
||||||
|
CreateUsers,
|
||||||
|
DeleteUsers,
|
||||||
|
UpdateUsers,
|
||||||
|
ActivateUsers,
|
||||||
|
|
||||||
|
// Role permissions
|
||||||
|
ReadListRoles,
|
||||||
|
ReadDetailRoles,
|
||||||
|
CreateRoles,
|
||||||
|
DeleteRoles,
|
||||||
|
UpdateRoles,
|
||||||
|
|
||||||
|
// Permission permissions
|
||||||
|
ReadListPermissions,
|
||||||
|
ReadDetailPermissions,
|
||||||
|
CreatePermissions,
|
||||||
|
DeletePermissions,
|
||||||
|
UpdatePermissions,
|
||||||
|
|
||||||
|
// Team permissions
|
||||||
|
ReadListTeams,
|
||||||
|
ReadDetailTeams,
|
||||||
|
|
||||||
|
// Administrator permissions
|
||||||
|
ManageAllUsers,
|
||||||
|
ManageAllRoles,
|
||||||
|
ManageAllPermissions,
|
||||||
|
ManageAllTeams,
|
||||||
|
ViewAllSensitiveData,
|
||||||
|
AccessAdminDashboard,
|
||||||
|
Administrator,
|
||||||
|
|
||||||
|
// Gacha permissions
|
||||||
|
CreateGachaClaims,
|
||||||
|
ReadDetailGachaClaims,
|
||||||
|
ReadListGachaItems,
|
||||||
|
ReadDetailGachaItems,
|
||||||
|
CreateGachaItems,
|
||||||
|
DeleteGachaItems,
|
||||||
|
UpdateGachaItems,
|
||||||
|
ReadDetailGachaRolls,
|
||||||
|
CreateGachaRolls,
|
||||||
|
ExecuteGachaRolls,
|
||||||
|
DeleteGachaRolls,
|
||||||
|
|
||||||
|
// Mentor permissions
|
||||||
|
ReadListMentors,
|
||||||
|
ReadDetailMentors,
|
||||||
|
RegisterMentors,
|
||||||
|
ReadOwnMentorProfile,
|
||||||
|
UpdateOwnMentorProfile,
|
||||||
|
ReadOwnMentorStatus,
|
||||||
|
UpdateMentors,
|
||||||
|
VerifyMentors,
|
||||||
|
DeleteMentors,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PermissionsEnum {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let permission_str = match self {
|
||||||
|
// User permissions
|
||||||
|
PermissionsEnum::ReadListUsers => "Read List Users",
|
||||||
|
PermissionsEnum::ReadDetailUsers => "Read Detail Users",
|
||||||
|
PermissionsEnum::CreateUsers => "Create Users",
|
||||||
|
PermissionsEnum::DeleteUsers => "Delete Users",
|
||||||
|
PermissionsEnum::UpdateUsers => "Update Users",
|
||||||
|
PermissionsEnum::ActivateUsers => "Activate Users",
|
||||||
|
|
||||||
|
// Role permissions
|
||||||
|
PermissionsEnum::ReadListRoles => "Read List Roles",
|
||||||
|
PermissionsEnum::ReadDetailRoles => "Read Detail Roles",
|
||||||
|
PermissionsEnum::CreateRoles => "Create Roles",
|
||||||
|
PermissionsEnum::DeleteRoles => "Delete Roles",
|
||||||
|
PermissionsEnum::UpdateRoles => "Update Roles",
|
||||||
|
|
||||||
|
// Permission permissions
|
||||||
|
PermissionsEnum::ReadListPermissions => "Read List Permissions",
|
||||||
|
PermissionsEnum::ReadDetailPermissions => "Read Detail Permissions",
|
||||||
|
PermissionsEnum::CreatePermissions => "Create Permissions",
|
||||||
|
PermissionsEnum::DeletePermissions => "Delete Permissions",
|
||||||
|
PermissionsEnum::UpdatePermissions => "Update Permissions",
|
||||||
|
|
||||||
|
// Team permissions
|
||||||
|
PermissionsEnum::ReadListTeams => "Read List Teams",
|
||||||
|
PermissionsEnum::ReadDetailTeams => "Read Detail Teams",
|
||||||
|
|
||||||
|
// Gacha permissions
|
||||||
|
PermissionsEnum::CreateGachaClaims => "Create Gacha Claims",
|
||||||
|
PermissionsEnum::ReadDetailGachaClaims => "Read Detail Gacha Claims",
|
||||||
|
PermissionsEnum::ReadListGachaItems => "Read List Gacha Items",
|
||||||
|
PermissionsEnum::ReadDetailGachaItems => "Read Detail Gacha Items",
|
||||||
|
PermissionsEnum::CreateGachaItems => "Create Gacha Items",
|
||||||
|
PermissionsEnum::DeleteGachaItems => "Delete Gacha Items",
|
||||||
|
PermissionsEnum::UpdateGachaItems => "Update Gacha Items",
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls => "Read Detail Gacha Rolls",
|
||||||
|
PermissionsEnum::CreateGachaRolls => "Create Gacha Rolls",
|
||||||
|
PermissionsEnum::ExecuteGachaRolls => "Execute Gacha Rolls",
|
||||||
|
PermissionsEnum::DeleteGachaRolls => "Delete Gacha Rolls",
|
||||||
|
|
||||||
|
// Mentor permissions
|
||||||
|
PermissionsEnum::ReadListMentors => "Read List Mentors",
|
||||||
|
PermissionsEnum::ReadDetailMentors => "Read Detail Mentors",
|
||||||
|
PermissionsEnum::RegisterMentors => "Register Mentors",
|
||||||
|
PermissionsEnum::ReadOwnMentorProfile => "Read Own Mentor Profile",
|
||||||
|
PermissionsEnum::UpdateOwnMentorProfile => "Update Own Mentor Profile",
|
||||||
|
PermissionsEnum::ReadOwnMentorStatus => "Read Own Mentor Status",
|
||||||
|
PermissionsEnum::UpdateMentors => "Update Mentors",
|
||||||
|
PermissionsEnum::VerifyMentors => "Verify Mentors",
|
||||||
|
PermissionsEnum::DeleteMentors => "Delete Mentors",
|
||||||
|
|
||||||
|
// Administrator permissions
|
||||||
|
PermissionsEnum::ManageAllUsers => "Manage All Users",
|
||||||
|
PermissionsEnum::ManageAllRoles => "Manage All Roles",
|
||||||
|
PermissionsEnum::ManageAllPermissions => "Manage All Permissions",
|
||||||
|
PermissionsEnum::ManageAllTeams => "Manage All Teams",
|
||||||
|
PermissionsEnum::ViewAllSensitiveData => "View All Sensitive Data",
|
||||||
|
PermissionsEnum::AccessAdminDashboard => "Access Admin Dashboard",
|
||||||
|
PermissionsEnum::Administrator => "Administrator",
|
||||||
|
};
|
||||||
|
write!(f, "{permission_str}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PermissionsEnum {
|
||||||
|
pub fn id(&self) -> String {
|
||||||
|
match self {
|
||||||
|
// User permissions
|
||||||
|
PermissionsEnum::ReadListUsers => "7c15e31d-36e2-49f9-97db-138c03fb0cf6".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailUsers => "319ee593-ff0a-4f29-bbaf-9feb3174a3a6".to_string(),
|
||||||
|
PermissionsEnum::CreateUsers => "023e2dfe-93c3-4008-94a8-b5dff403f73b".to_string(),
|
||||||
|
PermissionsEnum::DeleteUsers => "96df0689-2ae9-4894-bf00-837c19415e5c".to_string(),
|
||||||
|
PermissionsEnum::UpdateUsers => "98b3dc4c-0124-461f-afcd-166637c5e6e8".to_string(),
|
||||||
|
PermissionsEnum::ActivateUsers => "4da8b434-89f9-4d91-85ae-eebd63cdbeda".to_string(),
|
||||||
|
|
||||||
|
// Role permissions
|
||||||
|
PermissionsEnum::ReadListRoles => "9164ca6e-c7e3-4238-a15f-f36ab9577e7e".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailRoles => "73888d18-b3e9-4f62-95a5-ba2c0d69fccb".to_string(),
|
||||||
|
PermissionsEnum::CreateRoles => "319ee593-ff0a-4f29-bbaf-9feb3174a3a2".to_string(),
|
||||||
|
PermissionsEnum::DeleteRoles => "35b0d992-65c8-4b62-b030-e6e0320e4048".to_string(),
|
||||||
|
PermissionsEnum::UpdateRoles => "a00d5608-4c48-4542-845c-dfe004687022".to_string(),
|
||||||
|
|
||||||
|
// Permission permissions
|
||||||
|
PermissionsEnum::ReadListPermissions => "8195eeb8-e64f-4172-aa57-596492c84a72".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailPermissions => "dad435cf-042c-41bd-a946-cea61ed2ffbc".to_string(),
|
||||||
|
PermissionsEnum::CreatePermissions => "0269ed71-0ae0-4c43-ad29-e3d861d8f9a0".to_string(),
|
||||||
|
PermissionsEnum::DeletePermissions => "b2dc3928-86ba-4c59-a03d-0b57d5183ebc".to_string(),
|
||||||
|
PermissionsEnum::UpdatePermissions => "299cb4d5-6556-4cc9-b6c1-32e6d31e0f9b".to_string(),
|
||||||
|
|
||||||
|
// Team permissions
|
||||||
|
PermissionsEnum::ReadListTeams => "e1f23456-7890-1234-5678-90abcdef1234".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailTeams => "f2345678-8901-2345-6789-01bcdef23456".to_string(),
|
||||||
|
|
||||||
|
// Gacha permissions
|
||||||
|
PermissionsEnum::CreateGachaClaims => "f41d53ce-4f88-4bb6-b9b4-5e3a8c38d962".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailGachaClaims => "c1c3d6c2-19fb-4b70-b58c-c19f2e8cfc79".to_string(),
|
||||||
|
PermissionsEnum::ReadListGachaItems => "fa6eb842-0a61-40c2-9c24-b226ad975037".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailGachaItems => "9c7857d7-b5ae-4688-923d-ef5572e9bc8b".to_string(),
|
||||||
|
PermissionsEnum::CreateGachaItems => "cf063be1-4d71-489e-b9fb-1c08c65f396c".to_string(),
|
||||||
|
PermissionsEnum::DeleteGachaItems => "46f8c6cf-ea0c-4c90-860c-69e2e65f7eb1".to_string(),
|
||||||
|
PermissionsEnum::UpdateGachaItems => "2d0cf4ae-56ae-4714-a12e-655cfc3d9eb2".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls => "53d6483a-04cd-4667-8792-2d0cc8e2d343".to_string(),
|
||||||
|
PermissionsEnum::CreateGachaRolls => "18e36c63-fcb7-4877-b911-c5aa611e878f".to_string(),
|
||||||
|
PermissionsEnum::ExecuteGachaRolls => "14c6a1cd-5c63-4643-89b5-b1a5f9920cc0".to_string(),
|
||||||
|
PermissionsEnum::DeleteGachaRolls => "12345678-ABCD-EFAB-CDEF-0123456789AB".to_string(),
|
||||||
|
|
||||||
|
// Mentor permissions
|
||||||
|
PermissionsEnum::ReadListMentors => "a1b2c3d4-5e6f-7890-abcd-ef1234567890".to_string(),
|
||||||
|
PermissionsEnum::ReadDetailMentors => "b2c3d4e5-6f78-9012-bcde-f23456789012".to_string(),
|
||||||
|
PermissionsEnum::RegisterMentors => "c3d4e5f6-7890-1234-cdef-345678901234".to_string(),
|
||||||
|
PermissionsEnum::ReadOwnMentorProfile => "d4e5f6a7-8901-2345-def0-456789012345".to_string(),
|
||||||
|
PermissionsEnum::UpdateOwnMentorProfile => "e5f6a7b8-9012-3456-ef01-567890123456".to_string(),
|
||||||
|
PermissionsEnum::ReadOwnMentorStatus => "f6a7b8c9-0123-4567-f012-678901234567".to_string(),
|
||||||
|
PermissionsEnum::UpdateMentors => "a7b8c9d0-1234-5678-0123-789012345678".to_string(),
|
||||||
|
PermissionsEnum::VerifyMentors => "b8c9d0e1-2345-6789-1234-890123456789".to_string(),
|
||||||
|
PermissionsEnum::DeleteMentors => "c9d0e1f2-3456-7890-2345-901234567890".to_string(),
|
||||||
|
|
||||||
|
// Administrator permissions
|
||||||
|
PermissionsEnum::ManageAllUsers => "d0e1f2a3-4567-8901-2345-0123456789ab".to_string(),
|
||||||
|
PermissionsEnum::ManageAllRoles => "e1f2a3b4-5678-9012-3456-1234567890ab".to_string(),
|
||||||
|
PermissionsEnum::ManageAllPermissions => "f2a3b4c5-6789-0123-4567-2345678901ab".to_string(),
|
||||||
|
PermissionsEnum::ManageAllTeams => "a3b4c5d6-7890-1234-5678-3456789012ab".to_string(),
|
||||||
|
PermissionsEnum::ViewAllSensitiveData => "b4c5d6e7-8901-2345-6789-4567890123ab".to_string(),
|
||||||
|
PermissionsEnum::AccessAdminDashboard => "c5d6e7f8-9012-3456-7890-5678901234ab".to_string(),
|
||||||
|
PermissionsEnum::Administrator => "d6e7f8a9-0123-4567-8901-6789012345ab".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a new unique ID for a permission
|
||||||
|
pub fn generate_id() -> String {
|
||||||
|
Uuid::new_v4().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all permissions as a vector
|
||||||
|
pub fn all() -> Vec<PermissionsEnum> {
|
||||||
|
vec![
|
||||||
|
// User permissions
|
||||||
|
PermissionsEnum::ReadListUsers,
|
||||||
|
PermissionsEnum::ReadDetailUsers,
|
||||||
|
PermissionsEnum::CreateUsers,
|
||||||
|
PermissionsEnum::DeleteUsers,
|
||||||
|
PermissionsEnum::UpdateUsers,
|
||||||
|
PermissionsEnum::ActivateUsers,
|
||||||
|
|
||||||
|
// Role permissions
|
||||||
|
PermissionsEnum::ReadListRoles,
|
||||||
|
PermissionsEnum::ReadDetailRoles,
|
||||||
|
PermissionsEnum::CreateRoles,
|
||||||
|
PermissionsEnum::DeleteRoles,
|
||||||
|
PermissionsEnum::UpdateRoles,
|
||||||
|
|
||||||
|
// Permission permissions
|
||||||
|
PermissionsEnum::ReadListPermissions,
|
||||||
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
|
PermissionsEnum::CreatePermissions,
|
||||||
|
PermissionsEnum::DeletePermissions,
|
||||||
|
PermissionsEnum::UpdatePermissions,
|
||||||
|
|
||||||
|
// Team permissions
|
||||||
|
PermissionsEnum::ReadListTeams,
|
||||||
|
PermissionsEnum::ReadDetailTeams,
|
||||||
|
|
||||||
|
// Gacha permissions
|
||||||
|
PermissionsEnum::CreateGachaClaims,
|
||||||
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
|
PermissionsEnum::ReadListGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::CreateGachaItems,
|
||||||
|
PermissionsEnum::DeleteGachaItems,
|
||||||
|
PermissionsEnum::UpdateGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
PermissionsEnum::CreateGachaRolls,
|
||||||
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
PermissionsEnum::DeleteGachaRolls,
|
||||||
|
|
||||||
|
// Mentor permissions
|
||||||
|
PermissionsEnum::ReadListMentors,
|
||||||
|
PermissionsEnum::ReadDetailMentors,
|
||||||
|
PermissionsEnum::RegisterMentors,
|
||||||
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
|
PermissionsEnum::UpdateMentors,
|
||||||
|
PermissionsEnum::VerifyMentors,
|
||||||
|
PermissionsEnum::DeleteMentors,
|
||||||
|
|
||||||
|
// Administrator permissions
|
||||||
|
PermissionsEnum::ManageAllUsers,
|
||||||
|
PermissionsEnum::ManageAllRoles,
|
||||||
|
PermissionsEnum::ManageAllPermissions,
|
||||||
|
PermissionsEnum::ManageAllTeams,
|
||||||
|
PermissionsEnum::ViewAllSensitiveData,
|
||||||
|
PermissionsEnum::AccessAdminDashboard,
|
||||||
|
PermissionsEnum::Administrator,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct PermissionsItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PermissionsItemDto {
|
||||||
|
pub fn from(dto: &PermissionsQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id.as_ref().map(|id| id.id.to_raw()).unwrap_or_default(),
|
||||||
|
name: dto.name.clone().unwrap_or_default(),
|
||||||
|
created_at: dto.created_at.clone(),
|
||||||
|
updated_at: dto.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PermissionsQueryDto {
|
||||||
|
pub id: Option<Thing>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use crate::permissions::{PermissionsQueryDto, PermissionsItemDto};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ExperienceDto {
|
||||||
|
pub id: String,
|
||||||
|
pub company: String,
|
||||||
|
pub position: String,
|
||||||
|
pub duration: String,
|
||||||
|
pub period: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EducationDto {
|
||||||
|
pub id: String,
|
||||||
|
pub institution: String,
|
||||||
|
pub degree: String,
|
||||||
|
pub field: String,
|
||||||
|
pub period: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RolesDetailQueryDto {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub permissions: Option<Vec<Option<PermissionsQueryDto>>>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RolesDetailQueryDto {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Thing::from(("".to_string(), surrealdb::sql::Id::Number(0))),
|
||||||
|
name: String::new(),
|
||||||
|
permissions: None,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)]
|
||||||
|
pub struct RolesDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub permissions: Vec<PermissionsItemDto>,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RolesDetailItemDto {
|
||||||
|
pub fn from(dto: &RolesDetailQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id.id.to_raw(),
|
||||||
|
name: dto.name.clone(),
|
||||||
|
is_deleted: dto.is_deleted,
|
||||||
|
permissions: dto
|
||||||
|
.permissions
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&vec![])
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| p.as_ref())
|
||||||
|
.map(PermissionsItemDto::from)
|
||||||
|
.collect(),
|
||||||
|
created_at: dto.created_at.clone(),
|
||||||
|
updated_at: dto.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UsersDetailQueryDto {
|
||||||
|
pub id: Thing,
|
||||||
|
pub fullname: String,
|
||||||
|
pub legal_name: Option<String>,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
pub domicile: 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>,
|
||||||
|
pub website_url: Option<String>,
|
||||||
|
pub twitter_url: Option<String>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub skills: Option<Vec<String>>,
|
||||||
|
pub experience: Option<Vec<ExperienceDto>>,
|
||||||
|
pub education: Option<Vec<EducationDto>>,
|
||||||
|
pub career_status: Option<String>,
|
||||||
|
pub password: String,
|
||||||
|
pub role: RolesDetailQueryDto,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub mentor_id: Option<Thing>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsersDetailQueryDto {
|
||||||
|
pub fn from(self) -> Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,43 @@
|
|||||||
pub mod v1;
|
pub mod v1;
|
||||||
|
|
||||||
pub use imphnen_entities::*;
|
// Re-export core entity types used across the gacha system
|
||||||
pub use imphnen_libs::*;
|
pub use imphnen_entities::{
|
||||||
pub use imphnen_utils::*;
|
CountResult,
|
||||||
pub use v1::*;
|
Error,
|
||||||
|
ExperienceDto,
|
||||||
|
EducationDto,
|
||||||
|
MessageResponseDto,
|
||||||
|
MetaRequestDto,
|
||||||
|
MetaResponseDto,
|
||||||
|
PermissionsEnum,
|
||||||
|
PermissionsItemDto,
|
||||||
|
PermissionsQueryDto,
|
||||||
|
ResponseListSuccessDto,
|
||||||
|
ResponseSuccessDto,
|
||||||
|
UsersDetailQueryDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Explicitly import only what we need from libs and utils to avoid pollution
|
||||||
|
pub use imphnen_libs::{
|
||||||
|
AppState,
|
||||||
|
MinioService,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use imphnen_utils::{
|
||||||
|
bind_filter,
|
||||||
|
csrf_token,
|
||||||
|
extract_email,
|
||||||
|
generate_date,
|
||||||
|
generate_otp,
|
||||||
|
get_id,
|
||||||
|
logger,
|
||||||
|
make_thing,
|
||||||
|
query_builder,
|
||||||
|
query_list,
|
||||||
|
response_format,
|
||||||
|
serde_helpers,
|
||||||
|
validator,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export public v1 API
|
||||||
|
pub use v1::gacha_router;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use axum::http::HeaderMap;
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Json, extract::Path};
|
use axum::{Json, extract::Path};
|
||||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||||
use imphnen_libs::{AppState, MessageResponseDto, ResponseSuccessDto};
|
use crate::AppState;
|
||||||
|
use imphnen_entities::{MessageResponseDto, ResponseSuccessDto};
|
||||||
use super::{GachaClaimItemDto, GachaClaimRequestDto, GachaClaimService};
|
use crate::v1::gacha_claims::{GachaClaimItemDto, GachaClaimRequestDto, GachaClaimService};
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -15,7 +15,7 @@ use super::{GachaClaimItemDto, GachaClaimRequestDto, GachaClaimService};
|
|||||||
),
|
),
|
||||||
params(("id" = String, Path, description = "Gacha Claim ID")),
|
params(("id" = String, Path, description = "Gacha Claim ID")),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get Gacha Claim by ID", body = ResponseSuccessDto<GachaClaimItemDto>)
|
(status = 200, description = "[ADMIN] Get Gacha Claim by ID", body = ResponseSuccessDto<GachaClaimItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -44,7 +44,7 @@ pub async fn get_detail_gacha_claim(
|
|||||||
path = "/v1/gacha/claims/create",
|
path = "/v1/gacha/claims/create",
|
||||||
request_body = GachaClaimRequestDto,
|
request_body = GachaClaimRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "Create new gacha claim", body = MessageResponseDto)
|
(status = 201, description = "[ADMIN] Create new gacha claim", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
use crate::{GachaItemDto, GachaItemSchema};
|
use crate::v1::gacha_items::GachaItemDto;
|
||||||
|
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||||
use imphnen_iam::{UsersDetailItemDto, UsersDetailQueryDto};
|
use imphnen_iam::{UsersDetailItemDto, UsersDetailQueryDto};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use validator::Validate;
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
// Custom validator for user ID format (UUID-like validation)
|
||||||
|
pub fn validate_user_id_format(user_id: &str) -> Result<(), ValidationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref UUID_REGEX: Regex = Regex::new(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$").unwrap();
|
||||||
|
}
|
||||||
|
if UUID_REGEX.is_match(user_id) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("invalid_format"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct GachaClaimRequestDto {
|
pub struct GachaClaimRequestDto {
|
||||||
#[validate(length(min = 1, message = "User ID must not be empty"))]
|
#[validate(length(min = 1, message = "User ID must not be empty"))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_user_id_format",
|
||||||
|
message = "User ID must be a valid UUID"
|
||||||
|
))]
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
|
|
||||||
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
||||||
pub item_id: String,
|
pub item_id: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::{GachaClaimQueryDto, GachaClaimSchema};
|
use crate::v1::gacha_claims::gacha_claims_dto::GachaClaimQueryDto;
|
||||||
use crate::{AppState, ResourceEnum};
|
use crate::v1::gacha_claims::gacha_claims_schema::GachaClaimSchema;
|
||||||
|
use crate::AppState;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use imphnen_iam::DetailQueryBuilder;
|
use imphnen_iam::DetailQueryBuilder;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use crate::{GachaRollQueryDto, ResourceEnum, make_thing};
|
use crate::v1::gacha_rolls::gacha_rolls_dto::GachaRollQueryDto;
|
||||||
|
use crate::{make_thing};
|
||||||
use imphnen_iam::get_iso_date;
|
use imphnen_iam::get_iso_date;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::{Uuid, sql::Thing};
|
use surrealdb::{Uuid, sql::Thing};
|
||||||
|
|
||||||
use super::GachaClaimRequestDto;
|
use crate::v1::gacha_claims::gacha_claims_dto::GachaClaimRequestDto;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct GachaClaimSchema {
|
pub struct GachaClaimSchema {
|
||||||
@@ -57,7 +59,12 @@ impl GachaClaimSchema {
|
|||||||
&Uuid::new_v4().to_string(),
|
&Uuid::new_v4().to_string(),
|
||||||
),
|
),
|
||||||
user: user_id,
|
user: user_id,
|
||||||
item: roll.item.id.clone(),
|
// roll.item is optional at the DTO level; assume caller ensured a valid item exists
|
||||||
|
item: roll
|
||||||
|
.item
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.id.clone())
|
||||||
|
.unwrap_or_else(|| make_thing(&ResourceEnum::GachaItems.to_string(), &Uuid::new_v4().to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::{
|
use crate::AppState;
|
||||||
AppState, GachaClaimItemDto, GachaClaimRepository, GachaClaimRequestDto,
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
GachaClaimSchema, ResponseSuccessDto, common_response, success_response,
|
use imphnen_utils::{common_response, success_response, validate_request};
|
||||||
validate_request,
|
use crate::v1::gacha_claims::gacha_claims_dto::{GachaClaimItemDto, GachaClaimRequestDto};
|
||||||
};
|
use crate::v1::gacha_claims::gacha_claims_repository::GachaClaimRepository;
|
||||||
|
use crate::v1::gacha_claims::gacha_claims_schema::GachaClaimSchema;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod gacha_claims_controller;
|
pub mod gacha_claims_controller;
|
||||||
@@ -9,14 +9,14 @@ pub mod gacha_claims_repository;
|
|||||||
pub mod gacha_claims_schema;
|
pub mod gacha_claims_schema;
|
||||||
pub mod gacha_claims_service;
|
pub mod gacha_claims_service;
|
||||||
|
|
||||||
pub use gacha_claims_controller::*;
|
// Export only public API functions
|
||||||
pub use gacha_claims_dto::*;
|
pub use gacha_claims_controller::{post_create_gacha_claim, get_detail_gacha_claim};
|
||||||
pub use gacha_claims_repository::*;
|
pub use gacha_claims_dto::{GachaClaimItemDto, GachaClaimRequestDto};
|
||||||
pub use gacha_claims_schema::*;
|
pub use gacha_claims_service::GachaClaimService;
|
||||||
pub use gacha_claims_service::*;
|
|
||||||
|
|
||||||
|
/// Creates router for gacha claims endpoints
|
||||||
pub fn gacha_claim_router() -> Router {
|
pub fn gacha_claim_router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/create", post(post_create_gacha_claim))
|
.route("/create", post(post_create_gacha_claim))
|
||||||
.route("/detail/{id}", get(get_detail_gacha_claim))
|
.route("/detail/{id}", get(get_detail_gacha_claim))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::Json,
|
||||||
|
http::HeaderMap,
|
||||||
|
response::Response,
|
||||||
|
Extension,
|
||||||
|
};
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::v1::gacha_credits::gacha_credits_dto::GachaCreditRequestDto;
|
||||||
|
use crate::v1::gacha_credits::gacha_credits_service::GachaCreditService;
|
||||||
|
|
||||||
|
pub struct GachaCreditController;
|
||||||
|
|
||||||
|
impl GachaCreditController {
|
||||||
|
pub async fn get_user_credits(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Response {
|
||||||
|
GachaCreditService::get_user_credits(&headers, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_user_credits(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<GachaCreditRequestDto>,
|
||||||
|
) -> Response {
|
||||||
|
GachaCreditService::add_user_credits(&headers, &state, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn consume_user_credit(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Response {
|
||||||
|
GachaCreditService::consume_user_credit(&headers, &state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,38 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct GachaCreditRequestDto {
|
pub struct GachaCreditRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "User ID must not be empty"))]
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
|
|
||||||
|
#[validate(range(
|
||||||
|
min = 1,
|
||||||
|
message = "Amount must be at least 1 credit"
|
||||||
|
))]
|
||||||
pub amount: i32,
|
pub amount: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GachaCreditResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub available_rolls: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&crate::v1::gacha_credits::gacha_credits_schema::GachaCreditSchema> for GachaCreditResponseDto {
|
||||||
|
fn from(credit: &crate::v1::gacha_credits::gacha_credits_schema::GachaCreditSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: credit.id.id.to_raw(),
|
||||||
|
user_id: credit.user.id.to_raw(),
|
||||||
|
available_rolls: credit.available_rolls,
|
||||||
|
is_deleted: credit.is_deleted,
|
||||||
|
created_at: credit.created_at.clone(),
|
||||||
|
updated_at: credit.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::{GachaCreditRequestDto, GachaCreditSchema};
|
use crate::v1::gacha_credits::gacha_credits_dto::GachaCreditRequestDto;
|
||||||
use crate::{AppState, ResourceEnum};
|
use crate::v1::gacha_credits::gacha_credits_schema::GachaCreditSchema;
|
||||||
|
use crate::AppState;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use imphnen_iam::make_thing;
|
use imphnen_iam::make_thing;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -23,9 +25,9 @@ impl<'a> GachaCreditRepository<'a> {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1",
|
"SELECT * FROM {} WHERE user = type::thing('{}', $user_id) AND is_deleted = false LIMIT 1",
|
||||||
ResourceEnum::GachaCredits,
|
ResourceEnum::GachaCredits,
|
||||||
ResourceEnum::Users
|
ResourceEnum::Users.as_str()
|
||||||
);
|
);
|
||||||
info!(query = %sql, "Executing SurrealDB query");
|
info!(query = %sql, "Executing SurrealDB query");
|
||||||
let result: Vec<GachaCreditSchema> =
|
let result: Vec<GachaCreditSchema> =
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use axum::{Router, routing::get};
|
||||||
|
use axum::routing::post;
|
||||||
|
|
||||||
|
pub fn gacha_credit_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(crate::v1::gacha_credits::GachaCreditController::get_user_credits))
|
||||||
|
.route("/add", post(crate::v1::gacha_credits::GachaCreditController::add_user_credits))
|
||||||
|
.route("/consume", post(crate::v1::gacha_credits::GachaCreditController::consume_user_credit))
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
|
use imphnen_utils::{errors::AppError, error_response};
|
||||||
|
use imphnen_utils::{common_response, success_response, validate_request};
|
||||||
|
use crate::v1::gacha_credits::gacha_credits_dto::{GachaCreditRequestDto, GachaCreditResponseDto};
|
||||||
|
use crate::v1::gacha_credits::gacha_credits_repository::GachaCreditRepository;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::Response;
|
||||||
|
use imphnen_iam::UsersRepository;
|
||||||
|
use imphnen_utils::extract_email;
|
||||||
|
|
||||||
|
pub struct GachaCreditService;
|
||||||
|
|
||||||
|
impl GachaCreditService {
|
||||||
|
pub async fn get_user_credits(headers: &axum::http::HeaderMap, state: &AppState) -> Response {
|
||||||
|
let repo = GachaCreditRepository::new(state);
|
||||||
|
let repo_user = UsersRepository::new(state);
|
||||||
|
let Some(email) = extract_email(headers) else {
|
||||||
|
return error_response(AppError::AuthenticationError("Unauthorized".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
||||||
|
return error_response(AppError::NotFoundError("User not found".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
match repo.query_by_user_id(user.id.id.to_raw()).await {
|
||||||
|
Ok(Some(credit)) => {
|
||||||
|
let response_dto = GachaCreditResponseDto::from(&credit);
|
||||||
|
success_response(ResponseSuccessDto { data: response_dto })
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Return empty credits if no record exists
|
||||||
|
let response_dto = GachaCreditResponseDto {
|
||||||
|
id: "".to_string(),
|
||||||
|
user_id: user.id.id.to_raw(),
|
||||||
|
available_rolls: 0,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
};
|
||||||
|
success_response(ResponseSuccessDto { data: response_dto })
|
||||||
|
}
|
||||||
|
Err(e) => error_response(AppError::InternalServerError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_user_credits(
|
||||||
|
headers: &axum::http::HeaderMap,
|
||||||
|
state: &AppState,
|
||||||
|
payload: GachaCreditRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let repo = GachaCreditRepository::new(state);
|
||||||
|
let repo_user = UsersRepository::new(state);
|
||||||
|
let Some(email) = extract_email(headers) else {
|
||||||
|
return error_response(AppError::AuthenticationError("Unauthorized".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
||||||
|
return error_response(AppError::NotFoundError("User not found".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure the user can only modify their own credits
|
||||||
|
if payload.user_id != user.id.id.to_raw() {
|
||||||
|
return error_response(AppError::AuthorizationError("You can only modify your own credits".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let amount = payload.amount; // Extract amount before moving payload
|
||||||
|
match repo.query_add_credit(payload).await {
|
||||||
|
Ok(_) => common_response(
|
||||||
|
StatusCode::OK,
|
||||||
|
&format!("Added {} credits successfully", amount)
|
||||||
|
),
|
||||||
|
Err(e) => error_response(AppError::InternalServerError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn consume_user_credit(headers: &axum::http::HeaderMap, state: &AppState) -> Response {
|
||||||
|
let repo = GachaCreditRepository::new(state);
|
||||||
|
let repo_user = UsersRepository::new(state);
|
||||||
|
let Some(email) = extract_email(headers) else {
|
||||||
|
return error_response(AppError::AuthenticationError("Unauthorized".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
||||||
|
return error_response(AppError::NotFoundError("User not found".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
match repo.query_consume_credit(user.id.id.to_raw()).await {
|
||||||
|
Ok(_) => common_response(StatusCode::OK, "Consumed 1 credit successfully"),
|
||||||
|
Err(e) => error_response(AppError::BadRequestError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
|
pub mod gacha_credits_controller;
|
||||||
pub mod gacha_credits_dto;
|
pub mod gacha_credits_dto;
|
||||||
pub mod gacha_credits_repository;
|
pub mod gacha_credits_repository;
|
||||||
pub mod gacha_credits_schema;
|
pub mod gacha_credits_schema;
|
||||||
|
pub mod gacha_credits_service;
|
||||||
|
pub mod gacha_credits_router;
|
||||||
|
|
||||||
pub use gacha_credits_dto::*;
|
// Export only public types and functions
|
||||||
pub use gacha_credits_repository::*;
|
pub use gacha_credits_controller::GachaCreditController;
|
||||||
pub use gacha_credits_schema::*;
|
pub use gacha_credits_dto::{GachaCreditRequestDto, GachaCreditResponseDto};
|
||||||
|
pub use gacha_credits_repository::GachaCreditRepository;
|
||||||
|
pub use gacha_credits_service::GachaCreditService;
|
||||||
|
pub use gacha_credits_router::gacha_credit_router;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
use crate::{
|
use crate::{AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||||
AppState, GachaItemDto, GachaItemRequestDto, GachaItemUpdateRequestDto, GachaItemService, MessageResponseDto,
|
use imphnen_entities::MessageResponseDto;
|
||||||
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
use crate::v1::gacha_items::GachaItemDto;
|
||||||
};
|
use crate::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
||||||
|
use crate::v1::gacha_items::gacha_items_service::GachaItemService;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Json,
|
Extension,
|
||||||
extract::{Path, Query},
|
extract::{Path, Query},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
use imphnen_libs::ValidatedJson;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -26,7 +28,7 @@ use imphnen_iam::{PermissionsEnum, permissions_guard};
|
|||||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get gacha item list", body = ResponseListSuccessDto<Vec<GachaItemDto>>)
|
(status = 200, description = "[ADMIN] Get gacha item list", body = ResponseListSuccessDto<Vec<GachaItemDto>>)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -35,16 +37,9 @@ pub async fn get_gacha_item_list(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::ReadListGachaItems], {
|
||||||
headers,
|
GachaItemService::get_gacha_item_list(&state, meta).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::ReadListGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::get_gacha_item_list(&state, meta).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -55,7 +50,7 @@ pub async fn get_gacha_item_list(
|
|||||||
),
|
),
|
||||||
params(("id" = String, Path, description = "Gacha Item ID")),
|
params(("id" = String, Path, description = "Gacha Item ID")),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get gacha item by ID", body = ResponseSuccessDto<GachaItemDto>)
|
(status = 200, description = "[ADMIN] Get gacha item by ID", body = ResponseSuccessDto<GachaItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -64,16 +59,9 @@ pub async fn get_gacha_item_by_id(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::ReadDetailGachaItems], {
|
||||||
headers,
|
GachaItemService::get_gacha_item_by_id(&state, id).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::ReadDetailGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::get_gacha_item_by_id(&state, id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -84,25 +72,18 @@ pub async fn get_gacha_item_by_id(
|
|||||||
),
|
),
|
||||||
request_body = GachaItemRequestDto,
|
request_body = GachaItemRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "Create gacha item", body = MessageResponseDto)
|
(status = 201, description = "[ADMIN] Create gacha item", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
pub async fn post_create_gacha_item(
|
pub async fn post_create_gacha_item(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<GachaItemRequestDto>,
|
ValidatedJson(payload): ValidatedJson<GachaItemRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::CreateGachaItems], {
|
||||||
headers,
|
GachaItemService::create_gacha_item(&state, payload).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::CreateGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::create_gacha_item(&state, payload).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -113,7 +94,7 @@ pub async fn post_create_gacha_item(
|
|||||||
),
|
),
|
||||||
request_body = GachaItemUpdateRequestDto,
|
request_body = GachaItemUpdateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Update gacha item", body = MessageResponseDto)
|
(status = 200, description = "[ADMIN] Update gacha item", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -121,18 +102,11 @@ pub async fn put_update_gacha_item(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(payload): Json<GachaItemUpdateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<GachaItemUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::UpdateGachaItems], {
|
||||||
headers,
|
GachaItemService::update_gacha_item(&state, payload, id).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::UpdateGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::update_gacha_item(&state, payload, id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -142,7 +116,7 @@ pub async fn put_update_gacha_item(
|
|||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Delete gacha item", body = MessageResponseDto)
|
(status = 200, description = "[ADMIN] Delete gacha item", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -151,14 +125,7 @@ pub async fn delete_gacha_item(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::DeleteGachaItems], {
|
||||||
headers,
|
GachaItemService::delete_gacha_item(&state, id).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::DeleteGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::delete_gacha_item(&state, id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,46 @@
|
|||||||
use super::GachaItemSchema;
|
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use validator::Validate;
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
// Custom validator for image URLs
|
||||||
|
pub fn validate_image_url(url: &str) -> Result<(), ValidationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref IMAGE_URL_REGEX: Regex = Regex::new(r"^https?://[^\s]+\.(jpg|jpeg|png|gif|webp)$").unwrap();
|
||||||
|
}
|
||||||
|
if IMAGE_URL_REGEX.is_match(url) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("invalid_image_url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct GachaItemRequestDto {
|
pub struct GachaItemRequestDto {
|
||||||
#[validate(length(min = 1, message = "Item name must not be empty"))]
|
#[validate(length(min = 1, max = 100, message = "Item name must be between 1 and 100 characters"))]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
#[validate(length(min = 1, message = "Image URL must not be empty"))]
|
#[validate(length(min = 1, message = "Image URL must not be empty"))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_image_url",
|
||||||
|
message = "Image URL must be a valid URL pointing to JPG, JPEG, PNG, GIF, or WebP image"
|
||||||
|
))]
|
||||||
pub image_url: String,
|
pub image_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct GachaItemUpdateRequestDto {
|
pub struct GachaItemUpdateRequestDto {
|
||||||
#[validate(length(min = 1, message = "Item name must not be empty"))]
|
#[validate(length(min = 1, max = 100, message = "Item name must be between 1 and 100 characters"))]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
||||||
#[validate(length(min = 1, message = "Image URL must not be empty"))]
|
#[validate(length(min = 1, message = "Image URL must not be empty"))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_image_url",
|
||||||
|
message = "Image URL must be a valid URL pointing to JPG, JPEG, PNG, GIF, or WebP image"
|
||||||
|
))]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub image_url: Option<String>,
|
pub image_url: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -34,10 +58,10 @@ impl GachaItemDto {
|
|||||||
pub fn from(dto: GachaItemSchema) -> Self {
|
pub fn from(dto: GachaItemSchema) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: dto.id.id.to_raw(),
|
id: dto.id.id.to_raw(),
|
||||||
name: dto.name.clone(),
|
name: dto.name,
|
||||||
is_deleted: dto.is_deleted,
|
is_deleted: dto.is_deleted,
|
||||||
created_at: dto.created_at.clone(),
|
created_at: dto.created_at,
|
||||||
updated_at: dto.updated_at.clone(),
|
updated_at: dto.updated_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use super::GachaItemSchema;
|
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||||
use crate::{
|
use crate::{AppState, MetaRequestDto, ResponseListSuccessDto, get_id, make_thing};
|
||||||
AppState, GachaItemDto, MetaRequestDto, ResourceEnum, ResponseListSuccessDto,
|
use crate::v1::gacha_items::GachaItemDto;
|
||||||
get_id, make_thing,
|
use imphnen_libs::ResourceEnum;
|
||||||
};
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use imphnen_iam::QueryListBuilder;
|
use imphnen_iam::QueryListBuilder;
|
||||||
use imphnen_utils::get_iso_date;
|
use imphnen_utils::get_iso_date;
|
||||||
@@ -28,7 +27,7 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let surreal_query = format!(
|
let surreal_query = format!(
|
||||||
"SELECT * FROM {} WHERE is_deleted = false AND name LIKE ?",
|
"SELECT * FROM {} WHERE is_deleted = false AND name LIKE ?",
|
||||||
ResourceEnum::GachaItems.to_string()
|
ResourceEnum::GachaItems
|
||||||
);
|
);
|
||||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
||||||
@@ -63,7 +62,7 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let surreal_query = format!("SELECT * FROM {} WHERE id = '{}'", ResourceEnum::GachaItems.to_string(), id);
|
let surreal_query = format!("SELECT * FROM {} WHERE id = '{}'", ResourceEnum::GachaItems, id);
|
||||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let result: Option<GachaItemSchema> = db
|
let result: Option<GachaItemSchema> = db
|
||||||
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
||||||
@@ -87,7 +86,7 @@ impl<'a> GachaItemRepository<'a> {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let surreal_query = format!("CREATE {} CONTENT ...", ResourceEnum::GachaItems.to_string());
|
let surreal_query = format!("CREATE {} CONTENT ...", ResourceEnum::GachaItems);
|
||||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||||
let record: Option<GachaItemSchema> = db
|
let record: Option<GachaItemSchema> = db
|
||||||
.create(ResourceEnum::GachaItems.to_string())
|
.create(ResourceEnum::GachaItems.to_string())
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use crate::{ResourceEnum, make_thing};
|
use crate::make_thing;
|
||||||
use imphnen_iam::get_iso_date;
|
use imphnen_iam::get_iso_date;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::{Uuid, sql::Thing};
|
use surrealdb::{Uuid, sql::Thing};
|
||||||
|
|
||||||
use super::GachaItemRequestDto;
|
use crate::v1::gacha_items::gacha_items_dto::GachaItemRequestDto;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct GachaItemSchema {
|
pub struct GachaItemSchema {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use crate::{
|
use crate::AppState;
|
||||||
AppState, GachaItemDto, GachaItemRepository, GachaItemRequestDto, GachaItemUpdateRequestDto, GachaItemSchema,
|
use imphnen_entities::{MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||||
MetaRequestDto, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
use imphnen_utils::{common_response, make_thing, success_list_response, success_response};
|
||||||
common_response, make_thing, success_list_response, success_response,
|
use crate::v1::gacha_items::GachaItemDto;
|
||||||
validate_request,
|
use crate::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
||||||
};
|
use crate::v1::gacha_items::gacha_items_repository::GachaItemRepository;
|
||||||
|
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use imphnen_utils::get_iso_date;
|
use imphnen_utils::get_iso_date;
|
||||||
@@ -42,9 +44,7 @@ impl GachaItemService {
|
|||||||
state: &AppState,
|
state: &AppState,
|
||||||
payload: GachaItemRequestDto,
|
payload: GachaItemRequestDto,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
// Validation is now automatic via ValidatedJson extractor
|
||||||
return common_response(status, &message);
|
|
||||||
}
|
|
||||||
let repo = GachaItemRepository::new(state);
|
let repo = GachaItemRepository::new(state);
|
||||||
let schema = GachaItemSchema {
|
let schema = GachaItemSchema {
|
||||||
id: make_thing(&ResourceEnum::GachaItems.to_string(), &payload.name), // Fixed: Use payload.name or some other identifier
|
id: make_thing(&ResourceEnum::GachaItems.to_string(), &payload.name), // Fixed: Use payload.name or some other identifier
|
||||||
@@ -63,9 +63,7 @@ impl GachaItemService {
|
|||||||
payload: GachaItemUpdateRequestDto,
|
payload: GachaItemUpdateRequestDto,
|
||||||
id: String,
|
id: String,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
// Validation is now automatic via ValidatedJson extractor
|
||||||
return common_response(status, &message);
|
|
||||||
}
|
|
||||||
let repo = GachaItemRepository::new(state);
|
let repo = GachaItemRepository::new(state);
|
||||||
|
|
||||||
// Get current gacha item data first
|
// Get current gacha item data first
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod gacha_items_controller;
|
pub mod gacha_items_controller;
|
||||||
@@ -9,17 +9,22 @@ pub mod gacha_items_repository;
|
|||||||
pub mod gacha_items_schema;
|
pub mod gacha_items_schema;
|
||||||
pub mod gacha_items_service;
|
pub mod gacha_items_service;
|
||||||
|
|
||||||
pub use gacha_items_controller::*;
|
// Export only public API functions and types
|
||||||
pub use gacha_items_dto::*;
|
pub use gacha_items_controller::{
|
||||||
pub use gacha_items_repository::*;
|
get_gacha_item_list,
|
||||||
pub use gacha_items_schema::*;
|
post_create_gacha_item,
|
||||||
pub use gacha_items_service::*;
|
get_gacha_item_by_id,
|
||||||
|
put_update_gacha_item,
|
||||||
|
delete_gacha_item,
|
||||||
|
};
|
||||||
|
pub use gacha_items_dto::GachaItemDto;
|
||||||
|
|
||||||
|
/// Creates router for gacha items endpoints
|
||||||
pub fn gacha_item_router() -> Router {
|
pub fn gacha_item_router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(get_gacha_item_list))
|
.route("/", get(get_gacha_item_list))
|
||||||
.route("/create", post(post_create_gacha_item))
|
.route("/create", post(post_create_gacha_item))
|
||||||
.route("/detail/{id}", get(get_gacha_item_by_id))
|
.route("/detail/{id}", get(get_gacha_item_by_id))
|
||||||
.route("/update/{id}", put(put_update_gacha_item))
|
.route("/update/{id}", put(put_update_gacha_item))
|
||||||
.route("/delete/{id}", delete(delete_gacha_item))
|
.route("/delete/{id}", delete(delete_gacha_item))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::AppState;
|
||||||
AppState, GachaRollItemDto, GachaRollRequestDto, GachaRollService,
|
use imphnen_entities::{MessageResponseDto, ResponseSuccessDto};
|
||||||
MessageResponseDto, ResponseSuccessDto,
|
use crate::v1::gacha_rolls::gacha_rolls_dto::{GachaRollItemDto, GachaRollRequestDto};
|
||||||
};
|
use crate::v1::gacha_rolls::gacha_rolls_service::GachaRollService;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Json, extract::Path, http::HeaderMap, response::IntoResponse,
|
Extension, Json, extract::Path, http::HeaderMap, response::IntoResponse,
|
||||||
};
|
};
|
||||||
@@ -15,7 +15,7 @@ use imphnen_iam::{PermissionsEnum, permissions_guard};
|
|||||||
),
|
),
|
||||||
params(("id" = String, Path, description = "Gacha Roll ID")),
|
params(("id" = String, Path, description = "Gacha Roll ID")),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get Gacha Roll by ID", body = ResponseSuccessDto<GachaRollItemDto>)
|
(status = 200, description = "[ADMIN] Get Gacha Roll by ID", body = ResponseSuccessDto<GachaRollItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -44,7 +44,7 @@ pub async fn get_detail_gacha_roll(
|
|||||||
),
|
),
|
||||||
request_body = GachaRollRequestDto,
|
request_body = GachaRollRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "Create new gacha roll", body = MessageResponseDto)
|
(status = 201, description = "[ADMIN] Create new gacha roll", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -72,7 +72,7 @@ pub async fn post_create_gacha_roll(
|
|||||||
("Bearer" = [])
|
("Bearer" = [])
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Execute and get 1 gacha result", body = ResponseSuccessDto<GachaRollItemDto>)
|
(status = 200, description = "[ADMIN] Execute and get 1 gacha result", body = ResponseSuccessDto<GachaRollItemDto>)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
@@ -100,7 +100,7 @@ pub async fn post_execute_gacha_roll(
|
|||||||
),
|
),
|
||||||
params(("id" = String, Path, description = "Gacha Roll ID")),
|
params(("id" = String, Path, description = "Gacha Roll ID")),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Delete Gacha Roll (soft delete)", body = MessageResponseDto)
|
(status = 200, description = "[ADMIN] Delete Gacha Roll (soft delete)", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Gacha"
|
tag = "Gacha"
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::{GachaItemDto, GachaItemSchema};
|
use crate::v1::gacha_items::GachaItemDto;
|
||||||
|
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::sql::Thing;
|
use surrealdb::sql::Thing;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
@@ -6,10 +7,13 @@ use validator::Validate;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct GachaRollRequestDto {
|
pub struct GachaRollRequestDto {
|
||||||
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
#[validate(length(min = 1, max = 100, message = "Item ID must be between 1 and 100 characters"))]
|
||||||
pub item_id: String,
|
pub item_id: String,
|
||||||
|
|
||||||
|
#[validate(range(min = 0.0, max = 1.0, message = "Weight must be between 0.0 and 1.0"))]
|
||||||
pub weight: f32,
|
pub weight: f32,
|
||||||
#[validate(range(min = 1, message = "Quantity must be at least 1"))]
|
|
||||||
|
#[validate(range(min = 1, max = 100, message = "Quantity must be between 1 and 100"))]
|
||||||
pub quantity: i32,
|
pub quantity: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +32,17 @@ impl GachaRollItemDto {
|
|||||||
pub fn from(dto: &GachaRollQueryDto) -> Self {
|
pub fn from(dto: &GachaRollQueryDto) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: dto.id.id.to_raw(),
|
id: dto.id.id.to_raw(),
|
||||||
item: GachaItemDto::from(dto.item.clone()),
|
// Handle case where item might be missing
|
||||||
|
item: match &dto.item {
|
||||||
|
Some(item) => GachaItemDto::from(item.clone()),
|
||||||
|
None => GachaItemDto {
|
||||||
|
id: "".to_string(),
|
||||||
|
name: "Unknown".to_string(),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
},
|
||||||
weight: dto.weight,
|
weight: dto.weight,
|
||||||
quantity: dto.quantity,
|
quantity: dto.quantity,
|
||||||
is_deleted: dto.is_deleted,
|
is_deleted: dto.is_deleted,
|
||||||
@@ -41,7 +55,8 @@ impl GachaRollItemDto {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct GachaRollQueryDto {
|
pub struct GachaRollQueryDto {
|
||||||
pub id: Thing,
|
pub id: Thing,
|
||||||
pub item: GachaItemSchema,
|
// item can be missing in the DB (during partial queries); make optional to allow graceful handling
|
||||||
|
pub item: Option<GachaItemSchema>,
|
||||||
pub weight: f32,
|
pub weight: f32,
|
||||||
pub quantity: i32,
|
pub quantity: i32,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use super::GachaRollQueryDto;
|
use crate::v1::gacha_rolls::gacha_rolls_dto::GachaRollQueryDto;
|
||||||
use super::GachaRollSchema;
|
use crate::v1::gacha_rolls::gacha_rolls_schema::GachaRollSchema;
|
||||||
use crate::{AppState, DetailQueryBuilder, ResourceEnum, get_id, make_thing};
|
use crate::AppState;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
|
use imphnen_utils::DetailQueryBuilder;
|
||||||
|
use crate::{get_id, make_thing};
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
|
||||||
use imphnen_utils::get_iso_date;
|
use imphnen_utils::get_iso_date;
|
||||||
use rand_distr::weighted::WeightedIndex;
|
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -78,36 +80,68 @@ impl<'a> GachaRollRepository<'a> {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let table_name = ResourceEnum::GachaRolls.to_string();
|
let table_name = ResourceEnum::GachaRolls.to_string();
|
||||||
let sql =
|
|
||||||
format!("SELECT * FROM {table_name} WHERE is_deleted = false FETCH item");
|
// Use DetailQueryBuilder to properly fetch related item data
|
||||||
info!(query = %sql, "Executing SurrealDB query");
|
let builder = DetailQueryBuilder::new(table_name)
|
||||||
let result: Vec<GachaRollQueryDto> = db.query(sql).await?.take(0)?;
|
.with_condition("is_deleted = false AND quantity > 0")
|
||||||
|
.with_select_fields(vec!["*"])
|
||||||
|
.with_fetch("item");
|
||||||
|
let sql = builder.build();
|
||||||
|
info!(query = %sql, "Executing SurrealDB query for active rolls");
|
||||||
|
|
||||||
|
let mut result = builder.apply_bindings(db.query(sql)).await?;
|
||||||
|
let results = match result.take(0) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return Ok(Vec::new()),
|
||||||
|
};
|
||||||
|
|
||||||
let elapsed = now.elapsed();
|
let elapsed = now.elapsed();
|
||||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||||
== "development"
|
== "development"
|
||||||
{
|
{
|
||||||
println!("Query 'query_all_active_rolls' took: {elapsed:.2?}");
|
println!("Query 'query_all_active_rolls' took: {elapsed:.2?}");
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub fn roll_once(rolls: &[GachaRollQueryDto]) -> Option<GachaRollQueryDto> {
|
pub fn roll_once(rolls: &[GachaRollQueryDto]) -> Option<GachaRollQueryDto> {
|
||||||
let filtered: Vec<_> = rolls
|
let filtered: Vec<GachaRollQueryDto> = rolls
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|r| !r.is_deleted && r.quantity > 0)
|
.filter(|r| !r.is_deleted && r.quantity > 0)
|
||||||
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
let weights: Vec<f32> = filtered
|
|
||||||
.iter()
|
if filtered.is_empty() {
|
||||||
.map(|r| r.weight * r.quantity as f32)
|
|
||||||
.collect();
|
|
||||||
if weights.iter().all(|&w| w <= 0.0) {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let dist = WeightedIndex::new(&weights).ok()?;
|
|
||||||
|
// Simple random selection based on quantity weights
|
||||||
|
let total_weight: f32 = filtered.iter()
|
||||||
|
.map(|r| r.weight * r.quantity as f32)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
if total_weight <= 0.0 {
|
||||||
|
// Fallback to equal probability if weights are invalid
|
||||||
|
let mut rng = rand::rngs::ThreadRng::default();
|
||||||
|
let index = rng.random_range(0..filtered.len());
|
||||||
|
return Some(filtered[index].clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weighted random selection
|
||||||
let mut rng = rand::rngs::ThreadRng::default();
|
let mut rng = rand::rngs::ThreadRng::default();
|
||||||
let index = dist.sample(&mut rng);
|
let random_value = rng.random_range(0.0..total_weight);
|
||||||
Some(filtered[index].clone())
|
|
||||||
|
let mut cumulative_weight = 0.0;
|
||||||
|
for roll in &filtered {
|
||||||
|
cumulative_weight += roll.weight * roll.quantity as f32;
|
||||||
|
if random_value <= cumulative_weight {
|
||||||
|
return Some(roll.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should rarely happen but provides a fallback
|
||||||
|
Some(filtered[0].clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, id), err)]
|
#[instrument(skip(self, id), err)]
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use crate::{ResourceEnum, make_thing};
|
use crate::make_thing;
|
||||||
use imphnen_iam::get_iso_date;
|
use imphnen_iam::get_iso_date;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use surrealdb::{Uuid, sql::Thing};
|
use surrealdb::{Uuid, sql::Thing};
|
||||||
|
|
||||||
use super::GachaRollRequestDto;
|
use crate::v1::gacha_rolls::gacha_rolls_dto::GachaRollRequestDto;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct GachaRollSchema {
|
pub struct GachaRollSchema {
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
use crate::{
|
use crate::AppState;
|
||||||
AppState, GachaClaimRepository, GachaClaimSchema, GachaRollItemDto,
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
GachaRollRepository, GachaRollRequestDto, GachaRollSchema, ResponseSuccessDto,
|
use imphnen_utils::{common_response, success_response, validate_request};
|
||||||
common_response, success_response, validate_request,
|
use crate::v1::gacha_claims::gacha_claims_repository::GachaClaimRepository;
|
||||||
};
|
use crate::v1::gacha_claims::gacha_claims_schema::GachaClaimSchema;
|
||||||
|
use crate::v1::gacha_rolls::gacha_rolls_dto::{GachaRollItemDto, GachaRollRequestDto};
|
||||||
|
use crate::v1::gacha_rolls::gacha_rolls_repository::GachaRollRepository;
|
||||||
|
use crate::v1::gacha_rolls::gacha_rolls_schema::GachaRollSchema;
|
||||||
|
use crate::v1::gacha_credits::gacha_credits_repository::GachaCreditRepository;
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use imphnen_iam::{UsersRepository, extract_email};
|
use imphnen_iam::UsersRepository;
|
||||||
|
use imphnen_utils::extract_email;
|
||||||
|
|
||||||
pub struct GachaRollService;
|
pub struct GachaRollService;
|
||||||
|
|
||||||
@@ -36,33 +41,63 @@ impl GachaRollService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_roll_once(headers: HeaderMap, state: &AppState) -> Response {
|
pub async fn execute_roll_once(headers: HeaderMap, state: &AppState) -> Response {
|
||||||
let repo = GachaRollRepository::new(state);
|
let repo = GachaRollRepository::new(state);
|
||||||
let repo_claim = GachaClaimRepository::new(state);
|
let repo_claim = GachaClaimRepository::new(state);
|
||||||
let repo_user = UsersRepository::new(state);
|
let repo_user = UsersRepository::new(state);
|
||||||
let Some(email) = extract_email(&headers) else {
|
let repo_credits = GachaCreditRepository::new(state);
|
||||||
return common_response(StatusCode::UNAUTHORIZED, "Unauthorized");
|
let Some(email) = extract_email(&headers) else {
|
||||||
};
|
return common_response(StatusCode::UNAUTHORIZED, "Unauthorized");
|
||||||
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
};
|
||||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
||||||
};
|
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||||
match repo.query_all_active_rolls().await {
|
};
|
||||||
Ok(rolls) => match GachaRollRepository::roll_once(&rolls) {
|
|
||||||
Some(roll) => {
|
// Check if user has enough credits
|
||||||
let claim = GachaClaimSchema::roll(roll.clone(), user.id);
|
let credit_opt = repo_credits.query_by_user_id(user.id.id.to_raw()).await;
|
||||||
match repo_claim.query_create_gacha_claim(claim).await {
|
let has_enough_credits = match credit_opt {
|
||||||
Ok(_) => success_response(ResponseSuccessDto {
|
Ok(Some(credit)) => credit.available_rolls > 0,
|
||||||
data: GachaRollItemDto::from(&roll),
|
Ok(None) => false, // No credit record means no credits
|
||||||
}),
|
Err(e) => {
|
||||||
Err(e) => {
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !has_enough_credits {
|
||||||
|
return common_response(StatusCode::PAYMENT_REQUIRED, "Not enough credits to perform this action");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume one credit
|
||||||
|
match repo_credits.query_consume_credit(user.id.id.to_raw()).await {
|
||||||
|
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with the roll
|
||||||
|
match repo.query_all_active_rolls().await {
|
||||||
|
Ok(rolls) => match GachaRollRepository::roll_once(&rolls) {
|
||||||
|
Some(roll) => {
|
||||||
|
let user_id_clone = user.id.clone();
|
||||||
|
let claim = GachaClaimSchema::roll(roll.clone(), user_id_clone);
|
||||||
|
match repo_claim.query_create_gacha_claim(claim).await {
|
||||||
|
Ok(_) => success_response(ResponseSuccessDto {
|
||||||
|
data: GachaRollItemDto::from(&roll),
|
||||||
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
// Refund the credit if claim creation fails
|
||||||
|
let user_id = user.id.id.to_raw(); // Extract value before potential move
|
||||||
|
let _ = repo_credits.query_add_credit(crate::v1::gacha_credits::gacha_credits_dto::GachaCreditRequestDto {
|
||||||
|
user_id,
|
||||||
|
amount: 1,
|
||||||
|
}).await;
|
||||||
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
None => common_response(StatusCode::NOT_FOUND, "No rollable item available"),
|
||||||
None => common_response(StatusCode::NOT_FOUND, "No rollable item available"),
|
},
|
||||||
},
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn soft_delete_gacha_roll(state: &AppState, id: String) -> Response {
|
pub async fn soft_delete_gacha_roll(state: &AppState, id: String) -> Response {
|
||||||
let repo = GachaRollRepository::new(state);
|
let repo = GachaRollRepository::new(state);
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
|
||||||
pub mod gacha_rolls_controller;
|
pub mod gacha_rolls_controller;
|
||||||
pub mod gacha_rolls_dto;
|
pub mod gacha_rolls_dto;
|
||||||
pub mod gacha_rolls_repository;
|
pub mod gacha_rolls_repository;
|
||||||
pub mod gacha_rolls_schema;
|
pub mod gacha_rolls_schema;
|
||||||
pub mod gacha_rolls_service;
|
pub mod gacha_rolls_service;
|
||||||
|
|
||||||
use axum::{
|
// Export only public API functions and types
|
||||||
Router,
|
pub use gacha_rolls_controller::{
|
||||||
routing::{get, post},
|
post_create_gacha_roll,
|
||||||
|
post_execute_gacha_roll,
|
||||||
|
get_detail_gacha_roll,
|
||||||
};
|
};
|
||||||
pub use gacha_rolls_controller::*;
|
pub use gacha_rolls_dto::GachaRollItemDto;
|
||||||
pub use gacha_rolls_dto::*;
|
|
||||||
pub use gacha_rolls_repository::*;
|
|
||||||
pub use gacha_rolls_schema::*;
|
|
||||||
pub use gacha_rolls_service::*;
|
|
||||||
|
|
||||||
|
/// Creates router for gacha rolls endpoints
|
||||||
pub fn gacha_roll_router() -> Router {
|
pub fn gacha_roll_router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/create", post(post_create_gacha_roll))
|
.route("/create", post(post_create_gacha_roll))
|
||||||
.route("/execute", post(post_execute_gacha_roll))
|
.route("/execute", post(post_execute_gacha_roll))
|
||||||
.route("/detail/{id}", get(get_detail_gacha_roll))
|
.route("/detail/{id}", get(get_detail_gacha_roll))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ pub mod gacha_claims;
|
|||||||
pub mod gacha_credits;
|
pub mod gacha_credits;
|
||||||
pub mod gacha_items;
|
pub mod gacha_items;
|
||||||
pub mod gacha_rolls;
|
pub mod gacha_rolls;
|
||||||
|
use crate::v1::gacha_items::gacha_items_controller;
|
||||||
|
|
||||||
pub use gacha_claims::*;
|
// Export only public router functions to avoid namespace pollution
|
||||||
pub use gacha_credits::*;
|
pub use gacha_credits::gacha_credit_router;
|
||||||
pub use gacha_items::*;
|
pub use gacha_items::gacha_item_router;
|
||||||
pub use gacha_rolls::*;
|
pub use gacha_rolls::gacha_roll_router;
|
||||||
|
pub use gacha_claims::gacha_claim_router;
|
||||||
|
|
||||||
|
/// Creates the main gacha router with all version 1 endpoints
|
||||||
pub fn gacha_router() -> Router {
|
pub fn gacha_router() -> Router {
|
||||||
Router::new()
|
let mut router = Router::new();
|
||||||
.nest("/gacha/claims", gacha_claim_router())
|
router = router.nest("/credits", gacha_credit_router());
|
||||||
.nest("/gacha/items", gacha_item_router())
|
router = router.nest("/items", gacha_item_router());
|
||||||
.nest("/gacha/rolls", gacha_roll_router())
|
router = router.nest("/rolls", gacha_roll_router());
|
||||||
|
router = router.nest("/claims", gacha_claim_router());
|
||||||
|
// Minimal admin router mounted at /admin to satisfy test.sh expectations
|
||||||
|
// This will expose GET /v1/gacha/admin -> list items (admin view)
|
||||||
|
router = router.nest("/admin", Router::new().route("/", axum::routing::get(gacha_items_controller::get_gacha_item_list)));
|
||||||
|
router
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ imphnen-entities.workspace = true
|
|||||||
imphnen-middleware.workspace = true
|
imphnen-middleware.workspace = true
|
||||||
imphnen-cms.workspace = true
|
imphnen-cms.workspace = true
|
||||||
imphnen-dimentorin.workspace = true
|
imphnen-dimentorin.workspace = true
|
||||||
|
imphnen-hackathon.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
+229
-36
@@ -1,11 +1,9 @@
|
|||||||
use imphnen_cms::{
|
use imphnen_cms::v1::landing::events::events_controller;
|
||||||
events_controller,
|
use imphnen_cms::v1::landing::events::events_dto::{EventsDetailItemDto, EventsListItemDto};
|
||||||
events_dto::{EventsDetailItemDto, EventsListItemDto},
|
use imphnen_cms::v1::landing::testimonials::testimonials_controller;
|
||||||
testimonials_controller,
|
use imphnen_cms::v1::landing::testimonials::testimonials_dto::{
|
||||||
testimonials_dto::{
|
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use imphnen_dimentorin::v1::mentors::{
|
use imphnen_dimentorin::v1::mentors::{
|
||||||
mentors_controller,
|
mentors_controller,
|
||||||
@@ -16,23 +14,55 @@ use imphnen_dimentorin::v1::mentors::{
|
|||||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use imphnen_gacha::{
|
use imphnen_dimentorin::v1::sessions::{
|
||||||
GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto,
|
sessions_controller,
|
||||||
GachaRollItemDto, GachaRollRequestDto, gacha_claims, gacha_items, gacha_rolls,
|
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||||
|
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||||
|
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||||
|
AvailabilitySlotDto,
|
||||||
};
|
};
|
||||||
use imphnen_iam::{
|
use imphnen_gacha::v1::gacha_claims::{gacha_claims_controller, GachaClaimItemDto, GachaClaimRequestDto};
|
||||||
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
|
use imphnen_gacha::v1::gacha_items::{gacha_items_controller, GachaItemDto};
|
||||||
AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto,
|
use imphnen_gacha::v1::gacha_items::gacha_items_dto::GachaItemRequestDto;
|
||||||
MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto,
|
use imphnen_gacha::v1::gacha_rolls::{gacha_rolls_controller, GachaRollItemDto};
|
||||||
PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
use imphnen_gacha::v1::gacha_rolls::gacha_rolls_dto::GachaRollRequestDto;
|
||||||
RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto,
|
use imphnen_hackathon::v1::hackathon::{
|
||||||
RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto,
|
hackathon_controller,
|
||||||
UsersListItemDto, UsersUpdateRequestDto, auth, permissions, roles, users,
|
hackathon_dto::{
|
||||||
|
HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto, HackathonEventDto,
|
||||||
|
HackathonEventUpdateRequestDto, HackathonSubmissionCreateRequestDto,
|
||||||
|
HackathonSubmissionDto, HackathonSubmissionUpdateRequestDto, HackathonTimelineCreateRequestDto,
|
||||||
|
HackathonTimelineDto, HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use imphnen_iam::users::users_controller::FileUploadSchema;
|
use imphnen_hackathon::v1::registrations::{
|
||||||
|
registration_controller,
|
||||||
|
RegistrationRequestDto, RegistrationResponseDto, RegistrationListResponseDto,
|
||||||
|
RegistrationListItemDto, UpdateRegistrationStatusRequestDto, UpdateRegistrationStatusResponseDto,
|
||||||
|
CheckInResponseDto, RegistrationStatsDto, UserHackathonsResponseDto, UserHackathonDto,
|
||||||
|
RegistrationStatus, ParticipantRole,
|
||||||
|
};
|
||||||
|
use imphnen_hackathon::v1::notifications::{
|
||||||
|
notification_controller,
|
||||||
|
notification_dto::{
|
||||||
|
NotificationDto, NotificationListResponseDto, MarkAsReadResponseDto,
|
||||||
|
MarkAllAsReadResponseDto, DeleteNotificationResponseDto, UnreadCountResponseDto,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use imphnen_entities::{PermissionsItemDto, RolesDetailItemDto};
|
||||||
|
use imphnen_entities::{MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||||
|
use imphnen_iam::v1::auth::auth_dto::{AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto};
|
||||||
|
use imphnen_iam::v1::permissions::permissions_dto::PermissionsRequestDto;
|
||||||
|
use imphnen_iam::v1::roles::RolesListItemDto;
|
||||||
|
use imphnen_iam::v1::roles::roles_dto::{RolesRequestCreateDto, RolesRequestUpdateDto};
|
||||||
|
use imphnen_iam::v1::users::UsersDetailItemDto;
|
||||||
|
use imphnen_iam::v1::users::users_dto::{UsersCreateRequestDto, UsersListItemDto, UsersUpdateRequestDto};
|
||||||
|
use imphnen_iam::v1::teams::teams_dto::{TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto, TeamAcceptInvitationRequestDto, TeamsDetailItemDto, TeamsListItemDto, TeamMemberDto, TeamInvitationDto, TeamsSearchQueryDto};
|
||||||
|
use imphnen_iam::v1::{auth, permissions, roles, users, teams};
|
||||||
|
use imphnen_iam::v1::users::users_controller::FileUploadSchema;
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
Modify, OpenApi,
|
Modify, OpenApi,
|
||||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
openapi::security::{Http, HttpAuthScheme, SecurityScheme, SecurityRequirement},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
@@ -65,16 +95,26 @@ use utoipa::{
|
|||||||
permissions::permissions_controller::post_create_permission,
|
permissions::permissions_controller::post_create_permission,
|
||||||
permissions::permissions_controller::put_update_permission,
|
permissions::permissions_controller::put_update_permission,
|
||||||
permissions::permissions_controller::delete_permission,
|
permissions::permissions_controller::delete_permission,
|
||||||
gacha_claims::get_detail_gacha_claim,
|
teams::teams_controller::get_team_list,
|
||||||
gacha_claims::post_create_gacha_claim,
|
teams::teams_controller::get_team_by_id,
|
||||||
gacha_items::get_gacha_item_list,
|
teams::teams_controller::post_create_team,
|
||||||
gacha_items::get_gacha_item_by_id,
|
teams::teams_controller::put_update_team,
|
||||||
gacha_items::post_create_gacha_item,
|
teams::teams_controller::delete_team,
|
||||||
gacha_items::put_update_gacha_item,
|
teams::teams_controller::post_invite_team_members,
|
||||||
gacha_items::delete_gacha_item,
|
teams::teams_controller::post_accept_invitation,
|
||||||
gacha_rolls::get_detail_gacha_roll,
|
teams::teams_controller::get_public_team_search,
|
||||||
gacha_rolls::post_create_gacha_roll,
|
teams::teams_controller::get_team_members,
|
||||||
gacha_rolls::post_execute_gacha_roll,
|
teams::teams_controller::post_leave_team,
|
||||||
|
gacha_claims_controller::get_detail_gacha_claim,
|
||||||
|
gacha_claims_controller::post_create_gacha_claim,
|
||||||
|
gacha_items_controller::get_gacha_item_list,
|
||||||
|
gacha_items_controller::get_gacha_item_by_id,
|
||||||
|
gacha_items_controller::post_create_gacha_item,
|
||||||
|
gacha_items_controller::put_update_gacha_item,
|
||||||
|
gacha_items_controller::delete_gacha_item,
|
||||||
|
gacha_rolls_controller::get_detail_gacha_roll,
|
||||||
|
gacha_rolls_controller::post_create_gacha_roll,
|
||||||
|
gacha_rolls_controller::post_execute_gacha_roll,
|
||||||
events_controller::get_event_list,
|
events_controller::get_event_list,
|
||||||
events_controller::get_event_by_id,
|
events_controller::get_event_by_id,
|
||||||
events_controller::post_create_event,
|
events_controller::post_create_event,
|
||||||
@@ -94,6 +134,41 @@ use utoipa::{
|
|||||||
mentors_controller::put_update_mentor,
|
mentors_controller::put_update_mentor,
|
||||||
mentors_controller::put_verify_mentor,
|
mentors_controller::put_verify_mentor,
|
||||||
mentors_controller::delete_mentor,
|
mentors_controller::delete_mentor,
|
||||||
|
sessions_controller::post_book_session,
|
||||||
|
sessions_controller::get_mentor_sessions,
|
||||||
|
sessions_controller::get_mentor_availability,
|
||||||
|
sessions_controller::put_update_session_status,
|
||||||
|
sessions_controller::post_submit_feedback,
|
||||||
|
sessions_controller::get_my_sessions,
|
||||||
|
hackathon_controller::create_hackathon,
|
||||||
|
hackathon_controller::get_hackathon,
|
||||||
|
hackathon_controller::list_hackathons,
|
||||||
|
hackathon_controller::update_hackathon,
|
||||||
|
hackathon_controller::delete_hackathon,
|
||||||
|
hackathon_controller::create_hackathon_event,
|
||||||
|
hackathon_controller::list_hackathon_events,
|
||||||
|
hackathon_controller::update_hackathon_event,
|
||||||
|
hackathon_controller::delete_hackathon_event,
|
||||||
|
hackathon_controller::create_hackathon_timeline,
|
||||||
|
hackathon_controller::list_hackathon_timeline,
|
||||||
|
hackathon_controller::update_hackathon_timeline,
|
||||||
|
hackathon_controller::delete_hackathon_timeline,
|
||||||
|
hackathon_controller::create_hackathon_submission,
|
||||||
|
hackathon_controller::list_hackathon_submissions,
|
||||||
|
hackathon_controller::update_hackathon_submission,
|
||||||
|
hackathon_controller::submit_hackathon_submission,
|
||||||
|
hackathon_controller::delete_hackathon_submission,
|
||||||
|
registration_controller::post_register_hackathon,
|
||||||
|
registration_controller::get_hackathon_registrations,
|
||||||
|
registration_controller::get_my_hackathons,
|
||||||
|
registration_controller::put_update_registration_status,
|
||||||
|
registration_controller::post_check_in_participant,
|
||||||
|
registration_controller::get_registration_stats,
|
||||||
|
notification_controller::get_notifications_handler,
|
||||||
|
notification_controller::mark_as_read_handler,
|
||||||
|
notification_controller::mark_all_as_read_handler,
|
||||||
|
notification_controller::delete_notification_handler,
|
||||||
|
notification_controller::get_unread_count_handler,
|
||||||
),
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
@@ -154,6 +229,81 @@ use utoipa::{
|
|||||||
ResponseListSuccessDto<Vec<MentorListResponseDto>>,
|
ResponseListSuccessDto<Vec<MentorListResponseDto>>,
|
||||||
ResponseSuccessDto<MentorDetailResponseDto>,
|
ResponseSuccessDto<MentorDetailResponseDto>,
|
||||||
ResponseSuccessDto<MentorRegisterResponseDto>,
|
ResponseSuccessDto<MentorRegisterResponseDto>,
|
||||||
|
BookSessionRequestDto,
|
||||||
|
BookSessionResponseDto,
|
||||||
|
SessionListResponseDto,
|
||||||
|
SessionListItemDto,
|
||||||
|
MentorAvailabilityDto,
|
||||||
|
AvailabilitySlotDto,
|
||||||
|
UpdateSessionStatusRequestDto,
|
||||||
|
UpdateSessionStatusResponseDto,
|
||||||
|
SessionFeedbackRequestDto,
|
||||||
|
SessionFeedbackResponseDto,
|
||||||
|
ResponseSuccessDto<BookSessionResponseDto>,
|
||||||
|
ResponseSuccessDto<SessionListResponseDto>,
|
||||||
|
ResponseSuccessDto<MentorAvailabilityDto>,
|
||||||
|
ResponseSuccessDto<UpdateSessionStatusResponseDto>,
|
||||||
|
ResponseSuccessDto<SessionFeedbackResponseDto>,
|
||||||
|
TeamsCreateRequestDto,
|
||||||
|
TeamsUpdateRequestDto,
|
||||||
|
TeamInviteRequestDto,
|
||||||
|
TeamAcceptInvitationRequestDto,
|
||||||
|
TeamsDetailItemDto,
|
||||||
|
TeamsListItemDto,
|
||||||
|
TeamMemberDto,
|
||||||
|
TeamInvitationDto,
|
||||||
|
TeamsSearchQueryDto,
|
||||||
|
ResponseListSuccessDto<Vec<TeamsListItemDto>>,
|
||||||
|
ResponseSuccessDto<TeamsDetailItemDto>,
|
||||||
|
HackathonCreateRequestDto,
|
||||||
|
HackathonDto,
|
||||||
|
HackathonEventCreateRequestDto,
|
||||||
|
HackathonEventDto,
|
||||||
|
HackathonEventUpdateRequestDto,
|
||||||
|
HackathonSubmissionCreateRequestDto,
|
||||||
|
HackathonSubmissionDto,
|
||||||
|
HackathonSubmissionUpdateRequestDto,
|
||||||
|
HackathonTimelineCreateRequestDto,
|
||||||
|
HackathonTimelineDto,
|
||||||
|
HackathonTimelineUpdateRequestDto,
|
||||||
|
HackathonUpdateRequestDto,
|
||||||
|
RegistrationRequestDto,
|
||||||
|
RegistrationResponseDto,
|
||||||
|
RegistrationListResponseDto,
|
||||||
|
RegistrationListItemDto,
|
||||||
|
UpdateRegistrationStatusRequestDto,
|
||||||
|
UpdateRegistrationStatusResponseDto,
|
||||||
|
CheckInResponseDto,
|
||||||
|
RegistrationStatsDto,
|
||||||
|
UserHackathonsResponseDto,
|
||||||
|
UserHackathonDto,
|
||||||
|
RegistrationStatus,
|
||||||
|
ParticipantRole,
|
||||||
|
NotificationDto,
|
||||||
|
NotificationListResponseDto,
|
||||||
|
MarkAsReadResponseDto,
|
||||||
|
MarkAllAsReadResponseDto,
|
||||||
|
DeleteNotificationResponseDto,
|
||||||
|
UnreadCountResponseDto,
|
||||||
|
ResponseSuccessDto<RegistrationResponseDto>,
|
||||||
|
ResponseSuccessDto<RegistrationListResponseDto>,
|
||||||
|
ResponseSuccessDto<UpdateRegistrationStatusResponseDto>,
|
||||||
|
ResponseSuccessDto<CheckInResponseDto>,
|
||||||
|
ResponseSuccessDto<RegistrationStatsDto>,
|
||||||
|
ResponseSuccessDto<UserHackathonsResponseDto>,
|
||||||
|
ResponseSuccessDto<NotificationListResponseDto>,
|
||||||
|
ResponseSuccessDto<MarkAsReadResponseDto>,
|
||||||
|
ResponseSuccessDto<MarkAllAsReadResponseDto>,
|
||||||
|
ResponseSuccessDto<DeleteNotificationResponseDto>,
|
||||||
|
ResponseSuccessDto<UnreadCountResponseDto>,
|
||||||
|
ResponseListSuccessDto<Vec<HackathonDto>>,
|
||||||
|
ResponseSuccessDto<HackathonDto>,
|
||||||
|
ResponseListSuccessDto<Vec<HackathonEventDto>>,
|
||||||
|
ResponseSuccessDto<HackathonEventDto>,
|
||||||
|
ResponseListSuccessDto<Vec<HackathonSubmissionDto>>,
|
||||||
|
ResponseSuccessDto<HackathonSubmissionDto>,
|
||||||
|
ResponseListSuccessDto<Vec<HackathonTimelineDto>>,
|
||||||
|
ResponseSuccessDto<HackathonTimelineDto>,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
info(
|
info(
|
||||||
@@ -179,13 +329,20 @@ use utoipa::{
|
|||||||
(name = "Testimonials", description = "Testimonial Management Endpoints"),
|
(name = "Testimonials", description = "Testimonial Management Endpoints"),
|
||||||
(name = "Mentors", description = "Mentor Management Endpoints"),
|
(name = "Mentors", description = "Mentor Management Endpoints"),
|
||||||
(name = "Mentors - Admin", description = "Mentor Admin Management Endpoints (Admin Access Required)"),
|
(name = "Mentors - Admin", description = "Mentor Admin Management Endpoints (Admin Access Required)"),
|
||||||
|
(name = "sessions", description = "Mentoring Sessions Management API"),
|
||||||
(name = "Gacha", description = "Gacha System Endpoints"),
|
(name = "Gacha", description = "Gacha System Endpoints"),
|
||||||
|
(name = "Hackathons", description = "Hackathon Management Endpoints"),
|
||||||
|
(name = "Hackathon Events", description = "Hackathon Event Management Endpoints"),
|
||||||
|
(name = "Hackathon Timeline", description = "Hackathon Timeline Management Endpoints"),
|
||||||
|
(name = "Hackathon Submissions", description = "Hackathon Submission Management Endpoints"),
|
||||||
|
(name = "registrations", description = "Hackathon Registration Management API"),
|
||||||
|
(name = "notifications", description = "User Notifications Management API"),
|
||||||
|
(name = "Teams", description = "Team Management Endpoints"),
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
|
pub struct ApiDoc;
|
||||||
|
|
||||||
pub struct ApiDoc;
|
pub struct SecurityAddon;
|
||||||
|
|
||||||
struct SecurityAddon;
|
|
||||||
|
|
||||||
impl Modify for SecurityAddon {
|
impl Modify for SecurityAddon {
|
||||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
@@ -195,7 +352,43 @@ impl Modify for SecurityAddon {
|
|||||||
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Walk all paths and add a Bearer security requirement to any operation
|
||||||
|
// that declares 401 or 403 responses. This helps ensure protected
|
||||||
|
// endpoints are shown with the Bearer lock in the generated docs
|
||||||
|
// without having to annotate every controller manually.
|
||||||
|
let paths = &mut openapi.paths;
|
||||||
|
for (_path, path_item) in paths.paths.iter_mut() {
|
||||||
|
// helper to process each possible operation on the path
|
||||||
|
let process_op = |op: &mut Option<utoipa::openapi::path::Operation>| {
|
||||||
|
if let Some(operation) = op.as_mut() {
|
||||||
|
let mut has_auth_response = false;
|
||||||
|
let responses = &operation.responses.responses;
|
||||||
|
for status in responses.keys() {
|
||||||
|
if status == "401" || status == "403" {
|
||||||
|
has_auth_response = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_auth_response {
|
||||||
|
// assign security requirement for Bearer if not already present
|
||||||
|
if operation.security.is_none() {
|
||||||
|
operation.security = Some(vec![SecurityRequirement::new::<&str, Vec<&str>, &str>("Bearer", vec![])]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process_op(&mut path_item.get);
|
||||||
|
process_op(&mut path_item.post);
|
||||||
|
process_op(&mut path_item.put);
|
||||||
|
process_op(&mut path_item.patch);
|
||||||
|
process_op(&mut path_item.delete);
|
||||||
|
process_op(&mut path_item.options);
|
||||||
|
process_op(&mut path_item.head);
|
||||||
|
process_op(&mut path_item.trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn docs_router() -> utoipa::openapi::OpenApi {
|
pub fn docs_router() -> utoipa::openapi::OpenApi {
|
||||||
|
|||||||
+48
-32
@@ -1,48 +1,64 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Extension, Router, middleware::from_fn, response::Redirect, routing::get,
|
Extension,
|
||||||
|
Router,
|
||||||
|
middleware::from_fn,
|
||||||
|
response::Redirect,
|
||||||
|
routing::get,
|
||||||
};
|
};
|
||||||
use imphnen_cms::{
|
use imphnen_cms::{
|
||||||
events_protected_routes, events_public_routes, testimonials_protected_routes,
|
events_protected_routes,
|
||||||
testimonials_public_routes,
|
events_public_routes,
|
||||||
|
testimonials_protected_routes,
|
||||||
|
testimonials_public_routes,
|
||||||
};
|
};
|
||||||
use imphnen_dimentorin::dimentorin_router;
|
use imphnen_dimentorin::dimentorin_router;
|
||||||
use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient};
|
|
||||||
use imphnen_gacha::gacha_router;
|
use imphnen_gacha::gacha_router;
|
||||||
use imphnen_iam::{iam_protected_routes, iam_public_routes};
|
use imphnen_hackathon::v1::{hackathon_protected_routes, hackathon_public_routes};
|
||||||
use imphnen_middleware::{auth_middleware, cors_middleware};
|
use imphnen_iam::{
|
||||||
|
iam_protected_routes,
|
||||||
|
iam_public_routes,
|
||||||
|
v1::users::users_service::UsersService,
|
||||||
|
v1::auth::auth_repository::AuthRepoImpl,
|
||||||
|
};
|
||||||
|
use imphnen_libs::{AppState, SurrealMemClient, SurrealWsClient};
|
||||||
|
use imphnen_middleware::{auth_middleware, cors_middleware, rate_limiting_middleware, security_headers_middleware};
|
||||||
|
use std::sync::Arc;
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
pub mod docs;
|
pub mod docs;
|
||||||
pub use docs::*;
|
pub use docs::{ApiDoc, SecurityAddon, docs_router};
|
||||||
|
|
||||||
pub async fn gateway_service(
|
pub async fn gateway_service(
|
||||||
surrealdb_ws: SurrealWsClient,
|
surrealdb_ws: SurrealWsClient,
|
||||||
surrealdb_mem: SurrealMemClient,
|
surrealdb_mem: SurrealMemClient,
|
||||||
) -> Router {
|
) -> Router {
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
surrealdb_ws,
|
surrealdb_ws,
|
||||||
surrealdb_mem,
|
surrealdb_mem: surrealdb_mem.clone(),
|
||||||
};
|
user_lookup_service: Arc::new(UsersService),
|
||||||
|
auth_repository: Arc::new(AuthRepoImpl { db: surrealdb_mem }),
|
||||||
|
};
|
||||||
|
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
.merge(iam_public_routes())
|
.merge(iam_public_routes().layer(from_fn(rate_limiting_middleware)))
|
||||||
.merge(testimonials_public_routes())
|
.merge(hackathon_public_routes())
|
||||||
.merge(events_public_routes());
|
.merge(testimonials_public_routes())
|
||||||
|
.merge(events_public_routes());
|
||||||
|
|
||||||
let protected_routes = Router::new()
|
let protected_routes = Router::new()
|
||||||
.merge(iam_protected_routes())
|
.merge(iam_protected_routes())
|
||||||
.merge(events_protected_routes())
|
.merge(events_protected_routes())
|
||||||
.merge(testimonials_protected_routes())
|
.merge(testimonials_protected_routes())
|
||||||
.merge(dimentorin_router())
|
.merge(dimentorin_router())
|
||||||
.merge(gacha_router())
|
.merge(hackathon_protected_routes())
|
||||||
.layer(from_fn(auth_middleware));
|
.nest("/gacha", gacha_router())
|
||||||
|
.layer(from_fn(auth_middleware));
|
||||||
|
|
||||||
let routes = public_routes.merge(protected_routes);
|
Router::new()
|
||||||
|
.route("/", get(Redirect::to("/docs")))
|
||||||
Router::new()
|
.nest("/v1", public_routes.merge(protected_routes))
|
||||||
.route("/", get(Redirect::to("/docs")))
|
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
||||||
.nest("/v1", routes)
|
.layer(cors_middleware())
|
||||||
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
.layer(from_fn(security_headers_middleware))
|
||||||
.layer(cors_middleware())
|
.layer(Extension(state))
|
||||||
.layer(Extension(state))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-hackathon"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-libs.workspace = true
|
||||||
|
imphnen-utils.workspace = true
|
||||||
|
|
||||||
|
imphnen-entities.workspace = true
|
||||||
|
imphnen-iam.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
oauth2 = { workspace = true }
|
||||||
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
strum_macros.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
once_cell.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
axum-extra.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dotenvy.workspace = true
|
||||||
|
tokio-test = { workspace = true }
|
||||||
|
mockall = { workspace = true }
|
||||||
|
http-body-util.workspace = true
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
pub mod v1;
|
||||||
|
|
||||||
|
// Re-export core entity types used across the hackathon system
|
||||||
|
pub use imphnen_entities::{
|
||||||
|
CountResult,
|
||||||
|
Error,
|
||||||
|
ErrorDto,
|
||||||
|
MessageResponseDto,
|
||||||
|
MetaRequestDto,
|
||||||
|
MetaResponseDto,
|
||||||
|
ResponseListSuccessDto,
|
||||||
|
ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Explicitly import only what we need from libs and utils to avoid pollution
|
||||||
|
pub use imphnen_libs::{
|
||||||
|
AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export public v1 API
|
||||||
|
pub use v1::hackathon::hackathon_controller::hackathon_routes;
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
use super::hackathon_dto::{
|
||||||
|
HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto,
|
||||||
|
HackathonTimelineCreateRequestDto,
|
||||||
|
};
|
||||||
|
use super::hackathon_repository::HackathonRepository;
|
||||||
|
use super::hackathon_audit_schema::{AuditAction, HackathonAuditLogSchema};
|
||||||
|
use super::hackathon_audit_repository::HackathonAuditRepository;
|
||||||
|
use super::hackathon_validation::{validate_timeline_phases, validate_dates, validate_organizers, validate_prizes, MAX_EVENTS_PER_HACKATHON};
|
||||||
|
use crate::{AppState, ResponseSuccessDto, ErrorDto};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
/// Request DTO for atomic hackathon creation with timeline and events
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonCompleteSetupRequestDto {
|
||||||
|
#[validate(nested)]
|
||||||
|
pub hackathon: HackathonCreateRequestDto,
|
||||||
|
|
||||||
|
#[validate(length(min = 1, message = "At least one timeline phase is required"))]
|
||||||
|
pub timelines: Vec<HackathonTimelineCreateRequestDto>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub events: Option<Vec<HackathonEventCreateRequestDto>>,
|
||||||
|
|
||||||
|
/// Actor ID for audit logging
|
||||||
|
pub actor_id: String,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub actor_email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response DTO for complete hackathon setup
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct HackathonCompleteSetupResponseDto {
|
||||||
|
pub hackathon: HackathonDto,
|
||||||
|
pub timelines: Vec<super::hackathon_dto::HackathonTimelineDto>,
|
||||||
|
pub events: Option<Vec<super::hackathon_dto::HackathonEventDto>>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service for atomic hackathon operations
|
||||||
|
pub struct HackathonAtomicService;
|
||||||
|
|
||||||
|
impl HackathonAtomicService {
|
||||||
|
/// Create hackathon with timeline and events atomically
|
||||||
|
/// This ensures all-or-nothing creation - if any step fails, nothing is created
|
||||||
|
pub async fn create_hackathon_complete(
|
||||||
|
payload: HackathonCompleteSetupRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<ResponseSuccessDto<HackathonCompleteSetupResponseDto>, ErrorDto> {
|
||||||
|
// 1. Validate all inputs before any database operations
|
||||||
|
if let Err((_, error_message)) = imphnen_utils::validator::validate_request(&payload) {
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
message: "Validation failed".to_string(),
|
||||||
|
details: Some(serde_json::json!({ "validation_errors": error_message })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate dates
|
||||||
|
if let Err(e) = validate_dates(
|
||||||
|
&payload.hackathon.start_date,
|
||||||
|
&payload.hackathon.end_date,
|
||||||
|
&payload.hackathon.registration_deadline,
|
||||||
|
) {
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
message: e.to_string(),
|
||||||
|
details: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate organizers
|
||||||
|
if let Err(e) = validate_organizers(&payload.hackathon.organizers) {
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
message: e.to_string(),
|
||||||
|
details: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validate prizes if provided
|
||||||
|
if let Some(ref prizes) = payload.hackathon.prizes {
|
||||||
|
let prize_schemas: Vec<super::hackathon_schema::Prize> = prizes
|
||||||
|
.iter()
|
||||||
|
.map(|p| super::hackathon_schema::Prize {
|
||||||
|
position: p.position,
|
||||||
|
title: p.title.clone(),
|
||||||
|
description: p.description.clone(),
|
||||||
|
value: p.value.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Err(e) = validate_prizes(&prize_schemas) {
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
message: e.to_string(),
|
||||||
|
details: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Validate events count if provided
|
||||||
|
if let Some(ref events) = payload.events {
|
||||||
|
if events.len() > MAX_EVENTS_PER_HACKATHON {
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
message: format!(
|
||||||
|
"Maximum {} events allowed per hackathon",
|
||||||
|
MAX_EVENTS_PER_HACKATHON
|
||||||
|
),
|
||||||
|
details: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let repo = HackathonRepository::new(state);
|
||||||
|
let audit_repo = HackathonAuditRepository::new(state);
|
||||||
|
|
||||||
|
// 6. Create hackathon first
|
||||||
|
let hackathon = match repo.create_hackathon(payload.hackathon.clone()).await {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create hackathon: {}", e);
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||||
|
message: "Failed to create hackathon".to_string(),
|
||||||
|
details: Some(serde_json::json!({ "error": e.to_string() })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hackathon_id = hackathon.id.id.to_string();
|
||||||
|
|
||||||
|
// 7. Create timelines - if this fails, we should ideally rollback hackathon
|
||||||
|
let mut created_timelines = Vec::new();
|
||||||
|
for timeline_dto in &payload.timelines {
|
||||||
|
match repo.create_hackathon_timeline(hackathon_id.clone(), timeline_dto.clone()).await {
|
||||||
|
Ok(timeline) => created_timelines.push(timeline),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create timeline, attempting cleanup: {}", e);
|
||||||
|
// Attempt to delete hackathon and created timelines
|
||||||
|
let _ = Self::cleanup_failed_creation(
|
||||||
|
&hackathon_id,
|
||||||
|
&created_timelines.iter().map(|t| t.id.id.to_string()).collect::<Vec<_>>(),
|
||||||
|
&[],
|
||||||
|
&repo,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||||
|
message: "Failed to create timeline, changes rolled back".to_string(),
|
||||||
|
details: Some(serde_json::json!({ "error": e.to_string() })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Validate timeline phases after all are created
|
||||||
|
if let Err(e) = validate_timeline_phases(&hackathon, &created_timelines) {
|
||||||
|
tracing::error!("Timeline validation failed, attempting cleanup: {}", e);
|
||||||
|
let _ = Self::cleanup_failed_creation(
|
||||||
|
&hackathon_id,
|
||||||
|
&created_timelines.iter().map(|t| t.id.id.to_string()).collect::<Vec<_>>(),
|
||||||
|
&[],
|
||||||
|
&repo,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
message: format!("Timeline validation failed: {}", e),
|
||||||
|
details: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Create events if provided
|
||||||
|
let mut created_events = Vec::new();
|
||||||
|
if let Some(ref events) = payload.events {
|
||||||
|
for event_dto in events {
|
||||||
|
match repo.create_hackathon_event(hackathon_id.clone(), event_dto.clone()).await {
|
||||||
|
Ok(event) => created_events.push(event),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create event, attempting cleanup: {}", e);
|
||||||
|
let _ = Self::cleanup_failed_creation(
|
||||||
|
&hackathon_id,
|
||||||
|
&created_timelines.iter().map(|t| t.id.id.to_string()).collect::<Vec<_>>(),
|
||||||
|
&created_events.iter().map(|e| e.id.id.to_string()).collect::<Vec<_>>(),
|
||||||
|
&repo,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
return Err(ErrorDto {
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||||
|
message: "Failed to create event, changes rolled back".to_string(),
|
||||||
|
details: Some(serde_json::json!({ "error": e.to_string() })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Log audit trail
|
||||||
|
let audit_log = HackathonAuditLogSchema::new(
|
||||||
|
Some(hackathon.id.clone()),
|
||||||
|
AuditAction::HackathonCreated,
|
||||||
|
payload.actor_id.clone(),
|
||||||
|
"hackathon".to_string(),
|
||||||
|
Some(hackathon_id.clone()),
|
||||||
|
)
|
||||||
|
.with_changes(serde_json::to_value(&payload).unwrap_or_default())
|
||||||
|
.with_request_info(None, None, payload.actor_email.clone());
|
||||||
|
|
||||||
|
if let Err(e) = audit_repo.log(audit_log).await {
|
||||||
|
tracing::error!("Failed to create audit log: {}", e);
|
||||||
|
// Don't fail the request if audit logging fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Return success response
|
||||||
|
let response = HackathonCompleteSetupResponseDto {
|
||||||
|
hackathon: super::hackathon_dto::HackathonDto::from(hackathon),
|
||||||
|
timelines: created_timelines
|
||||||
|
.into_iter()
|
||||||
|
.map(super::hackathon_dto::HackathonTimelineDto::from)
|
||||||
|
.collect(),
|
||||||
|
events: if created_events.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
created_events
|
||||||
|
.into_iter()
|
||||||
|
.map(super::hackathon_dto::HackathonEventDto::from)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
message: "Hackathon created successfully with timeline and events".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ResponseSuccessDto { data: response })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cleanup failed creation by deleting created resources
|
||||||
|
async fn cleanup_failed_creation(
|
||||||
|
hackathon_id: &str,
|
||||||
|
timeline_ids: &[String],
|
||||||
|
event_ids: &[String],
|
||||||
|
repo: &HackathonRepository<'_>,
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::info!("Starting cleanup for failed hackathon creation");
|
||||||
|
|
||||||
|
// Delete events
|
||||||
|
for event_id in event_ids {
|
||||||
|
if let Err(e) = repo.delete_hackathon_event(event_id.to_string()).await {
|
||||||
|
tracing::error!("Failed to cleanup event {}: {}", event_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete timelines
|
||||||
|
for timeline_id in timeline_ids {
|
||||||
|
if let Err(e) = repo.delete_hackathon_timeline(timeline_id.to_string()).await {
|
||||||
|
tracing::error!("Failed to cleanup timeline {}: {}", timeline_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete hackathon
|
||||||
|
if let Err(e) = repo.delete_hackathon(hackathon_id.to_string()).await {
|
||||||
|
tracing::error!("Failed to cleanup hackathon {}: {}", hackathon_id, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Cleanup completed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
use super::hackathon_audit_schema::{AuditAction, HackathonAuditLogSchema};
|
||||||
|
use anyhow::Result;
|
||||||
|
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HackathonAuditRepository<'a> {
|
||||||
|
pub state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> HackathonAuditRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, log), err)]
|
||||||
|
pub async fn log(&self, log: HackathonAuditLogSchema) -> Result<HackathonAuditLogSchema> {
|
||||||
|
let table = "app_hackathon_audit_logs";
|
||||||
|
let id = log.id.id.to_string();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
action = %log.action,
|
||||||
|
actor_id = %log.actor_id,
|
||||||
|
resource_type = %log.resource_type,
|
||||||
|
"Creating audit log entry"
|
||||||
|
);
|
||||||
|
|
||||||
|
let record: Option<HackathonAuditLogSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_ws
|
||||||
|
.create((table, id.clone()))
|
||||||
|
.content(log.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
record.ok_or_else(|| anyhow::anyhow!("Failed to create audit log"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self), err)]
|
||||||
|
pub async fn get_logs_by_hackathon(
|
||||||
|
&self,
|
||||||
|
hackathon_id: &Thing,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||||
|
let table = "app_hackathon_audit_logs";
|
||||||
|
let page = meta.page.unwrap_or(1);
|
||||||
|
let per_page = meta.per_page.unwrap_or(50);
|
||||||
|
let start = (page - 1) * per_page;
|
||||||
|
|
||||||
|
let condition = format!("hackathon_id = {}", hackathon_id);
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"SELECT * FROM {} WHERE {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||||
|
table, condition, per_page, start
|
||||||
|
);
|
||||||
|
|
||||||
|
let count_query = format!(
|
||||||
|
"SELECT count() as count FROM {} WHERE {} GROUP ALL",
|
||||||
|
table, condition
|
||||||
|
);
|
||||||
|
|
||||||
|
info!(query = %query, "Executing query to get audit logs");
|
||||||
|
|
||||||
|
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||||
|
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||||
|
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||||
|
|
||||||
|
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(ResponseListSuccessDto {
|
||||||
|
data: logs,
|
||||||
|
meta: Some(imphnen_libs::MetaResponseDto {
|
||||||
|
page: Some(page),
|
||||||
|
per_page: Some(per_page),
|
||||||
|
total: Some(total),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self), err)]
|
||||||
|
pub async fn get_logs_by_actor(
|
||||||
|
&self,
|
||||||
|
actor_id: &str,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||||
|
let table = "app_hackathon_audit_logs";
|
||||||
|
let page = meta.page.unwrap_or(1);
|
||||||
|
let per_page = meta.per_page.unwrap_or(50);
|
||||||
|
let start = (page - 1) * per_page;
|
||||||
|
|
||||||
|
let condition = format!("actor_id = '{}'", actor_id);
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"SELECT * FROM {} WHERE {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||||
|
table, condition, per_page, start
|
||||||
|
);
|
||||||
|
|
||||||
|
let count_query = format!(
|
||||||
|
"SELECT count() as count FROM {} WHERE {} GROUP ALL",
|
||||||
|
table, condition
|
||||||
|
);
|
||||||
|
|
||||||
|
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||||
|
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||||
|
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||||
|
|
||||||
|
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(ResponseListSuccessDto {
|
||||||
|
data: logs,
|
||||||
|
meta: Some(imphnen_libs::MetaResponseDto {
|
||||||
|
page: Some(page),
|
||||||
|
per_page: Some(per_page),
|
||||||
|
total: Some(total),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self), err)]
|
||||||
|
pub async fn get_logs_by_action(
|
||||||
|
&self,
|
||||||
|
action: AuditAction,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||||
|
let table = "app_hackathon_audit_logs";
|
||||||
|
let page = meta.page.unwrap_or(1);
|
||||||
|
let per_page = meta.per_page.unwrap_or(50);
|
||||||
|
let start = (page - 1) * per_page;
|
||||||
|
|
||||||
|
let condition = format!("action = '{}'", action.to_string());
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"SELECT * FROM {} WHERE {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||||
|
table, condition, per_page, start
|
||||||
|
);
|
||||||
|
|
||||||
|
let count_query = format!(
|
||||||
|
"SELECT count() as count FROM {} WHERE {} GROUP ALL",
|
||||||
|
table, condition
|
||||||
|
);
|
||||||
|
|
||||||
|
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||||
|
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||||
|
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||||
|
|
||||||
|
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(ResponseListSuccessDto {
|
||||||
|
data: logs,
|
||||||
|
meta: Some(imphnen_libs::MetaResponseDto {
|
||||||
|
page: Some(page),
|
||||||
|
per_page: Some(per_page),
|
||||||
|
total: Some(total),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self), err)]
|
||||||
|
pub async fn get_all_logs(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||||
|
let table = "app_hackathon_audit_logs";
|
||||||
|
let page = meta.page.unwrap_or(1);
|
||||||
|
let per_page = meta.per_page.unwrap_or(50);
|
||||||
|
let start = (page - 1) * per_page;
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"SELECT * FROM {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||||
|
table, per_page, start
|
||||||
|
);
|
||||||
|
|
||||||
|
let count_query = format!(
|
||||||
|
"SELECT count() as count FROM {} GROUP ALL",
|
||||||
|
table
|
||||||
|
);
|
||||||
|
|
||||||
|
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||||
|
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||||
|
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||||
|
|
||||||
|
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(ResponseListSuccessDto {
|
||||||
|
data: logs,
|
||||||
|
meta: Some(imphnen_libs::MetaResponseDto {
|
||||||
|
page: Some(page),
|
||||||
|
per_page: Some(per_page),
|
||||||
|
total: Some(total),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use imphnen_utils::{get_iso_date, make_thing};
|
||||||
|
|
||||||
|
/// Audit log schema for tracking all hackathon-related changes
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct HackathonAuditLogSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub hackathon_id: Option<Thing>, // None for system-wide events
|
||||||
|
pub action: AuditAction,
|
||||||
|
pub actor_id: String, // User who performed the action
|
||||||
|
pub actor_email: Option<String>, // For better traceability
|
||||||
|
pub resource_type: String, // hackathon, timeline, event, submission
|
||||||
|
pub resource_id: Option<String>, // ID of the affected resource
|
||||||
|
pub changes: Option<serde_json::Value>, // JSON of what changed
|
||||||
|
pub old_value: Option<serde_json::Value>,
|
||||||
|
pub new_value: Option<serde_json::Value>,
|
||||||
|
pub ip_address: Option<String>,
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum AuditAction {
|
||||||
|
// Hackathon actions
|
||||||
|
HackathonCreated,
|
||||||
|
HackathonUpdated,
|
||||||
|
HackathonDeleted,
|
||||||
|
HackathonStatusChanged,
|
||||||
|
|
||||||
|
// Timeline actions
|
||||||
|
TimelineCreated,
|
||||||
|
TimelineUpdated,
|
||||||
|
TimelineDeleted,
|
||||||
|
TimelineActivated,
|
||||||
|
|
||||||
|
// Event actions
|
||||||
|
EventCreated,
|
||||||
|
EventUpdated,
|
||||||
|
EventDeleted,
|
||||||
|
|
||||||
|
// Submission actions
|
||||||
|
SubmissionCreated,
|
||||||
|
SubmissionUpdated,
|
||||||
|
SubmissionDeleted,
|
||||||
|
SubmissionStatusChanged,
|
||||||
|
|
||||||
|
// Participant actions
|
||||||
|
ParticipantRegistered,
|
||||||
|
ParticipantRemoved,
|
||||||
|
|
||||||
|
// Organizer actions
|
||||||
|
OrganizerAdded,
|
||||||
|
OrganizerRemoved,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AuditAction {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AuditAction::HackathonCreated => write!(f, "hackathon_created"),
|
||||||
|
AuditAction::HackathonUpdated => write!(f, "hackathon_updated"),
|
||||||
|
AuditAction::HackathonDeleted => write!(f, "hackathon_deleted"),
|
||||||
|
AuditAction::HackathonStatusChanged => write!(f, "hackathon_status_changed"),
|
||||||
|
AuditAction::TimelineCreated => write!(f, "timeline_created"),
|
||||||
|
AuditAction::TimelineUpdated => write!(f, "timeline_updated"),
|
||||||
|
AuditAction::TimelineDeleted => write!(f, "timeline_deleted"),
|
||||||
|
AuditAction::TimelineActivated => write!(f, "timeline_activated"),
|
||||||
|
AuditAction::EventCreated => write!(f, "event_created"),
|
||||||
|
AuditAction::EventUpdated => write!(f, "event_updated"),
|
||||||
|
AuditAction::EventDeleted => write!(f, "event_deleted"),
|
||||||
|
AuditAction::SubmissionCreated => write!(f, "submission_created"),
|
||||||
|
AuditAction::SubmissionUpdated => write!(f, "submission_updated"),
|
||||||
|
AuditAction::SubmissionDeleted => write!(f, "submission_deleted"),
|
||||||
|
AuditAction::SubmissionStatusChanged => write!(f, "submission_status_changed"),
|
||||||
|
AuditAction::ParticipantRegistered => write!(f, "participant_registered"),
|
||||||
|
AuditAction::ParticipantRemoved => write!(f, "participant_removed"),
|
||||||
|
AuditAction::OrganizerAdded => write!(f, "organizer_added"),
|
||||||
|
AuditAction::OrganizerRemoved => write!(f, "organizer_removed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HackathonAuditLogSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
"app_hackathon_audit_logs",
|
||||||
|
&surrealdb::Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
hackathon_id: None,
|
||||||
|
action: AuditAction::HackathonCreated,
|
||||||
|
actor_id: String::new(),
|
||||||
|
actor_email: None,
|
||||||
|
resource_type: String::new(),
|
||||||
|
resource_id: None,
|
||||||
|
changes: None,
|
||||||
|
old_value: None,
|
||||||
|
new_value: None,
|
||||||
|
ip_address: None,
|
||||||
|
user_agent: None,
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HackathonAuditLogSchema {
|
||||||
|
pub fn new(
|
||||||
|
hackathon_id: Option<Thing>,
|
||||||
|
action: AuditAction,
|
||||||
|
actor_id: String,
|
||||||
|
resource_type: String,
|
||||||
|
resource_id: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
"app_hackathon_audit_logs",
|
||||||
|
&surrealdb::Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
hackathon_id,
|
||||||
|
action,
|
||||||
|
actor_id,
|
||||||
|
actor_email: None,
|
||||||
|
resource_type,
|
||||||
|
resource_id,
|
||||||
|
changes: None,
|
||||||
|
old_value: None,
|
||||||
|
new_value: None,
|
||||||
|
ip_address: None,
|
||||||
|
user_agent: None,
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_changes(mut self, changes: serde_json::Value) -> Self {
|
||||||
|
self.changes = Some(changes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_old_new_values(
|
||||||
|
mut self,
|
||||||
|
old_value: serde_json::Value,
|
||||||
|
new_value: serde_json::Value,
|
||||||
|
) -> Self {
|
||||||
|
self.old_value = Some(old_value);
|
||||||
|
self.new_value = Some(new_value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_request_info(
|
||||||
|
mut self,
|
||||||
|
ip_address: Option<String>,
|
||||||
|
user_agent: Option<String>,
|
||||||
|
actor_email: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
self.ip_address = ip_address;
|
||||||
|
self.user_agent = user_agent;
|
||||||
|
self.actor_email = actor_email;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,630 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::{ToSchema, schema};
|
||||||
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
// Custom validators
|
||||||
|
pub fn validate_url_format(url: &str) -> Result<(), ValidationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref URL_REGEX: Regex = Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap();
|
||||||
|
}
|
||||||
|
if URL_REGEX.is_match(url) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("invalid_url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_github_url(url: &str) -> Result<(), ValidationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref GITHUB_REGEX: Regex = Regex::new(r"^https?://github\.com/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)?$").unwrap();
|
||||||
|
}
|
||||||
|
if GITHUB_REGEX.is_match(url) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("invalid_github_url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_demo_url(url: &str) -> Result<(), ValidationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref DEMO_URL_REGEX: Regex = Regex::new(r"^https?://(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+(/[^\s]*)?$").unwrap();
|
||||||
|
}
|
||||||
|
if DEMO_URL_REGEX.is_match(url) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("invalid_demo_url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::v1::hackathon::hackathon_schema::{
|
||||||
|
HackathonEventType, HackathonEventsSchema, HackathonPhase, HackathonSchema,
|
||||||
|
HackathonStatus, HackathonSubmissionsSchema, HackathonTimelineSchema,
|
||||||
|
SubmissionStatus,
|
||||||
|
HackathonParticipantSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hackathon DTOs
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonCreateRequestDto {
|
||||||
|
#[validate(length(min = 1, max = 100, message = "Hackathon name must be between 1 and 100 characters"))]
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub registration_deadline: DateTime<Utc>,
|
||||||
|
|
||||||
|
#[validate(range(min = 1, max = 10000, message = "Max participants must be between 1 and 10000"))]
|
||||||
|
pub max_participants: Option<u32>,
|
||||||
|
|
||||||
|
#[validate(length(max = 200, message = "Theme cannot exceed 200 characters"))]
|
||||||
|
pub theme: Option<String>,
|
||||||
|
|
||||||
|
#[validate(length(max = 2000, message = "Rules cannot exceed 2000 characters"))]
|
||||||
|
pub rules: Option<String>,
|
||||||
|
|
||||||
|
pub prizes: Option<Vec<PrizeDto>>,
|
||||||
|
pub previous_winners: Option<Vec<WinnerDto>>,
|
||||||
|
|
||||||
|
#[validate(length(min = 1, message = "Organizers list cannot be empty"))]
|
||||||
|
pub organizers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonUpdateRequestDto {
|
||||||
|
#[validate(length(min = 1, max = 100, message = "Hackathon name must be between 1 and 100 characters"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_date: Option<DateTime<Utc>>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_date: Option<DateTime<Utc>>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub registration_deadline: Option<DateTime<Utc>>,
|
||||||
|
#[validate(range(min = 1, max = 10000, message = "Max participants must be between 1 and 10000"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_participants: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub theme: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub rules: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub prizes: Option<Vec<PrizeDto>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub previous_winners: Option<Vec<WinnerDto>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub organizers: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct HackathonDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub registration_deadline: DateTime<Utc>,
|
||||||
|
pub max_participants: Option<u32>,
|
||||||
|
pub status: HackathonStatus,
|
||||||
|
pub theme: Option<String>,
|
||||||
|
pub rules: Option<String>,
|
||||||
|
pub prizes: Option<Vec<PrizeDto>>,
|
||||||
|
pub previous_winners: Option<Vec<WinnerDto>>,
|
||||||
|
pub organizers: Vec<String>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct PrizeDto {
|
||||||
|
#[validate(range(min = 1, message = "Position must be at least 1"))]
|
||||||
|
pub position: u32,
|
||||||
|
#[validate(length(min = 1, message = "Prize title cannot be empty"))]
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub value: Option<String>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct WinnerDto {
|
||||||
|
#[validate(range(min = 1, message = "Position must be at least 1"))]
|
||||||
|
pub position: u32,
|
||||||
|
pub team_id: String,
|
||||||
|
#[validate(length(min = 1, message = "Project name cannot be empty"))]
|
||||||
|
pub project_name: String,
|
||||||
|
pub team_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Events DTOs
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonEventCreateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Event title cannot be empty"))]
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub event_type: HackathonEventType,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_time: DateTime<Utc>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_time: DateTime<Utc>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub virtual_link: Option<String>,
|
||||||
|
#[validate(range(min = 1, message = "Max attendees must be at least 1"))]
|
||||||
|
pub max_attendees: Option<u32>,
|
||||||
|
pub is_mandatory: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonEventUpdateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Event title cannot be empty"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub event_type: Option<HackathonEventType>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_time: Option<DateTime<Utc>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_time: Option<DateTime<Utc>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub location: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub virtual_link: Option<String>,
|
||||||
|
#[validate(range(min = 1, message = "Max attendees must be at least 1"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_attendees: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_mandatory: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct HackathonEventDto {
|
||||||
|
pub id: String,
|
||||||
|
pub hackathon_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub event_type: HackathonEventType,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_time: DateTime<Utc>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_time: DateTime<Utc>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub virtual_link: Option<String>,
|
||||||
|
pub max_attendees: Option<u32>,
|
||||||
|
pub is_mandatory: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Timeline DTOs
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonTimelineCreateRequestDto {
|
||||||
|
pub phase: HackathonPhase,
|
||||||
|
// Accept either `title` or `name` in incoming JSON (tests may send `name`).
|
||||||
|
// Make it optional so missing title doesn't cause a 422; service/repo will
|
||||||
|
// fallback to an empty title or a sensible default.
|
||||||
|
#[serde(alias = "name")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_active: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[validate(range(min = 0, message = "Order must be non-negative"))]
|
||||||
|
pub order: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validator for HackathonPhase (case-insensitive)
|
||||||
|
pub fn validate_hackathon_phase(phase: &str) -> Result<(), ValidationError> {
|
||||||
|
let normalized = phase.to_lowercase();
|
||||||
|
match normalized.as_str() {
|
||||||
|
"registration" | "ideation" | "development" | "submission" | "judging" | "awards" => Ok(()),
|
||||||
|
_ => Err(ValidationError::new("invalid_hackathon_phase")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validator to ensure start_date is in the future
|
||||||
|
pub fn validate_future_date(date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||||
|
let now = Utc::now();
|
||||||
|
if date <= &now {
|
||||||
|
Err(ValidationError::new("start_date_must_be_in_future"))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validator to ensure end_date is in the future or current
|
||||||
|
pub fn validate_future_or_current_date(date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||||
|
let now = Utc::now();
|
||||||
|
if date < &now {
|
||||||
|
Err(ValidationError::new("end_date_must_be_in_future_or_current"))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonTimelineUpdateRequestDto {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phase: Option<HackathonPhase>,
|
||||||
|
#[validate(length(min = 1, message = "Timeline title cannot be empty"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_date: Option<DateTime<Utc>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_date: Option<DateTime<Utc>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_active: Option<bool>,
|
||||||
|
#[validate(range(min = 0, message = "Order must be non-negative"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub order: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct HackathonTimelineDto {
|
||||||
|
pub id: String,
|
||||||
|
pub hackathon_id: String,
|
||||||
|
pub phase: HackathonPhase,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub order: u32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Submissions DTOs
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonSubmissionCreateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Project name cannot be empty"))]
|
||||||
|
pub project_name: String,
|
||||||
|
#[validate(length(min = 1, message = "Description cannot be empty"))]
|
||||||
|
pub description: String,
|
||||||
|
pub repository_url: Option<String>,
|
||||||
|
pub upload_file_url: Option<String>, // URL to uploaded zip/pdf file
|
||||||
|
pub demo_url: Option<String>,
|
||||||
|
pub slides_url: Option<String>,
|
||||||
|
pub technologies: Vec<String>,
|
||||||
|
// Social media contacts for demo (at least one required)
|
||||||
|
pub contact_instagram: Option<String>,
|
||||||
|
pub contact_twitter: Option<String>,
|
||||||
|
pub contact_linkedin: Option<String>,
|
||||||
|
pub contact_facebook: Option<String>,
|
||||||
|
pub contact_youtube: Option<String>,
|
||||||
|
pub contact_tiktok: Option<String>,
|
||||||
|
pub contact_other: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonSubmissionUpdateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Project name cannot be empty"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub project_name: Option<String>,
|
||||||
|
#[validate(length(min = 1, message = "Description cannot be empty"))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repository_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub upload_file_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub demo_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub slides_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub technologies: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub contact_instagram: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub contact_twitter: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub contact_linkedin: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub contact_facebook: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub contact_youtube: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub contact_tiktok: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub contact_other: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct HackathonSubmissionDto {
|
||||||
|
pub id: String,
|
||||||
|
pub hackathon_id: String,
|
||||||
|
pub team_id: String,
|
||||||
|
pub project_name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub repository_url: Option<String>,
|
||||||
|
pub upload_file_url: Option<String>,
|
||||||
|
pub demo_url: Option<String>,
|
||||||
|
pub slides_url: Option<String>,
|
||||||
|
pub technologies: Vec<String>,
|
||||||
|
pub contact_instagram: Option<String>,
|
||||||
|
pub contact_twitter: Option<String>,
|
||||||
|
pub contact_linkedin: Option<String>,
|
||||||
|
pub contact_facebook: Option<String>,
|
||||||
|
pub contact_youtube: Option<String>,
|
||||||
|
pub contact_tiktok: Option<String>,
|
||||||
|
pub contact_other: Option<String>,
|
||||||
|
#[serde(rename = "status")]
|
||||||
|
pub submission_status: SubmissionStatus,
|
||||||
|
pub judge_feedback: Option<String>,
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub submitted_at: DateTime<Utc>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query DTOs
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonQueryDto {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status: Option<HackathonStatus>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub organizer_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonEventQueryDto {
|
||||||
|
pub hackathon_id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub event_type: Option<HackathonEventType>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonTimelineQueryDto {
|
||||||
|
pub hackathon_id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phase: Option<HackathonPhase>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_active: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonSubmissionQueryDto {
|
||||||
|
pub hackathon_id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub team_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub submission_status: Option<SubmissionStatus>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion implementations
|
||||||
|
impl From<HackathonSchema> for HackathonDto {
|
||||||
|
fn from(schema: HackathonSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: schema.id.id.to_raw(),
|
||||||
|
name: schema.name,
|
||||||
|
description: schema.description,
|
||||||
|
start_date: schema.start_date,
|
||||||
|
end_date: schema.end_date,
|
||||||
|
registration_deadline: schema.registration_deadline,
|
||||||
|
max_participants: schema.max_participants,
|
||||||
|
status: schema.status,
|
||||||
|
theme: schema.theme,
|
||||||
|
rules: schema.rules,
|
||||||
|
prizes: schema.prizes.map(|prizes| {
|
||||||
|
prizes
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| PrizeDto {
|
||||||
|
position: p.position,
|
||||||
|
title: p.title,
|
||||||
|
description: p.description,
|
||||||
|
value: p.value,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
previous_winners: schema.previous_winners.map(|winners| {
|
||||||
|
winners
|
||||||
|
.into_iter()
|
||||||
|
.map(|w| WinnerDto {
|
||||||
|
position: w.position,
|
||||||
|
team_id: w.team_id,
|
||||||
|
project_name: w.project_name,
|
||||||
|
team_name: w.team_name,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
organizers: schema.organizers,
|
||||||
|
is_deleted: schema.is_deleted,
|
||||||
|
created_at: schema.created_at,
|
||||||
|
updated_at: schema.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HackathonEventsSchema> for HackathonEventDto {
|
||||||
|
fn from(schema: HackathonEventsSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: schema.id.id.to_raw(),
|
||||||
|
hackathon_id: schema.hackathon_id.id.to_raw(),
|
||||||
|
title: schema.title,
|
||||||
|
description: schema.description,
|
||||||
|
event_type: schema.event_type,
|
||||||
|
start_time: schema.start_time,
|
||||||
|
end_time: schema.end_time,
|
||||||
|
location: schema.location,
|
||||||
|
virtual_link: schema.virtual_link,
|
||||||
|
max_attendees: schema.max_attendees,
|
||||||
|
is_mandatory: schema.is_mandatory,
|
||||||
|
is_deleted: schema.is_deleted,
|
||||||
|
created_at: schema.created_at,
|
||||||
|
updated_at: schema.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HackathonTimelineSchema> for HackathonTimelineDto {
|
||||||
|
fn from(schema: HackathonTimelineSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: schema.id.id.to_raw(),
|
||||||
|
hackathon_id: schema.hackathon_id.id.to_raw(),
|
||||||
|
phase: schema.phase,
|
||||||
|
title: schema.title,
|
||||||
|
description: schema.description,
|
||||||
|
start_date: schema.start_date,
|
||||||
|
end_date: schema.end_date,
|
||||||
|
is_active: schema.is_active,
|
||||||
|
order: schema.order,
|
||||||
|
is_deleted: schema.is_deleted,
|
||||||
|
created_at: schema.created_at,
|
||||||
|
updated_at: schema.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HackathonSubmissionsSchema> for HackathonSubmissionDto {
|
||||||
|
fn from(schema: HackathonSubmissionsSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: schema.id.id.to_raw(),
|
||||||
|
hackathon_id: schema.hackathon_id.id.to_raw(),
|
||||||
|
team_id: schema.team_id.map(|t| t.id.to_raw()).unwrap_or_default(),
|
||||||
|
project_name: schema.project_name.unwrap_or_default(),
|
||||||
|
description: schema.description.unwrap_or_default(),
|
||||||
|
repository_url: schema.repository_url,
|
||||||
|
upload_file_url: schema.upload_file_url,
|
||||||
|
demo_url: schema.demo_url,
|
||||||
|
slides_url: schema.slides_url,
|
||||||
|
technologies: schema.technologies.unwrap_or_default(),
|
||||||
|
contact_instagram: schema.contact_instagram,
|
||||||
|
contact_twitter: schema.contact_twitter,
|
||||||
|
contact_linkedin: schema.contact_linkedin,
|
||||||
|
contact_facebook: schema.contact_facebook,
|
||||||
|
contact_youtube: schema.contact_youtube,
|
||||||
|
contact_tiktok: schema.contact_tiktok,
|
||||||
|
contact_other: schema.contact_other,
|
||||||
|
submission_status: schema.submission_status.unwrap_or(super::hackathon_schema::SubmissionStatus::Draft),
|
||||||
|
judge_feedback: schema.judge_feedback,
|
||||||
|
submitted_at: schema.submitted_at.unwrap_or(chrono::Utc::now()),
|
||||||
|
is_deleted: schema.is_deleted,
|
||||||
|
created_at: schema.created_at,
|
||||||
|
updated_at: schema.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Participant DTOs
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct RegisterParticipantRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "user_id cannot be empty"))]
|
||||||
|
pub user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct HackathonParticipantDto {
|
||||||
|
pub id: String,
|
||||||
|
pub hackathon_id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HackathonParticipantSchema> for HackathonParticipantDto {
|
||||||
|
fn from(schema: HackathonParticipantSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: schema.id.id.to_raw(),
|
||||||
|
hackathon_id: schema.hackathon_id.id.to_raw(),
|
||||||
|
user_id: schema.user_id,
|
||||||
|
is_deleted: schema.is_deleted,
|
||||||
|
created_at: schema.created_at,
|
||||||
|
updated_at: schema.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin Sensitive Data Management DTOs
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Validate, ToSchema)]
|
||||||
|
pub struct AdminManageSensitiveDataRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "At least one user ID is required"))]
|
||||||
|
pub user_ids: Vec<String>,
|
||||||
|
#[validate(length(min = 1, message = "At least one raw score is required"))]
|
||||||
|
pub raw_scores: Vec<i32>,
|
||||||
|
pub personal_info: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct AdminSensitiveDataMemberDto {
|
||||||
|
pub user_id: String,
|
||||||
|
pub masked_email: String,
|
||||||
|
pub masked_phone: String,
|
||||||
|
pub name: String,
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct AdminSensitiveDataDto {
|
||||||
|
pub submission_id: String,
|
||||||
|
pub team_id: String,
|
||||||
|
pub project_name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub technologies: Vec<String>,
|
||||||
|
pub score: Option<i32>,
|
||||||
|
pub members: Vec<AdminSensitiveDataMemberDto>,
|
||||||
|
pub raw_scores: Option<Vec<i32>>,
|
||||||
|
pub submission_date: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct AdminSensitiveDataResponseDto {
|
||||||
|
pub data: Vec<AdminSensitiveDataDto>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
// Status Change DTOs
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct HackathonStatusChangeRequestDto {
|
||||||
|
pub status: HackathonStatus,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reason: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub actor_id: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,905 @@
|
|||||||
|
use super::hackathon_dto::{
|
||||||
|
HackathonCreateRequestDto, HackathonEventCreateRequestDto,
|
||||||
|
HackathonEventUpdateRequestDto, HackathonSubmissionCreateRequestDto,
|
||||||
|
HackathonSubmissionUpdateRequestDto, HackathonTimelineCreateRequestDto,
|
||||||
|
HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
|
||||||
|
};
|
||||||
|
use super::hackathon_schema::{
|
||||||
|
HackathonEventsSchema, HackathonPhase, HackathonSchema, HackathonSubmissionsSchema, HackathonTimelineSchema,
|
||||||
|
Prize,
|
||||||
|
};
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use imphnen_utils::{QueryListBuilder, get_iso_date};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use tracing::{instrument, info};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HackathonRepository<'a> {
|
||||||
|
pub state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> HackathonRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize an incoming id so callers can pass either the full thing string
|
||||||
|
// (e.g. "app_hackathons:1") or the raw id ("1"). If the id starts with
|
||||||
|
// the table prefix ("{table}:") the prefix is stripped.
|
||||||
|
fn normalize_id(&self, table: &str, id: &str) -> String {
|
||||||
|
if id.starts_with(&format!("{}:", table)) {
|
||||||
|
if let Some((_, rest)) = id.split_once(':') {
|
||||||
|
rest.to_string()
|
||||||
|
} else {
|
||||||
|
id.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon CRUD operations
|
||||||
|
impl<'a> HackathonRepository<'a> {
|
||||||
|
#[instrument(skip(self, hackathon), err)]
|
||||||
|
pub async fn create_hackathon(&self, hackathon: HackathonCreateRequestDto) -> Result<HackathonSchema> {
|
||||||
|
let table = ResourceEnum::Hackathons.to_string();
|
||||||
|
let id = surrealdb::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let prizes: Option<Vec<Prize>> = hackathon.prizes.map(|p| {
|
||||||
|
p.into_iter()
|
||||||
|
.map(|prize| Prize {
|
||||||
|
position: prize.position,
|
||||||
|
title: prize.title,
|
||||||
|
description: prize.description,
|
||||||
|
value: prize.value,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
let previous_winners: Option<Vec<super::hackathon_schema::Winner>> = hackathon.previous_winners.map(|w| {
|
||||||
|
w.into_iter()
|
||||||
|
.map(|winner| super::hackathon_schema::Winner {
|
||||||
|
position: winner.position,
|
||||||
|
team_id: winner.team_id,
|
||||||
|
project_name: winner.project_name,
|
||||||
|
team_name: winner.team_name,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let schema = HackathonSchema {
|
||||||
|
id: Thing::from((table.clone(), id.clone())),
|
||||||
|
name: hackathon.name,
|
||||||
|
description: hackathon.description,
|
||||||
|
start_date: hackathon.start_date,
|
||||||
|
end_date: hackathon.end_date,
|
||||||
|
registration_deadline: hackathon.registration_deadline,
|
||||||
|
max_participants: hackathon.max_participants,
|
||||||
|
status: super::hackathon_schema::HackathonStatus::Draft,
|
||||||
|
theme: hackathon.theme,
|
||||||
|
rules: hackathon.rules,
|
||||||
|
prizes,
|
||||||
|
previous_winners,
|
||||||
|
organizers: hackathon.organizers,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.create((table, id))
|
||||||
|
.content(schema.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(h) => Ok(h),
|
||||||
|
None => bail!("Failed to create hackathon"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn get_hackathon_by_id(&self, id: String) -> Result<HackathonSchema> {
|
||||||
|
let table = ResourceEnum::Hackathons.to_string();
|
||||||
|
info!(query = %format!("SELECT * FROM {} WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
|
||||||
|
let normalized_id = self.normalize_id(&table, &id);
|
||||||
|
|
||||||
|
let record: Option<HackathonSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_ws
|
||||||
|
.select((table, normalized_id.clone()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(h) => {
|
||||||
|
if h.is_deleted {
|
||||||
|
bail!("Hackathon not found");
|
||||||
|
}
|
||||||
|
Ok(h)
|
||||||
|
}
|
||||||
|
None => bail!("Hackathon not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, meta), err)]
|
||||||
|
pub async fn list_hackathons(&self, meta: imphnen_libs::MetaRequestDto) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonSchema>>> {
|
||||||
|
let table = ResourceEnum::Hackathons.to_string();
|
||||||
|
|
||||||
|
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||||
|
.with_condition("is_deleted = false")
|
||||||
|
.search_field("name")
|
||||||
|
.select_fields(vec!["*"]);
|
||||||
|
|
||||||
|
let mut result = builder.build().await?;
|
||||||
|
|
||||||
|
// Ensure deterministic ordering for listings by sorting on created_at (oldest first).
|
||||||
|
// Tests expect insertion order (first created appears first). created_at is an Option<String>
|
||||||
|
// with ISO 8601 format from `get_iso_date()`, so string comparison is chronologically correct.
|
||||||
|
result.data.sort_by_key(|s: &HackathonSchema| s.created_at.clone());
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id, updates), err)]
|
||||||
|
pub async fn update_hackathon(&self, id: String, updates: HackathonUpdateRequestDto) -> Result<HackathonSchema> {
|
||||||
|
let table = ResourceEnum::Hackathons.to_string();
|
||||||
|
|
||||||
|
// First get the existing hackathon
|
||||||
|
let mut existing = self.get_hackathon_by_id(id.clone()).await?;
|
||||||
|
|
||||||
|
// Apply updates
|
||||||
|
if let Some(name) = updates.name {
|
||||||
|
existing.name = name;
|
||||||
|
}
|
||||||
|
if let Some(description) = updates.description {
|
||||||
|
existing.description = description;
|
||||||
|
}
|
||||||
|
if let Some(start_date) = updates.start_date {
|
||||||
|
existing.start_date = start_date;
|
||||||
|
}
|
||||||
|
if let Some(end_date) = updates.end_date {
|
||||||
|
existing.end_date = end_date;
|
||||||
|
}
|
||||||
|
if let Some(registration_deadline) = updates.registration_deadline {
|
||||||
|
existing.registration_deadline = registration_deadline;
|
||||||
|
}
|
||||||
|
if let Some(max_participants) = updates.max_participants {
|
||||||
|
existing.max_participants = Some(max_participants);
|
||||||
|
}
|
||||||
|
if let Some(theme) = updates.theme {
|
||||||
|
existing.theme = Some(theme);
|
||||||
|
}
|
||||||
|
if let Some(rules) = updates.rules {
|
||||||
|
existing.rules = Some(rules);
|
||||||
|
}
|
||||||
|
if let Some(prizes) = updates.prizes {
|
||||||
|
let prizes_schema: Vec<Prize> = prizes
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| Prize {
|
||||||
|
position: p.position,
|
||||||
|
title: p.title,
|
||||||
|
description: p.description,
|
||||||
|
value: p.value,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
existing.prizes = Some(prizes_schema);
|
||||||
|
if let Some(previous_winners) = updates.previous_winners {
|
||||||
|
let winners_schema: Vec<super::hackathon_schema::Winner> = previous_winners
|
||||||
|
.into_iter()
|
||||||
|
.map(|w| super::hackathon_schema::Winner {
|
||||||
|
position: w.position,
|
||||||
|
team_id: w.team_id,
|
||||||
|
project_name: w.project_name,
|
||||||
|
team_name: w.team_name,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
existing.previous_winners = Some(winners_schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(organizers) = updates.organizers {
|
||||||
|
existing.organizers = organizers;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.content(existing.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(h) => Ok(h),
|
||||||
|
None => bail!("Failed to update hackathon"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn delete_hackathon(&self, id: String) -> Result<String> {
|
||||||
|
let table = ResourceEnum::Hackathons.to_string();
|
||||||
|
|
||||||
|
// Soft delete by setting is_deleted = true
|
||||||
|
let updates: HashMap<String, serde_json::Value> = HashMap::from([
|
||||||
|
("is_deleted".to_string(), true.into()),
|
||||||
|
("updated_at".to_string(), get_iso_date().into()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let normalized_id = self.normalize_id(&table, &id);
|
||||||
|
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, normalized_id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, normalized_id.clone()))
|
||||||
|
.merge(serde_json::to_value(updates)?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Hackathon deleted successfully".to_string()),
|
||||||
|
None => bail!("Failed to delete hackathon"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id, status), err)]
|
||||||
|
pub async fn update_hackathon_status(&self, id: String, status: super::hackathon_schema::HackathonStatus) -> Result<HackathonSchema> {
|
||||||
|
let table = ResourceEnum::Hackathons.to_string();
|
||||||
|
|
||||||
|
// Get existing hackathon
|
||||||
|
let mut existing = self.get_hackathon_by_id(id.clone()).await?;
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
existing.status = status;
|
||||||
|
existing.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET status = {:?} WHERE id = '{}'", table, existing.status, id), "Executing SurrealDB query");
|
||||||
|
let normalized_id = self.normalize_id(&table, &id);
|
||||||
|
let record: Option<HackathonSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, normalized_id))
|
||||||
|
.content(existing.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(h) => Ok(h),
|
||||||
|
None => bail!("Failed to update hackathon status"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Events CRUD operations
|
||||||
|
impl<'a> HackathonRepository<'a> {
|
||||||
|
#[instrument(skip(self, hackathon_id, event), err)]
|
||||||
|
pub async fn create_hackathon_event(&self, hackathon_id: String, event: HackathonEventCreateRequestDto) -> Result<HackathonEventsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonEvents.to_string();
|
||||||
|
let id = surrealdb::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
|
||||||
|
let schema = HackathonEventsSchema {
|
||||||
|
id: Thing::from((table.clone(), id.clone())),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), normalized_hackathon_id)),
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
event_type: event.event_type,
|
||||||
|
start_time: event.start_time,
|
||||||
|
end_time: event.end_time,
|
||||||
|
location: event.location,
|
||||||
|
virtual_link: event.virtual_link,
|
||||||
|
max_attendees: event.max_attendees,
|
||||||
|
is_mandatory: event.is_mandatory,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonEventsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.create((table, id))
|
||||||
|
.content(schema.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(e) => Ok(e),
|
||||||
|
None => bail!("Failed to create hackathon event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, meta, hackathon_id), err)]
|
||||||
|
pub async fn list_hackathon_events(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonEventsSchema>>> {
|
||||||
|
let table = ResourceEnum::HackathonEvents.to_string();
|
||||||
|
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
|
||||||
|
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||||
|
.with_condition("is_deleted = false")
|
||||||
|
.with_condition(&format!("hackathon_id = type::thing('app_hackathons', '{}')", normalized_hackathon_id))
|
||||||
|
.search_field("title")
|
||||||
|
.select_fields(vec!["*"]);
|
||||||
|
|
||||||
|
let mut result = builder.build().await?;
|
||||||
|
|
||||||
|
// Sort events by created_at (oldest first) to ensure deterministic ordering for tests
|
||||||
|
result.data.sort_by_key(|s: &HackathonEventsSchema| s.created_at.clone());
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn get_hackathon_event_by_id(&self, id: String) -> Result<HackathonEventsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonEvents.to_string();
|
||||||
|
|
||||||
|
let existing: Option<HackathonEventsSchema> = self.state.surrealdb_ws
|
||||||
|
.select((table, id.clone()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let event = existing.ok_or_else(|| anyhow!("Event not found"))?;
|
||||||
|
|
||||||
|
if event.is_deleted {
|
||||||
|
bail!("Event not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id, updates), err)]
|
||||||
|
pub async fn update_hackathon_event(&self, id: String, updates: HackathonEventUpdateRequestDto) -> Result<HackathonEventsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonEvents.to_string();
|
||||||
|
|
||||||
|
// Get existing event
|
||||||
|
let existing: Option<HackathonEventsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
|
||||||
|
let mut existing = existing.ok_or_else(|| anyhow!("Event not found"))?;
|
||||||
|
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Event not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply updates
|
||||||
|
if let Some(title) = updates.title {
|
||||||
|
existing.title = title;
|
||||||
|
}
|
||||||
|
if let Some(description) = updates.description {
|
||||||
|
existing.description = Some(description);
|
||||||
|
}
|
||||||
|
if let Some(event_type) = updates.event_type {
|
||||||
|
existing.event_type = event_type;
|
||||||
|
}
|
||||||
|
if let Some(start_time) = updates.start_time {
|
||||||
|
existing.start_time = start_time;
|
||||||
|
}
|
||||||
|
if let Some(end_time) = updates.end_time {
|
||||||
|
existing.end_time = end_time;
|
||||||
|
}
|
||||||
|
if let Some(location) = updates.location {
|
||||||
|
existing.location = Some(location);
|
||||||
|
}
|
||||||
|
if let Some(virtual_link) = updates.virtual_link {
|
||||||
|
existing.virtual_link = Some(virtual_link);
|
||||||
|
}
|
||||||
|
if let Some(max_attendees) = updates.max_attendees {
|
||||||
|
existing.max_attendees = Some(max_attendees);
|
||||||
|
}
|
||||||
|
if let Some(is_mandatory) = updates.is_mandatory {
|
||||||
|
existing.is_mandatory = is_mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonEventsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.content(existing.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(e) => Ok(e),
|
||||||
|
None => bail!("Failed to update hackathon event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn delete_hackathon_event(&self, id: String) -> Result<String> {
|
||||||
|
let table = ResourceEnum::HackathonEvents.to_string();
|
||||||
|
|
||||||
|
let updates: HashMap<String, serde_json::Value> = HashMap::from([
|
||||||
|
("is_deleted".to_string(), true.into()),
|
||||||
|
("updated_at".to_string(), get_iso_date().into()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonEventsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.merge(serde_json::to_value(updates)?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Event deleted successfully".to_string()),
|
||||||
|
None => bail!("Failed to delete event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Timeline CRUD operations
|
||||||
|
impl<'a> HackathonRepository<'a> {
|
||||||
|
#[instrument(skip(self, hackathon_id, timeline), err)]
|
||||||
|
pub async fn create_hackathon_timeline(&self, hackathon_id: String, timeline: HackathonTimelineCreateRequestDto) -> Result<HackathonTimelineSchema> {
|
||||||
|
let table = ResourceEnum::HackathonTimeline.to_string();
|
||||||
|
let id = surrealdb::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
|
||||||
|
let phase_clone = timeline.phase.clone();
|
||||||
|
let schema = HackathonTimelineSchema {
|
||||||
|
id: Thing::from((table.clone(), id.clone())),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), normalized_hackathon_id)),
|
||||||
|
phase: phase_clone.clone(),
|
||||||
|
title: timeline.title.unwrap_or_else(|| phase_clone.to_string()),
|
||||||
|
description: timeline.description,
|
||||||
|
start_date: timeline.start_date,
|
||||||
|
end_date: timeline.end_date,
|
||||||
|
is_active: timeline.is_active.unwrap_or(false),
|
||||||
|
order: timeline.order.unwrap_or(0),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonTimelineSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.create((table, id))
|
||||||
|
.content(schema.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(t) => Ok(t),
|
||||||
|
None => bail!("Failed to create hackathon timeline"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, meta, hackathon_id), err)]
|
||||||
|
pub async fn list_hackathon_timeline(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonTimelineSchema>>> {
|
||||||
|
let table = ResourceEnum::HackathonTimeline.to_string();
|
||||||
|
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
|
||||||
|
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||||
|
.with_condition("is_deleted = false")
|
||||||
|
.with_condition(&format!("hackathon_id = type::thing('app_hackathons', '{}')", normalized_hackathon_id))
|
||||||
|
.search_field("title")
|
||||||
|
.select_fields(vec!["*"]);
|
||||||
|
|
||||||
|
let result = builder.build().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn get_hackathon_timeline_by_id(&self, id: String) -> Result<HackathonTimelineSchema> {
|
||||||
|
let table = ResourceEnum::HackathonTimeline.to_string();
|
||||||
|
|
||||||
|
let existing: Option<HackathonTimelineSchema> = self.state.surrealdb_ws
|
||||||
|
.select((table, id.clone()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let timeline = existing.ok_or_else(|| anyhow!("Timeline not found"))?;
|
||||||
|
|
||||||
|
if timeline.is_deleted {
|
||||||
|
bail!("Timeline not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(timeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id, updates), err)]
|
||||||
|
pub async fn update_hackathon_timeline(&self, id: String, updates: HackathonTimelineUpdateRequestDto) -> Result<HackathonTimelineSchema> {
|
||||||
|
let table = ResourceEnum::HackathonTimeline.to_string();
|
||||||
|
|
||||||
|
let existing: Option<HackathonTimelineSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
|
||||||
|
let mut existing = existing.ok_or_else(|| anyhow!("Timeline not found"))?;
|
||||||
|
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Timeline not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply updates
|
||||||
|
if let Some(phase) = updates.phase {
|
||||||
|
existing.phase = phase;
|
||||||
|
}
|
||||||
|
if let Some(title) = updates.title {
|
||||||
|
existing.title = title;
|
||||||
|
}
|
||||||
|
if let Some(description) = updates.description {
|
||||||
|
existing.description = Some(description);
|
||||||
|
}
|
||||||
|
if let Some(start_date) = updates.start_date {
|
||||||
|
existing.start_date = start_date;
|
||||||
|
}
|
||||||
|
if let Some(end_date) = updates.end_date {
|
||||||
|
existing.end_date = end_date;
|
||||||
|
}
|
||||||
|
if let Some(is_active) = updates.is_active {
|
||||||
|
existing.is_active = is_active;
|
||||||
|
}
|
||||||
|
if let Some(order) = updates.order {
|
||||||
|
existing.order = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonTimelineSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.content(existing.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(t) => Ok(t),
|
||||||
|
None => bail!("Failed to update hackathon timeline"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn delete_hackathon_timeline(&self, id: String) -> Result<String> {
|
||||||
|
let table = ResourceEnum::HackathonTimeline.to_string();
|
||||||
|
|
||||||
|
let updates: HashMap<String, serde_json::Value> = HashMap::from([
|
||||||
|
("is_deleted".to_string(), true.into()),
|
||||||
|
("updated_at".to_string(), get_iso_date().into()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonTimelineSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.merge(serde_json::to_value(updates)?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Timeline deleted successfully".to_string()),
|
||||||
|
None => bail!("Failed to delete timeline"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Submissions CRUD operations
|
||||||
|
impl<'a> HackathonRepository<'a> {
|
||||||
|
#[instrument(skip(self, hackathon_id, team_id, submission), err)]
|
||||||
|
pub async fn create_hackathon_submission(&self, hackathon_id: String, team_id: String, submission: HackathonSubmissionCreateRequestDto) -> Result<HackathonSubmissionsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
let id = surrealdb::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
let normalized_team_id = self.normalize_id("app_teams", &team_id);
|
||||||
|
|
||||||
|
let schema = HackathonSubmissionsSchema {
|
||||||
|
id: Thing::from((table.clone(), id.clone())),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), normalized_hackathon_id)),
|
||||||
|
team_id: Some(Thing::from(("app_teams".to_string(), normalized_team_id))),
|
||||||
|
project_name: Some(submission.project_name),
|
||||||
|
description: Some(submission.description),
|
||||||
|
repository_url: submission.repository_url,
|
||||||
|
upload_file_url: submission.upload_file_url,
|
||||||
|
demo_url: submission.demo_url,
|
||||||
|
slides_url: submission.slides_url,
|
||||||
|
technologies: Some(submission.technologies),
|
||||||
|
contact_instagram: submission.contact_instagram,
|
||||||
|
contact_twitter: submission.contact_twitter,
|
||||||
|
contact_linkedin: submission.contact_linkedin,
|
||||||
|
contact_facebook: submission.contact_facebook,
|
||||||
|
contact_youtube: submission.contact_youtube,
|
||||||
|
contact_tiktok: submission.contact_tiktok,
|
||||||
|
contact_other: submission.contact_other,
|
||||||
|
submission_status: Some(super::hackathon_schema::SubmissionStatus::Draft),
|
||||||
|
judge_feedback: None,
|
||||||
|
submitted_at: Some(chrono::Utc::now()),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonSubmissionsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.create((table, id))
|
||||||
|
.content(schema.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(s) => Ok(s),
|
||||||
|
None => bail!("Failed to create hackathon submission"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, meta, hackathon_id), err)]
|
||||||
|
pub async fn list_hackathon_submissions(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonSubmissionsSchema>>> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
|
||||||
|
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||||
|
.with_condition("is_deleted = false")
|
||||||
|
// Some stray records (from earlier bugs) may lack team_id; ensure we only fetch proper submissions
|
||||||
|
.with_condition("team_id IS NOT NULL")
|
||||||
|
// Ensure required string fields exist to prevent deserialization errors
|
||||||
|
.with_condition("project_name IS NOT NULL")
|
||||||
|
.with_condition("description IS NOT NULL")
|
||||||
|
.with_condition("technologies IS NOT NULL")
|
||||||
|
.with_condition(&format!("hackathon_id = type::thing('app_hackathons', '{}')", normalized_hackathon_id))
|
||||||
|
.search_field("project_name")
|
||||||
|
.select_fields(vec!["*"]);
|
||||||
|
|
||||||
|
let mut result = builder.build().await?;
|
||||||
|
// Ensure deterministic ordering for listings by sorting on created_at (oldest first).
|
||||||
|
result.data.sort_by_key(|s: &HackathonSubmissionsSchema| s.created_at.clone());
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, meta, team_id), err)]
|
||||||
|
pub async fn list_submissions_by_team(&self, meta: imphnen_libs::MetaRequestDto, team_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonSubmissionsSchema>>> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
|
||||||
|
let normalized_team_id = self.normalize_id("app_teams", &team_id);
|
||||||
|
|
||||||
|
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||||
|
.with_condition("is_deleted = false")
|
||||||
|
// Ensure we don't deserialize records without a team_id
|
||||||
|
.with_condition("team_id IS NOT NULL")
|
||||||
|
// Ensure required string fields exist to prevent deserialization errors
|
||||||
|
.with_condition("project_name IS NOT NULL")
|
||||||
|
.with_condition("description IS NOT NULL")
|
||||||
|
.with_condition("technologies IS NOT NULL")
|
||||||
|
.with_condition(&format!("team_id = type::thing('app_teams', '{}')", normalized_team_id))
|
||||||
|
.search_field("project_name")
|
||||||
|
.select_fields(vec!["*"]);
|
||||||
|
|
||||||
|
let mut result = builder.build().await?;
|
||||||
|
result.data.sort_by_key(|s: &HackathonSubmissionsSchema| s.created_at.clone());
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id, status, feedback), err)]
|
||||||
|
pub async fn update_submission_status(&self, id: String, status: super::hackathon_schema::SubmissionStatus, feedback: Option<String>) -> Result<HackathonSubmissionsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
|
||||||
|
let existing: Option<HackathonSubmissionsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
|
||||||
|
let mut existing = existing.ok_or_else(|| anyhow!("Submission not found"))?;
|
||||||
|
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Submission not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.submission_status = Some(status);
|
||||||
|
existing.judge_feedback = feedback;
|
||||||
|
existing.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
let record: Option<HackathonSubmissionsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.content(existing.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(s) => Ok(s),
|
||||||
|
None => bail!("Failed to update submission status"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id, updates), err)]
|
||||||
|
pub async fn update_hackathon_submission(&self, id: String, updates: HackathonSubmissionUpdateRequestDto) -> Result<HackathonSubmissionsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
|
||||||
|
let existing: Option<HackathonSubmissionsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
|
||||||
|
let mut existing = existing.ok_or_else(|| anyhow!("Submission not found"))?;
|
||||||
|
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Submission not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply updates
|
||||||
|
if let Some(project_name) = updates.project_name {
|
||||||
|
existing.project_name = Some(project_name);
|
||||||
|
}
|
||||||
|
if let Some(description) = updates.description {
|
||||||
|
existing.description = Some(description);
|
||||||
|
}
|
||||||
|
if let Some(repository_url) = updates.repository_url {
|
||||||
|
existing.repository_url = Some(repository_url);
|
||||||
|
}
|
||||||
|
if let Some(upload_file_url) = updates.upload_file_url {
|
||||||
|
existing.upload_file_url = Some(upload_file_url);
|
||||||
|
}
|
||||||
|
if let Some(demo_url) = updates.demo_url {
|
||||||
|
existing.demo_url = Some(demo_url);
|
||||||
|
}
|
||||||
|
if let Some(slides_url) = updates.slides_url {
|
||||||
|
existing.slides_url = Some(slides_url);
|
||||||
|
}
|
||||||
|
if let Some(technologies) = updates.technologies {
|
||||||
|
existing.technologies = Some(technologies);
|
||||||
|
}
|
||||||
|
if let Some(contact_instagram) = updates.contact_instagram {
|
||||||
|
existing.contact_instagram = Some(contact_instagram);
|
||||||
|
}
|
||||||
|
if let Some(contact_twitter) = updates.contact_twitter {
|
||||||
|
existing.contact_twitter = Some(contact_twitter);
|
||||||
|
}
|
||||||
|
if let Some(contact_linkedin) = updates.contact_linkedin {
|
||||||
|
existing.contact_linkedin = Some(contact_linkedin);
|
||||||
|
}
|
||||||
|
if let Some(contact_facebook) = updates.contact_facebook {
|
||||||
|
existing.contact_facebook = Some(contact_facebook);
|
||||||
|
}
|
||||||
|
if let Some(contact_youtube) = updates.contact_youtube {
|
||||||
|
existing.contact_youtube = Some(contact_youtube);
|
||||||
|
}
|
||||||
|
if let Some(contact_tiktok) = updates.contact_tiktok {
|
||||||
|
existing.contact_tiktok = Some(contact_tiktok);
|
||||||
|
}
|
||||||
|
if let Some(contact_other) = updates.contact_other {
|
||||||
|
existing.contact_other = Some(contact_other);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonSubmissionsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.content(existing.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(s) => Ok(s),
|
||||||
|
None => bail!("Failed to update hackathon submission"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn get_hackathon_submission_by_id(&self, id: String) -> Result<HackathonSubmissionsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
info!(query = %format!("SELECT * FROM {} WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
|
||||||
|
let record: Option<HackathonSubmissionsSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_ws
|
||||||
|
.select((table, id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(s) => {
|
||||||
|
if s.is_deleted {
|
||||||
|
bail!("Submission not found");
|
||||||
|
}
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
None => bail!("Submission not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn submit_hackathon_submission(&self, id: String) -> Result<HackathonSubmissionsSchema> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
|
||||||
|
let existing: Option<HackathonSubmissionsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
|
||||||
|
let mut existing = existing.ok_or_else(|| anyhow!("Submission not found"))?;
|
||||||
|
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Submission not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.submission_status = Some(super::hackathon_schema::SubmissionStatus::Submitted);
|
||||||
|
existing.submitted_at = Some(chrono::Utc::now());
|
||||||
|
existing.updated_at = Some(get_iso_date());
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET submission_status = 'Submitted' WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonSubmissionsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.content(existing.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(s) => Ok(s),
|
||||||
|
None => bail!("Failed to submit hackathon submission"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, id), err)]
|
||||||
|
pub async fn delete_hackathon_submission(&self, id: String) -> Result<String> {
|
||||||
|
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||||
|
|
||||||
|
let updates: HashMap<String, serde_json::Value> = HashMap::from([
|
||||||
|
("is_deleted".to_string(), true.into()),
|
||||||
|
("updated_at".to_string(), get_iso_date().into()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<HackathonSubmissionsSchema> = self
|
||||||
|
.state.surrealdb_ws
|
||||||
|
.update((table, id))
|
||||||
|
.merge(serde_json::to_value(updates)?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Submission deleted successfully".to_string()),
|
||||||
|
None => bail!("Failed to delete submission"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[instrument(skip(self, hackathon_id), err)]
|
||||||
|
pub async fn get_submission_timeline_phase(&self, hackathon_id: String) -> Result<Option<HackathonTimelineSchema>> {
|
||||||
|
let table = ResourceEnum::HackathonTimeline.to_string();
|
||||||
|
|
||||||
|
info!(query = %format!("SELECT * FROM {} WHERE hackathon_id = 'app_hackathons:{}' AND phase = 'Submission' AND is_deleted = false LIMIT 1", table, hackathon_id), "Executing SurrealDB query");
|
||||||
|
|
||||||
|
let mut result = self.state.surrealdb_ws
|
||||||
|
.query("SELECT * FROM type::table($table) WHERE hackathon_id = type::thing('app_hackathons', $hackathon_id) AND phase = $phase AND is_deleted = false LIMIT 1")
|
||||||
|
.bind(("table", table))
|
||||||
|
.bind(("hackathon_id", hackathon_id))
|
||||||
|
.bind(("phase", HackathonPhase::Submission))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let timeline: Option<HackathonTimelineSchema> = result.take(0)?;
|
||||||
|
|
||||||
|
Ok(timeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackathon Participants CRUD operations
|
||||||
|
impl<'a> HackathonRepository<'a> {
|
||||||
|
#[instrument(skip(self, hackathon_id, user_id), err)]
|
||||||
|
pub async fn create_hackathon_participant(&self, hackathon_id: String, user_id: String) -> Result<super::hackathon_schema::HackathonParticipantSchema> {
|
||||||
|
// Use the dedicated participants table to avoid polluting submissions
|
||||||
|
let table = "app_hackathon_participants".to_string();
|
||||||
|
let id = surrealdb::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
|
||||||
|
let schema = super::hackathon_schema::HackathonParticipantSchema {
|
||||||
|
id: Thing::from((table.clone(), id.clone())),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), normalized_hackathon_id)),
|
||||||
|
user_id,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
|
||||||
|
let record: Option<super::hackathon_schema::HackathonParticipantSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_ws
|
||||||
|
.create((table, id))
|
||||||
|
.content(schema.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(p) => Ok(p),
|
||||||
|
None => bail!("Failed to create participant"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, meta, hackathon_id), err)]
|
||||||
|
pub async fn list_hackathon_participants(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<super::hackathon_schema::HackathonParticipantSchema>>> {
|
||||||
|
let table = "app_hackathon_participants".to_string();
|
||||||
|
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||||
|
|
||||||
|
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||||
|
.with_condition("is_deleted = false")
|
||||||
|
.with_condition(&format!("hackathon_id = type::thing('app_hackathons', '{}')", normalized_hackathon_id))
|
||||||
|
.select_fields(vec!["*"]);
|
||||||
|
|
||||||
|
let mut result = builder.build().await?;
|
||||||
|
// sort by created_at for deterministic results
|
||||||
|
result.data.sort_by_key(|s: &super::hackathon_schema::HackathonParticipantSchema| s.created_at.clone());
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize, Deserializer};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use serde::de;
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
use imphnen_utils::make_thing;
|
||||||
|
use imphnen_utils::get_iso_date;
|
||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct HackathonSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
pub registration_deadline: DateTime<Utc>,
|
||||||
|
pub max_participants: Option<u32>,
|
||||||
|
pub status: HackathonStatus,
|
||||||
|
pub theme: Option<String>,
|
||||||
|
pub rules: Option<String>,
|
||||||
|
pub prizes: Option<Vec<Prize>>,
|
||||||
|
pub previous_winners: Option<Vec<Winner>>,
|
||||||
|
pub organizers: Vec<String>, // User IDs
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct HackathonEventsSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub hackathon_id: Thing,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub event_type: HackathonEventType,
|
||||||
|
pub start_time: DateTime<Utc>,
|
||||||
|
pub end_time: DateTime<Utc>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub virtual_link: Option<String>,
|
||||||
|
pub max_attendees: Option<u32>,
|
||||||
|
pub is_mandatory: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct HackathonTimelineSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub hackathon_id: Thing,
|
||||||
|
pub phase: HackathonPhase,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub order: u32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct HackathonSubmissionsSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub hackathon_id: Thing,
|
||||||
|
pub team_id: Option<Thing>,
|
||||||
|
pub project_name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub repository_url: Option<String>,
|
||||||
|
pub upload_file_url: Option<String>,
|
||||||
|
pub demo_url: Option<String>,
|
||||||
|
pub slides_url: Option<String>,
|
||||||
|
pub technologies: Option<Vec<String>>,
|
||||||
|
pub contact_instagram: Option<String>,
|
||||||
|
pub contact_twitter: Option<String>,
|
||||||
|
pub contact_linkedin: Option<String>,
|
||||||
|
pub contact_facebook: Option<String>,
|
||||||
|
pub contact_youtube: Option<String>,
|
||||||
|
pub contact_tiktok: Option<String>,
|
||||||
|
pub contact_other: Option<String>,
|
||||||
|
pub submission_status: Option<SubmissionStatus>,
|
||||||
|
pub judge_feedback: Option<String>,
|
||||||
|
pub submitted_at: Option<DateTime<Utc>>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Prize {
|
||||||
|
pub position: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub value: Option<String>,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Winner {
|
||||||
|
pub position: u32,
|
||||||
|
pub team_id: String,
|
||||||
|
pub project_name: String,
|
||||||
|
pub team_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, utoipa::ToSchema, strum::Display)]
|
||||||
|
pub enum HackathonStatus {
|
||||||
|
Draft,
|
||||||
|
RegistrationOpen,
|
||||||
|
RegistrationClosed,
|
||||||
|
InProgress,
|
||||||
|
Judging,
|
||||||
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, PartialEq, utoipa::ToSchema, strum::Display)]
|
||||||
|
pub enum HackathonPhase {
|
||||||
|
Registration,
|
||||||
|
Ideation,
|
||||||
|
Development,
|
||||||
|
Submission,
|
||||||
|
Judging,
|
||||||
|
Awards,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add as_str method for HackathonPhase
|
||||||
|
impl HackathonPhase {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
HackathonPhase::Registration => "registration",
|
||||||
|
HackathonPhase::Ideation => "ideation",
|
||||||
|
HackathonPhase::Development => "development",
|
||||||
|
HackathonPhase::Submission => "submission",
|
||||||
|
HackathonPhase::Judging => "judging",
|
||||||
|
HackathonPhase::Awards => "awards",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Deserialize implementation for case-insensitive support
|
||||||
|
impl<'de> Deserialize<'de> for HackathonPhase {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
let normalized = s.to_lowercase();
|
||||||
|
|
||||||
|
match normalized.as_str() {
|
||||||
|
"registration" => Ok(HackathonPhase::Registration),
|
||||||
|
"ideation" => Ok(HackathonPhase::Ideation),
|
||||||
|
"development" => Ok(HackathonPhase::Development),
|
||||||
|
"submission" => Ok(HackathonPhase::Submission),
|
||||||
|
"judging" => Ok(HackathonPhase::Judging),
|
||||||
|
"awards" => Ok(HackathonPhase::Awards),
|
||||||
|
_ => Err(serde::de::Error::custom(format!("Invalid HackathonPhase: {}", s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, PartialEq, utoipa::ToSchema, strum::Display)]
|
||||||
|
pub enum HackathonEventType {
|
||||||
|
Workshop,
|
||||||
|
Keynote,
|
||||||
|
Networking,
|
||||||
|
Judging,
|
||||||
|
Ceremony,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement case-insensitive string parsing for HackathonEventType
|
||||||
|
impl FromStr for HackathonEventType {
|
||||||
|
type Err = String;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"workshop" => Ok(Self::Workshop),
|
||||||
|
"keynote" => Ok(Self::Keynote),
|
||||||
|
"networking" => Ok(Self::Networking),
|
||||||
|
"judging" => Ok(Self::Judging),
|
||||||
|
"ceremony" => Ok(Self::Ceremony),
|
||||||
|
"other" => Ok(Self::Other),
|
||||||
|
_ => Err(format!("Invalid HackathonEventType: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Deserialize implementation for case-insensitive support
|
||||||
|
impl<'de> Deserialize<'de> for HackathonEventType {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
Self::from_str(&s).map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, utoipa::ToSchema, strum::Display)]
|
||||||
|
pub enum SubmissionStatus {
|
||||||
|
Draft,
|
||||||
|
Submitted,
|
||||||
|
Accepted,
|
||||||
|
UnderReview,
|
||||||
|
Shortlisted,
|
||||||
|
Winner,
|
||||||
|
Rejected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct HackathonParticipantSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub hackathon_id: Thing,
|
||||||
|
pub user_id: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HackathonParticipantSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
HackathonParticipantSchema {
|
||||||
|
id: make_thing(
|
||||||
|
"app_hackathon_participants",
|
||||||
|
&surrealdb::Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
|
||||||
|
user_id: String::new(),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HackathonSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
HackathonSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Hackathons.to_string(),
|
||||||
|
&surrealdb::Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
name: String::new(),
|
||||||
|
description: String::new(),
|
||||||
|
start_date: Utc::now(),
|
||||||
|
end_date: Utc::now(),
|
||||||
|
registration_deadline: Utc::now(),
|
||||||
|
max_participants: None,
|
||||||
|
status: HackathonStatus::Draft,
|
||||||
|
theme: None,
|
||||||
|
rules: None,
|
||||||
|
prizes: None,
|
||||||
|
previous_winners: None,
|
||||||
|
organizers: vec![],
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HackathonEventsSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
HackathonEventsSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::HackathonEvents.to_string(),
|
||||||
|
&surrealdb::Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
|
||||||
|
title: String::new(),
|
||||||
|
description: None,
|
||||||
|
event_type: HackathonEventType::Other,
|
||||||
|
start_time: Utc::now(),
|
||||||
|
end_time: Utc::now(),
|
||||||
|
location: None,
|
||||||
|
virtual_link: None,
|
||||||
|
max_attendees: None,
|
||||||
|
is_mandatory: false,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HackathonTimelineSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
HackathonTimelineSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::HackathonTimeline.to_string(),
|
||||||
|
&surrealdb::Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
|
||||||
|
phase: HackathonPhase::Registration,
|
||||||
|
title: String::new(),
|
||||||
|
description: None,
|
||||||
|
start_date: Utc::now(),
|
||||||
|
end_date: Utc::now(),
|
||||||
|
is_active: false,
|
||||||
|
order: 0,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HackathonSubmissionsSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
HackathonSubmissionsSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::HackathonSubmissions.to_string(),
|
||||||
|
&surrealdb::Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
|
||||||
|
team_id: Some(Thing::from(("app_teams".to_string(), surrealdb::sql::Id::rand()))),
|
||||||
|
project_name: Some(String::new()),
|
||||||
|
description: Some(String::new()),
|
||||||
|
repository_url: None,
|
||||||
|
upload_file_url: None,
|
||||||
|
demo_url: None,
|
||||||
|
slides_url: None,
|
||||||
|
technologies: Some(vec![]),
|
||||||
|
contact_instagram: None,
|
||||||
|
contact_twitter: None,
|
||||||
|
contact_linkedin: None,
|
||||||
|
contact_facebook: None,
|
||||||
|
contact_youtube: None,
|
||||||
|
contact_tiktok: None,
|
||||||
|
contact_other: None,
|
||||||
|
submission_status: Some(SubmissionStatus::Draft),
|
||||||
|
judge_feedback: None,
|
||||||
|
submitted_at: Some(Utc::now()),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
|||||||
|
use super::hackathon_schema::{HackathonSchema, HackathonStatus, HackathonTimelineSchema, HackathonPhase};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
/// Validation rules for hackathon operations
|
||||||
|
|
||||||
|
// Constants for limits
|
||||||
|
pub const MAX_ORGANIZERS: usize = 20;
|
||||||
|
pub const MAX_TIMELINES: usize = 10;
|
||||||
|
pub const MAX_EVENTS_PER_HACKATHON: usize = 50;
|
||||||
|
pub const MAX_PRIZES: usize = 20;
|
||||||
|
pub const MIN_TEAM_SIZE: u32 = 2;
|
||||||
|
pub const MAX_TEAM_SIZE: u32 = 10;
|
||||||
|
|
||||||
|
/// Validate status transition
|
||||||
|
pub fn can_transition_status(
|
||||||
|
current: &HackathonStatus,
|
||||||
|
next: &HackathonStatus,
|
||||||
|
) -> Result<()> {
|
||||||
|
use HackathonStatus::*;
|
||||||
|
|
||||||
|
let allowed_transitions: Vec<HackathonStatus> = match current {
|
||||||
|
Draft => vec![RegistrationOpen, Cancelled],
|
||||||
|
RegistrationOpen => vec![RegistrationClosed, Cancelled],
|
||||||
|
RegistrationClosed => vec![InProgress, RegistrationOpen, Cancelled], // Allow reopen
|
||||||
|
InProgress => vec![Judging, Cancelled],
|
||||||
|
Judging => vec![Completed, Cancelled],
|
||||||
|
Completed => vec![], // Terminal state
|
||||||
|
Cancelled => vec![], // Terminal state
|
||||||
|
};
|
||||||
|
|
||||||
|
if allowed_transitions.contains(next) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"Invalid status transition: {:?} -> {:?}. Allowed transitions from {:?} are: {:?}",
|
||||||
|
current,
|
||||||
|
next,
|
||||||
|
current,
|
||||||
|
allowed_transitions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate hackathon is ready for registration
|
||||||
|
pub fn validate_ready_for_registration(hackathon: &HackathonSchema) -> Result<()> {
|
||||||
|
// Check required fields for registration
|
||||||
|
if hackathon.theme.is_none() || hackathon.theme.as_ref().unwrap().trim().is_empty() {
|
||||||
|
bail!("Theme is required before opening registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
if hackathon.rules.is_none() || hackathon.rules.as_ref().unwrap().trim().is_empty() {
|
||||||
|
bail!("Rules are required before opening registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
if hackathon.prizes.is_none() || hackathon.prizes.as_ref().unwrap().is_empty() {
|
||||||
|
bail!("At least one prize is required before opening registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
if hackathon.organizers.is_empty() {
|
||||||
|
bail!("At least one organizer is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check dates are valid
|
||||||
|
let now = Utc::now();
|
||||||
|
if hackathon.registration_deadline < now {
|
||||||
|
bail!("Registration deadline must be in the future");
|
||||||
|
}
|
||||||
|
|
||||||
|
if hackathon.start_date < now {
|
||||||
|
bail!("Start date must be in the future");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate timeline phases
|
||||||
|
pub fn validate_timeline_phases(
|
||||||
|
hackathon: &HackathonSchema,
|
||||||
|
timelines: &[HackathonTimelineSchema],
|
||||||
|
) -> Result<()> {
|
||||||
|
if timelines.is_empty() {
|
||||||
|
bail!("At least one timeline phase is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if timelines.len() > MAX_TIMELINES {
|
||||||
|
bail!("Maximum {} timeline phases allowed", MAX_TIMELINES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required phases exist
|
||||||
|
let phases: Vec<HackathonPhase> = timelines.iter().map(|t| t.phase.clone()).collect();
|
||||||
|
|
||||||
|
if !phases.contains(&HackathonPhase::Registration) {
|
||||||
|
bail!("Registration phase is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !phases.contains(&HackathonPhase::Submission) {
|
||||||
|
bail!("Submission phase is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate phases
|
||||||
|
let unique_phases: HashSet<String> = timelines.iter()
|
||||||
|
.map(|t| t.phase.to_string())
|
||||||
|
.collect();
|
||||||
|
if unique_phases.len() != timelines.len() {
|
||||||
|
bail!("Duplicate timeline phases found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check order is sequential
|
||||||
|
let mut orders: Vec<u32> = timelines.iter().map(|t| t.order).collect();
|
||||||
|
orders.sort();
|
||||||
|
for (i, &order) in orders.iter().enumerate() {
|
||||||
|
if order != i as u32 {
|
||||||
|
bail!("Timeline phases must have sequential order (expected {}, got {})", i, order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overlapping timelines
|
||||||
|
let mut sorted_timelines = timelines.to_vec();
|
||||||
|
sorted_timelines.sort_by(|a, b| a.start_date.cmp(&b.start_date));
|
||||||
|
|
||||||
|
for i in 0..sorted_timelines.len() - 1 {
|
||||||
|
if sorted_timelines[i].end_date > sorted_timelines[i + 1].start_date {
|
||||||
|
bail!(
|
||||||
|
"Timeline phases cannot overlap: '{}' (ends {}) overlaps with '{}' (starts {})",
|
||||||
|
sorted_timelines[i].title,
|
||||||
|
sorted_timelines[i].end_date,
|
||||||
|
sorted_timelines[i + 1].title,
|
||||||
|
sorted_timelines[i + 1].start_date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check timeline covers entire hackathon duration
|
||||||
|
let first = sorted_timelines.first().unwrap();
|
||||||
|
let last = sorted_timelines.last().unwrap();
|
||||||
|
|
||||||
|
// Allow small tolerance (1 hour) for timezone differences
|
||||||
|
let tolerance = chrono::Duration::hours(1);
|
||||||
|
|
||||||
|
if (first.start_date - hackathon.start_date).abs() > tolerance {
|
||||||
|
bail!(
|
||||||
|
"Timeline must start at hackathon start date (Timeline: {}, Hackathon: {})",
|
||||||
|
first.start_date,
|
||||||
|
hackathon.start_date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.end_date - hackathon.end_date).abs() > tolerance {
|
||||||
|
bail!(
|
||||||
|
"Timeline must end at hackathon end date (Timeline: {}, Hackathon: {})",
|
||||||
|
last.end_date,
|
||||||
|
hackathon.end_date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check only one timeline is active
|
||||||
|
let active_count = timelines.iter().filter(|t| t.is_active).count();
|
||||||
|
if active_count > 1 {
|
||||||
|
bail!("Only one timeline phase can be active at a time");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate organizers list
|
||||||
|
pub fn validate_organizers(organizers: &[String]) -> Result<()> {
|
||||||
|
if organizers.is_empty() {
|
||||||
|
bail!("At least one organizer is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if organizers.len() > MAX_ORGANIZERS {
|
||||||
|
bail!("Maximum {} organizers allowed", MAX_ORGANIZERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
let unique: HashSet<&String> = organizers.iter().collect();
|
||||||
|
if unique.len() != organizers.len() {
|
||||||
|
bail!("Duplicate organizers found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty or invalid IDs
|
||||||
|
for organizer in organizers {
|
||||||
|
if organizer.trim().is_empty() {
|
||||||
|
bail!("Organizer ID cannot be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate prizes
|
||||||
|
pub fn validate_prizes(prizes: &[super::hackathon_schema::Prize]) -> Result<()> {
|
||||||
|
if prizes.is_empty() {
|
||||||
|
bail!("At least one prize is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if prizes.len() > MAX_PRIZES {
|
||||||
|
bail!("Maximum {} prizes allowed", MAX_PRIZES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate positions
|
||||||
|
let positions: Vec<u32> = prizes.iter().map(|p| p.position).collect();
|
||||||
|
let unique_positions: HashSet<u32> = positions.iter().copied().collect();
|
||||||
|
if unique_positions.len() != positions.len() {
|
||||||
|
bail!("Duplicate prize positions found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check positions are valid (starting from 1)
|
||||||
|
for prize in prizes {
|
||||||
|
if prize.position == 0 {
|
||||||
|
bail!("Prize position must start from 1");
|
||||||
|
}
|
||||||
|
if prize.title.trim().is_empty() {
|
||||||
|
bail!("Prize title cannot be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate dates consistency
|
||||||
|
pub fn validate_dates(
|
||||||
|
start_date: &chrono::DateTime<Utc>,
|
||||||
|
end_date: &chrono::DateTime<Utc>,
|
||||||
|
registration_deadline: &chrono::DateTime<Utc>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if end_date <= start_date {
|
||||||
|
bail!("End date must be after start date");
|
||||||
|
}
|
||||||
|
|
||||||
|
if registration_deadline >= end_date {
|
||||||
|
bail!("Registration deadline must be before end date");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration deadline should ideally be before or at start date
|
||||||
|
if registration_deadline > start_date {
|
||||||
|
// Allow but warn - some hackathons allow registration during event
|
||||||
|
tracing::warn!(
|
||||||
|
"Registration deadline ({}) is after start date ({})",
|
||||||
|
registration_deadline,
|
||||||
|
start_date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate hackathon can be deleted
|
||||||
|
pub fn validate_can_delete(hackathon: &HackathonSchema) -> Result<()> {
|
||||||
|
// Cannot delete completed hackathons (for historical records)
|
||||||
|
if hackathon.status == HackathonStatus::Completed {
|
||||||
|
bail!("Cannot delete completed hackathons. They are kept for historical records.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current active phase based on timeline
|
||||||
|
pub fn get_current_phase(timelines: &[HackathonTimelineSchema]) -> Option<HackathonPhase> {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
// Find the timeline that contains current time
|
||||||
|
for timeline in timelines {
|
||||||
|
if timeline.start_date <= now && timeline.end_date >= now {
|
||||||
|
return Some(timeline.phase.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate submission is allowed in current phase
|
||||||
|
pub fn validate_submission_allowed(timelines: &[HackathonTimelineSchema]) -> Result<()> {
|
||||||
|
let current_phase = get_current_phase(timelines);
|
||||||
|
|
||||||
|
match current_phase {
|
||||||
|
Some(HackathonPhase::Submission) => Ok(()),
|
||||||
|
Some(phase) => bail!("Submissions are not allowed in {:?} phase", phase),
|
||||||
|
None => bail!("No active phase found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate registration is allowed
|
||||||
|
pub fn validate_registration_allowed(
|
||||||
|
hackathon: &HackathonSchema,
|
||||||
|
current_participant_count: u32,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Check status
|
||||||
|
if hackathon.status != HackathonStatus::RegistrationOpen {
|
||||||
|
bail!("Registration is not open for this hackathon");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check deadline
|
||||||
|
let now = Utc::now();
|
||||||
|
if hackathon.registration_deadline < now {
|
||||||
|
bail!("Registration deadline has passed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check max participants
|
||||||
|
if let Some(max) = hackathon.max_participants {
|
||||||
|
if current_participant_count >= max {
|
||||||
|
bail!("Maximum participant limit reached");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use chrono::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_transitions() {
|
||||||
|
assert!(can_transition_status(&HackathonStatus::Draft, &HackathonStatus::RegistrationOpen).is_ok());
|
||||||
|
assert!(can_transition_status(&HackathonStatus::Draft, &HackathonStatus::Completed).is_err());
|
||||||
|
assert!(can_transition_status(&HackathonStatus::Completed, &HackathonStatus::Draft).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_organizers() {
|
||||||
|
assert!(validate_organizers(&vec!["org1".to_string()]).is_ok());
|
||||||
|
assert!(validate_organizers(&vec![]).is_err());
|
||||||
|
assert!(validate_organizers(&vec!["org1".to_string(), "org1".to_string()]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_dates() {
|
||||||
|
let now = Utc::now();
|
||||||
|
let start = now + Duration::days(1);
|
||||||
|
let end = now + Duration::days(7);
|
||||||
|
let deadline = now + Duration::hours(12);
|
||||||
|
|
||||||
|
assert!(validate_dates(&start, &end, &deadline).is_ok());
|
||||||
|
assert!(validate_dates(&end, &start, &deadline).is_err()); // end before start
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
pub mod hackathon_controller;
|
||||||
|
pub mod hackathon_dto;
|
||||||
|
pub mod hackathon_repository;
|
||||||
|
pub mod hackathon_schema;
|
||||||
|
pub mod hackathon_service;
|
||||||
|
pub mod hackathon_audit_schema;
|
||||||
|
pub mod hackathon_audit_repository;
|
||||||
|
pub mod hackathon_validation;
|
||||||
|
pub mod hackathon_atomic_service;
|
||||||
|
|
||||||
|
// Export types and functions
|
||||||
|
pub use hackathon_dto::*;
|
||||||
|
pub use hackathon_repository::HackathonRepository;
|
||||||
|
pub use hackathon_schema::*;
|
||||||
|
pub use hackathon_service::{HackathonService, HackathonServiceTrait};
|
||||||
|
pub use hackathon_audit_schema::*;
|
||||||
|
pub use hackathon_audit_repository::HackathonAuditRepository;
|
||||||
|
pub use hackathon_validation::*;
|
||||||
|
pub use hackathon_atomic_service::*;
|
||||||
|
|
||||||
|
// Export controller functions
|
||||||
|
pub use hackathon_controller::*;
|
||||||
|
|
||||||
|
pub fn hackathon_router() -> Router {
|
||||||
|
hackathon_controller::hackathon_routes()
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
pub mod hackathon;
|
||||||
|
pub mod notifications;
|
||||||
|
pub mod registrations;
|
||||||
|
|
||||||
|
// Export the router function from hackathon module
|
||||||
|
pub use hackathon::hackathon_router;
|
||||||
|
pub use notifications::notifications_router;
|
||||||
|
pub use registrations::registrations_router;
|
||||||
|
|
||||||
|
// Main route constructor
|
||||||
|
pub fn hackathon_protected_routes() -> Router {
|
||||||
|
// Protected routes include the main hackathon router (create/update/delete) and
|
||||||
|
// a protected route for updating submission status.
|
||||||
|
use hackathon::hackathon_controller::{update_submission_status, get_admin_hackathon_results};
|
||||||
|
Router::new()
|
||||||
|
.nest("/hackathons", hackathon_router())
|
||||||
|
.route("/hackathons/submissions/update/{id}/status", axum::routing::patch(update_submission_status))
|
||||||
|
.route("/hackathons/{hackathon_id}/admin/results", axum::routing::get(get_admin_hackathon_results))
|
||||||
|
.merge(registrations_router())
|
||||||
|
.merge(notifications_router())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public routes for hackathons (only listing and retrieving)
|
||||||
|
pub fn hackathon_public_routes() -> Router {
|
||||||
|
use hackathon::hackathon_controller::{
|
||||||
|
list_hackathons,
|
||||||
|
search_hackathons,
|
||||||
|
get_user_hackathon_submissions,
|
||||||
|
get_public_hackathon_results,
|
||||||
|
};
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.nest("/hackathons", Router::new()
|
||||||
|
.route("/", axum::routing::get(list_hackathons))
|
||||||
|
.route("/{id}/results", axum::routing::get(get_public_hackathon_results))
|
||||||
|
.route("/search", axum::routing::post(search_hackathons))
|
||||||
|
)
|
||||||
|
.route("/users/{user_id}/hackathon-submissions", axum::routing::get(get_user_hackathon_submissions))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod notification_controller;
|
||||||
|
pub mod notification_dto;
|
||||||
|
pub mod notification_repository;
|
||||||
|
pub mod notification_schema;
|
||||||
|
pub mod notification_service;
|
||||||
|
|
||||||
|
pub use notification_controller::notifications_router;
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
use super::notification_dto::{
|
||||||
|
DeleteNotificationResponseDto, MarkAllAsReadResponseDto, MarkAsReadResponseDto, NotificationListQueryDto, NotificationListResponseDto,
|
||||||
|
UnreadCountResponseDto,
|
||||||
|
};
|
||||||
|
use super::notification_service::Service;
|
||||||
|
use axum::{
|
||||||
|
extract::{Extension, Path, Query},
|
||||||
|
http::{HeaderMap, Response, StatusCode},
|
||||||
|
routing::{delete, get, put},
|
||||||
|
Router, body::Body,
|
||||||
|
};
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use imphnen_utils::{extract_email::extract_email, response_format::common_response};
|
||||||
|
|
||||||
|
/// Get user's notifications with optional filtering
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/notifications",
|
||||||
|
tags = ["notifications"],
|
||||||
|
params(
|
||||||
|
("page_size" = Option<usize>, Query, description = "Number of notifications per page (1-100, default: 20)"),
|
||||||
|
("page" = Option<usize>, Query, description = "Page number (min: 1, default: 1)"),
|
||||||
|
("is_read" = Option<bool>, Query, description = "Filter by read status"),
|
||||||
|
("notification_type" = Option<String>, Query, description = "Filter by notification type"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successfully retrieved notifications", body = NotificationListResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("bearer" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_notifications_handler(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Query(query): Query<NotificationListQueryDto>,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
match extract_email(&headers) {
|
||||||
|
Some(email) => {
|
||||||
|
let service = Service::new(&state);
|
||||||
|
service.get_notifications(&email, query).await
|
||||||
|
}
|
||||||
|
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a notification as read
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/notifications/update/{id}/read",
|
||||||
|
tags = ["notifications"],
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Notification ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successfully marked as read", body = MarkAsReadResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||||
|
(status = 403, description = "Forbidden - Not the notification owner"),
|
||||||
|
(status = 404, description = "Notification not found"),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("bearer" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn mark_as_read_handler(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
match extract_email(&headers) {
|
||||||
|
Some(email) => {
|
||||||
|
let service = Service::new(&state);
|
||||||
|
service.mark_as_read(&email, &id).await
|
||||||
|
}
|
||||||
|
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark all notifications as read
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/notifications/read-all",
|
||||||
|
tags = ["notifications"],
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successfully marked all notifications as read", body = MarkAllAsReadResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("bearer" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn mark_all_as_read_handler(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
match extract_email(&headers) {
|
||||||
|
Some(email) => {
|
||||||
|
let service = Service::new(&state);
|
||||||
|
service.mark_all_as_read(&email).await
|
||||||
|
}
|
||||||
|
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a notification
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/notifications/delete/{id}",
|
||||||
|
tags = ["notifications"],
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Notification ID"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successfully deleted notification", body = DeleteNotificationResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||||
|
(status = 403, description = "Forbidden - Not the notification owner"),
|
||||||
|
(status = 404, description = "Notification not found"),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("bearer" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete_notification_handler(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
match extract_email(&headers) {
|
||||||
|
Some(email) => {
|
||||||
|
let service = Service::new(&state);
|
||||||
|
service.delete_notification(&email, &id).await
|
||||||
|
}
|
||||||
|
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get unread notifications count
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/notifications/unread/count",
|
||||||
|
tags = ["notifications"],
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successfully retrieved unread count", body = UnreadCountResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("bearer" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_unread_count_handler(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
match extract_email(&headers) {
|
||||||
|
Some(email) => {
|
||||||
|
let service = Service::new(&state);
|
||||||
|
service.get_unread_count(&email).await
|
||||||
|
}
|
||||||
|
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notifications_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/notifications", get(get_notifications_handler))
|
||||||
|
.route("/notifications/update/{id}/read", put(mark_as_read_handler))
|
||||||
|
.route("/notifications/read-all", put(mark_all_as_read_handler))
|
||||||
|
.route("/notifications/delete/{id}", delete(delete_notification_handler))
|
||||||
|
.route("/notifications/unread/count", get(get_unread_count_handler))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct NotificationDto {
|
||||||
|
pub id: String,
|
||||||
|
pub notification_type: String,
|
||||||
|
pub title: String,
|
||||||
|
pub message: String,
|
||||||
|
pub is_read: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub read_at: Option<String>,
|
||||||
|
pub related_id: Option<String>,
|
||||||
|
pub action_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct NotificationListResponseDto {
|
||||||
|
pub notifications: Vec<NotificationDto>,
|
||||||
|
pub total: usize,
|
||||||
|
pub unread_count: usize,
|
||||||
|
pub page: usize,
|
||||||
|
pub page_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||||
|
pub struct NotificationListQueryDto {
|
||||||
|
#[validate(range(min = 1, max = 100))]
|
||||||
|
#[serde(default = "default_page_size")]
|
||||||
|
pub page_size: usize,
|
||||||
|
|
||||||
|
#[validate(range(min = 1))]
|
||||||
|
#[serde(default = "default_page")]
|
||||||
|
pub page: usize,
|
||||||
|
|
||||||
|
pub is_read: Option<bool>,
|
||||||
|
pub notification_type: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_page_size() -> usize {
|
||||||
|
20
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_page() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct MarkAsReadResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub is_read: bool,
|
||||||
|
pub read_at: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct MarkAllAsReadResponseDto {
|
||||||
|
pub updated_count: usize,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct DeleteNotificationResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UnreadCountResponseDto {
|
||||||
|
pub unread_count: usize,
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
use crate::v1::notifications::notification_schema::NotificationSchema;
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use imphnen_utils::get_id;
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
pub struct Repository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Repository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_user_notifications(
|
||||||
|
&self,
|
||||||
|
user_id: &Thing,
|
||||||
|
is_read: Option<bool>,
|
||||||
|
notification_type: Option<String>,
|
||||||
|
page: usize,
|
||||||
|
page_size: usize,
|
||||||
|
) -> Result<Vec<NotificationSchema>, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let offset = (page - 1) * page_size;
|
||||||
|
|
||||||
|
let mut query = "SELECT * FROM notifications WHERE user_id = $user_id ".to_string();
|
||||||
|
|
||||||
|
if let Some(is_read_val) = is_read {
|
||||||
|
query.push_str(&format!(" AND is_read = {} ", is_read_val));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref notif_type) = notification_type {
|
||||||
|
query.push_str(&format!(" AND notification_type = '{}' ", notif_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
query.push_str(&format!(
|
||||||
|
" ORDER BY created_at DESC LIMIT {} START {} ",
|
||||||
|
page_size, offset
|
||||||
|
));
|
||||||
|
|
||||||
|
let user_id_clone = user_id.clone();
|
||||||
|
let mut result = db
|
||||||
|
.query(&query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Query failed: {}", e))?;
|
||||||
|
let notifications: Vec<NotificationSchema> = result.take(0).map_err(|e| format!("Failed to parse results: {}", e))?;
|
||||||
|
Ok(notifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_user_notifications(
|
||||||
|
&self,
|
||||||
|
user_id: &Thing,
|
||||||
|
is_read: Option<bool>,
|
||||||
|
notification_type: Option<String>,
|
||||||
|
) -> Result<usize, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
|
||||||
|
let mut query = "SELECT count() as total FROM notifications WHERE user_id = $user_id ".to_string();
|
||||||
|
|
||||||
|
if let Some(is_read_val) = is_read {
|
||||||
|
query.push_str(&format!(" AND is_read = {} ", is_read_val));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref notif_type) = notification_type {
|
||||||
|
query.push_str(&format!(" AND notification_type = '{}' ", notif_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
query.push_str(" GROUP ALL ");
|
||||||
|
|
||||||
|
let user_id_clone = user_id.clone();
|
||||||
|
let mut result = db
|
||||||
|
.query(&query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Query failed: {}", e))?;
|
||||||
|
let count: Option<usize> = result.take("total").map_err(|e| format!("Failed to get count: {}", e))?;
|
||||||
|
Ok(count.unwrap_or(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_notification_by_id(
|
||||||
|
&self,
|
||||||
|
notification_id: &Thing,
|
||||||
|
) -> Result<NotificationSchema, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(notification_id).map_err(|e| e.to_string())?;
|
||||||
|
let notification: Option<NotificationSchema> = db
|
||||||
|
.select(record_key)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to fetch notification: {}", e))?;
|
||||||
|
notification.ok_or("Notification not found".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_notification(
|
||||||
|
&self,
|
||||||
|
notification_id: &Thing,
|
||||||
|
notification: NotificationSchema,
|
||||||
|
) -> Result<NotificationSchema, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(notification_id).map_err(|e| e.to_string())?;
|
||||||
|
let updated: Option<NotificationSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.content(notification)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to update notification: {}", e))?;
|
||||||
|
updated.ok_or("Failed to update notification".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mark_all_as_read(&self, user_id: &Thing) -> Result<usize, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
|
||||||
|
let query = "UPDATE notifications SET is_read = true, read_at = time::now() WHERE user_id = $user_id AND is_read = false";
|
||||||
|
|
||||||
|
let user_id_clone = user_id.clone();
|
||||||
|
let mut result = db
|
||||||
|
.query(query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Query failed: {}", e))?;
|
||||||
|
let updated: Vec<NotificationSchema> = result.take(0).map_err(|e| format!("Failed to parse results: {}", e))?;
|
||||||
|
Ok(updated.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_notification(&self, notification_id: &Thing) -> Result<(), String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(notification_id).map_err(|e| e.to_string())?;
|
||||||
|
let _: Option<NotificationSchema> = db
|
||||||
|
.delete(record_key)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to delete notification: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_unread_notifications(&self, user_id: &Thing) -> Result<usize, String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
|
||||||
|
let query = "SELECT count() as total FROM notifications WHERE user_id = $user_id AND is_read = false GROUP ALL";
|
||||||
|
|
||||||
|
let user_id_clone = user_id.clone();
|
||||||
|
let mut result = db
|
||||||
|
.query(query)
|
||||||
|
.bind(("user_id", user_id_clone))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Query failed: {}", e))?;
|
||||||
|
let count: Option<usize> = result.take("total").map_err(|e| format!("Failed to get count: {}", e))?;
|
||||||
|
Ok(count.unwrap_or(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user