Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e432a1a743 | ||
|
|
1b3366d735 | ||
|
|
b429b3a9c7 | ||
|
|
6fe495eed1 | ||
|
|
97c2fce7be | ||
|
|
98c46611fb | ||
|
|
fbfe3dcd51 | ||
|
|
5cc5a3dfe1 | ||
|
|
95ae55c9df | ||
|
|
f7ca67d720 | ||
|
|
02421e9bc2 | ||
|
|
5e2b0d3caf | ||
|
|
b9a51ce6cc | ||
|
|
d4a6c4c9ea | ||
|
|
ece6499e2b | ||
|
|
1caaa8404b | ||
|
|
cb6eef2054 | ||
|
|
e77797d7bb | ||
|
|
d5ccf4cf75 | ||
|
|
1a36698962 | ||
|
|
15a2ee1950 | ||
|
|
bef8482402 | ||
|
|
768e583f81 | ||
|
|
1ca6d8f47c | ||
|
|
3fcfb3709e | ||
|
|
6915a97d79 | ||
|
|
6f596efadd | ||
|
|
b6b5f48055 | ||
|
|
789b20c278 | ||
|
|
884b679ba8 | ||
|
|
6ef624c169 | ||
|
|
c10443f881 | ||
|
|
466ba3391a | ||
|
|
8c2527d61e | ||
|
|
018124f7b3 | ||
|
|
7749f6fdec | ||
|
|
b27e4a4404 | ||
|
|
3dde67a4ac | ||
|
|
49abcf28c3 | ||
|
|
9cecc8fa05 | ||
|
|
a32f54873b | ||
|
|
af41a98111 | ||
|
|
4c435708c0 | ||
|
|
ac462ea771 | ||
|
|
3e52223730 | ||
|
|
01e479350b | ||
|
|
b0cf9817d7 | ||
|
|
e530eba60d | ||
|
|
ef1d63e893 | ||
|
|
5859af5294 | ||
|
|
c12da948aa | ||
|
|
3cde18360c | ||
|
|
14b22328de | ||
|
|
96debc210a | ||
|
|
b80662188f | ||
|
|
7b9ce6cda1 | ||
|
|
1bb2d6a419 | ||
|
|
14690a378f | ||
|
|
17cc4ccdba | ||
|
|
1b7368a4b7 | ||
|
|
ff2e14d5f9 | ||
|
|
012aff60ef | ||
|
|
f3d88e2ac9 | ||
|
|
0e48b6755f | ||
|
|
7056f8c8a6 | ||
|
|
4ecdcd12d2 | ||
|
|
dd4a73e360 | ||
|
|
49e0a89ad1 | ||
|
|
82b85abe8f | ||
|
|
df11aa5eb8 | ||
|
|
ac44867ec1 | ||
|
|
7205afe43b | ||
|
|
b1c678c72b | ||
|
|
fa3687c6be | ||
|
|
ba46a19a5a | ||
|
|
8cdbca24ba | ||
|
|
8f981572cd | ||
|
|
39b75f410f | ||
|
|
6da1b2a7e7 | ||
|
|
acccae9f7c | ||
|
|
e7c17784cc | ||
|
|
042e0b4eff | ||
|
|
0b51a56dee | ||
|
|
cb98440b65 | ||
|
|
3c19d262f3 | ||
|
|
bdffe85544 | ||
|
|
9ed619e3fd | ||
|
|
00fd7e1907 | ||
|
|
b70126734e | ||
|
|
6af9461079 | ||
|
|
57b3662951 | ||
|
|
c0c08f2821 | ||
|
|
8402415649 | ||
|
|
01e20baa45 | ||
|
|
10dc869eaf | ||
|
|
199d0c885e | ||
|
|
89ef48a57e | ||
|
|
da1826ebba | ||
|
|
874986f1d6 | ||
|
|
bbff555a06 | ||
|
|
d3eb687a58 | ||
|
|
c5516d4006 | ||
|
|
5f51dd281d | ||
|
|
f6232832af | ||
|
|
277cf0770d | ||
|
|
650728a385 | ||
|
|
3cc3e2a7d8 | ||
|
|
39fc2f86ad | ||
|
|
7bd0ef2274 | ||
|
|
b40a430c49 | ||
|
|
44b1e09551 | ||
|
|
1b9251b2dd | ||
|
|
34b6cdabfb | ||
|
|
a25e896fa2 | ||
|
|
adf3ef8e28 | ||
|
|
ad46687f07 | ||
|
|
0ba04abee9 | ||
|
|
1e25a5b496 | ||
|
|
d7178c792d | ||
|
|
d2754de9ef | ||
|
|
cda950ed7e | ||
|
|
c6a95c1231 | ||
|
|
86116b8157 | ||
|
|
ac06224ed1 | ||
|
|
83e765f358 | ||
|
|
558e0ce568 | ||
|
|
148cf3a5cd | ||
|
|
ee9d23e01b | ||
|
|
1a2e0c58b6 | ||
|
|
e66f1f1634 | ||
|
|
6f45cf80c8 | ||
|
|
02be09506a | ||
|
|
b88ecd1361 | ||
|
|
fa435994f2 | ||
|
|
16c613d699 | ||
|
|
f598dedb3b | ||
|
|
cfa251337f | ||
|
|
cca9aa6318 | ||
|
|
f1e4e02697 | ||
|
|
a3c2715a85 | ||
|
|
89cc631fb0 | ||
|
|
36c12110c2 | ||
|
|
070060365a | ||
|
|
68e816f956 | ||
|
|
48db5f07ef | ||
|
|
a4b5d91906 | ||
|
|
9878279660 | ||
|
|
465cbee2c3 | ||
|
|
33a2ae86c6 | ||
|
|
d906fc4747 | ||
|
|
055ef6ac8e | ||
|
|
e63658c082 | ||
|
|
f4b0ed0a4a | ||
|
|
1e5ed26f76 | ||
|
|
d97095a38d | ||
|
|
e48507901e | ||
|
|
a43ef8ec04 | ||
|
|
874f25aa80 | ||
|
|
12a730a394 | ||
|
|
bdcd41f54d | ||
|
|
96fb8eb7d3 | ||
|
|
d2c333dbd6 | ||
|
|
054f0f63ff | ||
|
|
eb05ea5994 | ||
|
|
4f870ec4c9 | ||
|
|
31f9b6cfea | ||
|
|
07a93ff0b7 | ||
|
|
d2b098ed92 | ||
|
|
09a7f4371a | ||
|
|
b4bf78f0d6 | ||
|
|
de9c5a6dc1 | ||
|
|
8a661df059 | ||
|
|
c7c16d4197 | ||
|
|
3aec0346a9 | ||
|
|
8d2f720de1 | ||
|
|
4b8da68df4 | ||
|
|
6460979bab | ||
|
|
3ae9dbd717 | ||
|
|
0816566d03 | ||
|
|
a97044edfc | ||
|
|
9d92a9a136 | ||
|
|
8e12c79ea6 | ||
|
|
5777268441 | ||
|
|
34eaa418d5 | ||
|
|
77f12610dd | ||
|
|
5be737ad70 | ||
|
|
0ae1e26ba8 | ||
|
|
79721b7af6 | ||
|
|
093e290221 | ||
|
|
1e6957720e | ||
|
|
a0f263bb4f | ||
|
|
413a28c089 | ||
|
|
a90e8662e1 | ||
|
|
955d384f96 | ||
|
|
ea5ece9ab2 | ||
|
|
8ff532bb67 | ||
|
|
eb6cfd51ae | ||
|
|
11d6249007 | ||
|
|
497c250e6d | ||
|
|
2872c1827a | ||
|
|
f9c49d44b5 | ||
|
|
12043f027c | ||
|
|
0c28d34d57 | ||
|
|
271b00c2c4 | ||
|
|
b096f4129a | ||
|
|
7ab036f5f7 | ||
|
|
9ffe808797 | ||
|
|
acfb8259bc | ||
|
|
298144bd36 | ||
|
|
ff9d8cba69 | ||
|
|
8b2769706e | ||
|
|
b8606ef0bb | ||
|
|
44de53a49d | ||
|
|
9abab01b9c | ||
|
|
53414687ff | ||
|
|
d2ec21c7b1 | ||
|
|
4deee3a9cb | ||
|
|
b50ce2b060 | ||
|
|
ffc72367d5 |
@@ -0,0 +1,2 @@
|
|||||||
|
[target.x86_64-pc-windows-msvc]
|
||||||
|
linker = "rust-lld.exe"
|
||||||
+5
-5
@@ -1,5 +1,5 @@
|
|||||||
target
|
target
|
||||||
.dockerignore
|
.dockerignore
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|||||||
+34
-4
@@ -1,4 +1,34 @@
|
|||||||
PORT=
|
RUST_ENV=development
|
||||||
DATABASE_URL=
|
RUST_LOG=debug
|
||||||
ACCESS_TOKEN_SECRET=
|
PORT=4099
|
||||||
REFRESH_TOKEN_SECRET=
|
SURREALDB_URL=ws://localhost:8000/rpc
|
||||||
|
SURREALDB_USERNAME=root
|
||||||
|
SURREALDB_PASSWORD=root
|
||||||
|
SURREALDB_NAMESPACE=test
|
||||||
|
SURREALDB_DBNAME=test
|
||||||
|
ACCESS_TOKEN_SECRET=your-access-token-secret-key-here
|
||||||
|
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
||||||
|
SMTP_EMAIL=your-email@example.com
|
||||||
|
SMTP_PASSWORD=your-smtp-password
|
||||||
|
SMTP_NAME="Your App Name"
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
REDISDB_URL=localhost
|
||||||
|
FE_URL=http://localhost
|
||||||
|
MINIO_ENDPOINT=http://localhost:9000
|
||||||
|
MINIO_BUCKET_NAME=default_bucket
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=minioadmin
|
||||||
|
MINIO_SECURE=false
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID="your_google_client_id"
|
||||||
|
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
||||||
|
POOL_SIZE=10
|
||||||
|
CONNECT_TIMEOUT=30
|
||||||
|
IDLE_TIMEOUT=60
|
||||||
|
MAX_LIFETIME=1800
|
||||||
|
STATEMENT_TIMEOUT=30000
|
||||||
|
IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000
|
||||||
|
SSLMODE=require
|
||||||
|
RETRY_ATTEMPTS=3
|
||||||
|
RETRY_DELAY=1
|
||||||
|
GOOGLE_REDIRECT_URL=http://localhost:8000/api/v1/auth/google/callback
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
use flake --impure
|
use flake --impure
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: Deploy to Ancikri
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Build the project
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Stop service on VPS before upload
|
||||||
|
uses: appleboy/ssh-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||||
|
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||||
|
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
echo "Stopping the service before uploading the binary"
|
||||||
|
sudo systemctl stop imphnen-backend-service
|
||||||
|
|
||||||
|
- name: Upload artifact to VPS
|
||||||
|
uses: appleboy/scp-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||||
|
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||||
|
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||||
|
source: ./target/release/*
|
||||||
|
target: /opt/imphnen-backend-service/imphnen-backend-service
|
||||||
|
rm: true
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||||
|
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||||
|
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Restarting the service"
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
sudo systemctl restart imphnen-backend-service
|
||||||
|
|
||||||
|
echo "Deployment completed successfully"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["develop"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["develop"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
+16
-14
@@ -1,14 +1,16 @@
|
|||||||
# Build
|
# Build
|
||||||
/target
|
/target
|
||||||
/resutl
|
/resutl
|
||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
/.direnv
|
/.direnv
|
||||||
|
/Cargo.nix
|
||||||
# Environment
|
|
||||||
.envrc
|
# Environment
|
||||||
.env
|
.envrc
|
||||||
.env.local
|
.env
|
||||||
.env.development
|
.env.local
|
||||||
.env.staging
|
.env.development
|
||||||
.env.production
|
.env.staging
|
||||||
|
.env.production
|
||||||
|
**/**.log
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||||
|
# * For C, use cpp
|
||||||
|
# * For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
language: rust
|
||||||
|
|
||||||
|
# the encoding used by text files in the project
|
||||||
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed) on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "imphnen-backend-service"
|
||||||
Generated
+2178
-590
File diff suppressed because it is too large
Load Diff
+86
-15
@@ -1,24 +1,95 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "imphnen-cms-be"
|
resolver = "2"
|
||||||
version = "0.1.0"
|
members = [
|
||||||
edition = "2021"
|
"imphnen-entities", # Most basic - core data structures
|
||||||
|
"imphnen-macros", # Macros
|
||||||
|
"imphnen-libs", # Depends on entities
|
||||||
|
"imphnen-utils", # Depends on libs and entities
|
||||||
|
"imphnen-middleware",# Utility for permissions
|
||||||
|
"imphnen-iam", # Core auth service, depends on libs, utils, entities
|
||||||
|
"imphnen-cms", # Content management, depends on core services
|
||||||
|
"imphnen-gacha", # Game mechanics, depends on core services
|
||||||
|
"imphnen-dimentorin",# Learning platform, depends on core services
|
||||||
|
"imphnen-gateway", # API gateway, depends on all services
|
||||||
|
"imphnen-backend", # Main application, depends on all services
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
axum = { version = "0.8.1", features = ["multipart"] }
|
[workspace.dependencies]
|
||||||
log = "0.4.25"
|
async-trait = "0.1.83"
|
||||||
sea-orm = { version = "1.1.4", features = ["sqlx-postgres", "macros", "with-json", "with-uuid", "runtime-tokio", "runtime-tokio-native-tls"] }
|
oauth2 = "5.0.0"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
reqwest = { version = "0.12.23", features = ["json"] }
|
||||||
serde_json = "1.0.138"
|
serde_json = "1.0.142"
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
axum = { version = "0.8.4", features = ["multipart", "macros"] }
|
||||||
|
log = "0.4.27"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||||
jsonwebtoken = "9.3.1"
|
jsonwebtoken = "9.3.1"
|
||||||
chrono = "0.4.39"
|
chrono = "0.4.41"
|
||||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
redis = "0.28.2"
|
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||||
|
thiserror = "2.0.14"
|
||||||
|
anyhow = "1.0.99"
|
||||||
|
rand = { version = "0.9.2", features = ["std", "alloc"] }
|
||||||
|
rand_distr = "0.5.1"
|
||||||
|
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
|
||||||
|
http-body-util = "0.1.3"
|
||||||
|
zod-rs = { version = "0.4", features = ["macros"] }
|
||||||
|
zod-rs-util = "0.4"
|
||||||
|
paginator-rs = "0.2"
|
||||||
|
paginator-utils = "0.2"
|
||||||
|
paginator-sea-orm = { version = "0.2", features = ["sqlx-postgres", "runtime-tokio"] }
|
||||||
|
paginator-axum = "0.2"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
regex = "1.11.1"
|
||||||
|
axum-test = "17.3.0"
|
||||||
|
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||||
|
fancy-regex = "0.16.1"
|
||||||
|
futures = "0.3.31"
|
||||||
|
tower = "0.5.2"
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
tracing = "0.1.41"
|
||||||
|
uuid = { version = "1.18.0", features = ["v4", "fast-rng", "serde"] }
|
||||||
|
strum = { version = "0.27.2", features = ["derive"] }
|
||||||
|
strum_macros = "0.27.2"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
hmac = "0.12"
|
||||||
|
hex = "0.4"
|
||||||
|
urlencoding = "2.1"
|
||||||
|
hyper = "1.6.0"
|
||||||
|
hyper-util = "0.1.16"
|
||||||
|
minio = "0.3.0"
|
||||||
|
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros", "with-chrono", "uuid"] }
|
||||||
|
num_cpus = "1.16.0"
|
||||||
|
|
||||||
|
|
||||||
|
tokio-test = "0.4.4"
|
||||||
|
mockall = "0.13.1"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
|
tokio-tungstenite = "0.27"
|
||||||
|
url = "2.5"
|
||||||
|
futures-util = "0.3"
|
||||||
|
http = "1.3"
|
||||||
|
imphnen-iam = { path = "./imphnen-iam" }
|
||||||
|
imphnen-cms = { path = "./imphnen-cms" }
|
||||||
|
imphnen-libs = { path = "./imphnen-libs" }
|
||||||
|
imphnen-utils = { path = "./imphnen-utils" }
|
||||||
|
imphnen-gacha = { path = "./imphnen-gacha" }
|
||||||
|
imphnen-gateway = { path = "./imphnen-gateway" }
|
||||||
|
imphnen-backend = { path = "./imphnen-backend" }
|
||||||
|
imphnen-entities = { path = "./imphnen-entities" }
|
||||||
|
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||||
|
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||||
|
imphnen-macros = { path = "./imphnen-macros" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|
||||||
|
|||||||
+47
-22
@@ -1,22 +1,47 @@
|
|||||||
FROM rust:1.83-slim-bullseye AS builder
|
FROM rust:1.86-alpine AS builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apk add --no-cache \
|
||||||
build-essential \
|
curl \
|
||||||
libssl-dev \
|
musl-dev \
|
||||||
pkg-config \
|
openssl-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
openssl-libs-static \
|
||||||
|
pkgconfig
|
||||||
WORKDIR /app
|
|
||||||
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY ./src ./src
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
RUN cargo build --release && strip /app/target/release/imphnen-cms-api
|
RUN mkdir -p imphnen-backend/src imphnen-cms/src imphnen-dimentorin/src \
|
||||||
|
imphnen-entities/src imphnen-gacha/src imphnen-gateway/src \
|
||||||
FROM gcr.io/distroless/cc AS runner
|
imphnen-iam/src imphnen-libs/src imphnen-middleware/src \
|
||||||
|
imphnen-utils/src tests/src && \
|
||||||
WORKDIR /app
|
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
||||||
|
find . -name "src" -type d -exec sh -c 'echo "// dummy" > "$1/lib.rs"' _ {} \;
|
||||||
COPY --from=builder /app/target/release/imphnen-cms-api .
|
|
||||||
|
RUN echo '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||||
CMD ["/app/imphnen-cms-api"]
|
|
||||||
|
|
||||||
|
RUN echo -e '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||||
|
|
||||||
|
|
||||||
|
COPY imphnen-backend ./imphnen-backend
|
||||||
|
COPY imphnen-cms ./imphnen-cms
|
||||||
|
COPY imphnen-dimentorin ./imphnen-dimentorin
|
||||||
|
COPY imphnen-entities ./imphnen-entities
|
||||||
|
COPY imphnen-gacha ./imphnen-gacha
|
||||||
|
COPY imphnen-gateway ./imphnen-gateway
|
||||||
|
COPY imphnen-iam ./imphnen-iam
|
||||||
|
COPY imphnen-libs ./imphnen-libs
|
||||||
|
COPY imphnen-middleware ./imphnen-middleware
|
||||||
|
COPY imphnen-utils ./imphnen-utils
|
||||||
|
COPY tests ./tests
|
||||||
|
|
||||||
|
RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-units=1 -C strip=symbols" \
|
||||||
|
cargo build -p imphnen-backend --release && \
|
||||||
|
strip target/release/api && \
|
||||||
|
upx --best --lzma target/release/api 2>/dev/null || true
|
||||||
|
|
||||||
|
FROM scratch AS runner
|
||||||
|
COPY --from=builder /app/target/release/api /api
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
ENTRYPOINT ["/api"]
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# IMPHNEN Backend Service
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
This repository serves as the **monorepo** for all backend services of IMPHNEN. It encompasses several main services:
|
||||||
|
|
||||||
|
1. **IMPHNEN-Backend** - Provides fundamental functionalities and shared resources for other services.
|
||||||
|
2. **IMPHNEN-IAM** - Handles identity and access management across IMPHNEN applications.
|
||||||
|
3. **IMPHNEN-CMS** - Supports the cms services by IMPHNEN [Landing Page website](https://imphnen.dev/).
|
||||||
|
4. **IMPHNEN-Gacha** - Supports the gacha services by IMPHNEN [Gacha website](https://gacha.imphnen.dev/).
|
||||||
|
5. **IMPHNEN-Dimentorin** - Supports the mentoring services by IMPHNEN [Dimentorin website](https://dimentorin.imphnen.dev/).
|
||||||
|
6. **IMPHNEN-Gateway** - Acts as the API gateway, routing requests to appropriate services.
|
||||||
|
7. **IMPHNEN-Middleware** - Acts as the middleware for the API Gateway, providing authentication and authorization.
|
||||||
|
|
||||||
|
## How to Install
|
||||||
|
|
||||||
|
1. **Clone the repository**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-backend-service.git
|
||||||
|
cd imphnen-backend-service
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up the environment**:
|
||||||
|
|
||||||
|
- Copy the example environment files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
if you use windows based system
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./apply-env.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
if you use unix based system
|
||||||
|
|
||||||
|
```sh
|
||||||
|
source ./apply-env.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- Modify the `.env` files with your specific configuration settings.
|
||||||
|
|
||||||
|
3. **Install dependencies**:
|
||||||
|
|
||||||
|
Ensure you have [Rust](https://www.rust-lang.org/) installed. Then, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the seeders**:
|
||||||
|
|
||||||
|
to run the seeders, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --bin seeder
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
To run the services in development mode:
|
||||||
|
|
||||||
|
1. **Start the database and other dependencies** using Docker Compose:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run using cargo run**. For example, to run the Core Service:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --bin api
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run using cargo watch**. For example, to run the Core Service:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo watch -x "run --bin api"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
For production deployment:
|
||||||
|
|
||||||
|
1. **Build the Docker image**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t imphnen-backend .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the Docker container**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --name imphnen-backend -d --env-file .env -p 3000:3000 imphnen-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust the port and environment variables as needed.
|
||||||
|
|
||||||
|
## How to Run the Tests
|
||||||
|
|
||||||
|
1. **Run the tests**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test -p tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
1. **Fork the repository** and clone it locally.
|
||||||
|
2. **Create a new branch** for your feature or fix:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git checkout -b feat/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Make your changes**, commit them, and push to your forked repository.
|
||||||
|
4. **Create a pull request** to the `develop` branch of this repository.
|
||||||
|
|
||||||
|
If you encounter any issues or have questions, feel free to create a new issue in the repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Note: For detailed API documentation, please refer to our [API Docs](https://api.imphnen.dev/docs)._
|
||||||
+15
-15
@@ -1,15 +1,15 @@
|
|||||||
{pkgs ? import <nixpkgs> {}}: let
|
{pkgs ? import <nixpkgs> {}}: let
|
||||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||||
rustDeps = pkgs.callPackage ./Cargo.nix {};
|
rustDeps = pkgs.callPackage ./Cargo.nix {};
|
||||||
packageEntry = rustDeps.workspaceMembers.${manifest.name};
|
packageEntry = rustDeps.workspaceMembers.${manifest.name};
|
||||||
deps = packageEntry.build.cargoDeps or null;
|
deps = packageEntry.build.cargoDeps or null;
|
||||||
in
|
in
|
||||||
pkgs.rustPlatform.buildRustPackage {
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
pname = manifest.name;
|
pname = manifest.name;
|
||||||
version = manifest.version;
|
version = manifest.version;
|
||||||
cargoDeps = deps;
|
cargoDeps = deps;
|
||||||
src = pkgs.lib.cleanSource ./.;
|
src = pkgs.lib.cleanSource ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
nativeBuildInputs = [pkgs.openssl pkgs.pkg-config];
|
nativeBuildInputs = [pkgs.openssl pkgs.pkg-config];
|
||||||
buildInputs = [pkgs.openssl];
|
buildInputs = [pkgs.openssl];
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-8
@@ -1,8 +1,46 @@
|
|||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "${PORT}:${PORT}"
|
- "${PORT}:${PORT}"
|
||||||
env_file: ".env"
|
env_file: ".env"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: imphnen_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-imphnen}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-imphnen}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: minio
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
|||||||
+43
-42
@@ -1,42 +1,43 @@
|
|||||||
{pkgs, ...}: let
|
{ pkgs, ... }:
|
||||||
baseImage = pkgs.ociTools.pullImage {
|
let
|
||||||
imageName = "ubuntu";
|
baseImage = pkgs.ociTools.pullImage {
|
||||||
tag = "latest";
|
imageName = "ubuntu";
|
||||||
};
|
tag = "latest";
|
||||||
in
|
};
|
||||||
pkgs.dockerTools.buildImage {
|
in
|
||||||
name = "imphnen-cms-api";
|
pkgs.dockerTools.buildImage {
|
||||||
|
name = "imphnen-backend-service";
|
||||||
fromImage = baseImage;
|
|
||||||
|
fromImage = baseImage;
|
||||||
copyToRoot = pkgs.buildEnv {
|
|
||||||
name = "imphnen-cms-api";
|
copyToRoot = pkgs.buildEnv {
|
||||||
paths = [
|
name = "imphnen-backend-service";
|
||||||
(pkgs.stdenv.mkDerivation {
|
paths = [
|
||||||
name = "imphnen-cms-api";
|
(pkgs.stdenv.mkDerivation {
|
||||||
src = ./src;
|
name = "imphnen-backend-service";
|
||||||
|
src = ./src;
|
||||||
buildInputs = [
|
|
||||||
pkgs.rustc
|
buildInputs = [
|
||||||
pkgs.cargo
|
pkgs.rustc
|
||||||
pkgs.openssl
|
pkgs.cargo
|
||||||
pkgs.pkg-config
|
pkgs.openssl
|
||||||
];
|
pkgs.pkg-config
|
||||||
|
];
|
||||||
buildPhase = ''
|
|
||||||
cargo build --release
|
buildPhase = ''
|
||||||
'';
|
cargo build --release
|
||||||
|
'';
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/bin
|
installPhase = ''
|
||||||
cp target/release/najm-course-api $out/bin/
|
mkdir -p $out/bin
|
||||||
'';
|
cp target/release/imphnen-backend-service $out/bin/
|
||||||
})
|
'';
|
||||||
];
|
})
|
||||||
};
|
];
|
||||||
|
};
|
||||||
config = {
|
|
||||||
Cmd = ["/bin/imphnen-cms-api"];
|
config = {
|
||||||
WorkingDir = "/bin";
|
Cmd = [ "/bin/imphnen-backend-service" ];
|
||||||
};
|
WorkingDir = "/bin";
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 351 KiB |
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1739020877,
|
||||||
|
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -1,24 +1,41 @@
|
|||||||
{
|
{
|
||||||
description = "IMPHNEN CMS API Nix Flake";
|
description = "IMPHNEN Backend Service Nix Flake";
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
inputs = {
|
||||||
};
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
outputs = {
|
};
|
||||||
self,
|
|
||||||
nixpkgs,
|
outputs =
|
||||||
}: let
|
{
|
||||||
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
self,
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
nixpkgs,
|
||||||
pkgsFor = nixpkgs.legacyPackages;
|
}:
|
||||||
in {
|
let
|
||||||
packages = forAllSystems (system: {
|
supportedSystems = [
|
||||||
default = pkgsFor.${system}.callPackage ./default.nix {};
|
"x86_64-linux"
|
||||||
});
|
"x86_64-darwin"
|
||||||
devShells = forAllSystems (system: {
|
"aarch64-darwin"
|
||||||
default = pkgsFor.${system}.callPackage ./shell.nix {};
|
"aarch64-linux"
|
||||||
});
|
];
|
||||||
dockerImages = forAllSystems (system: {
|
pkgsFor =
|
||||||
tryOutApi = pkgsFor.${system}.callPackage ./docker.nix {};
|
system:
|
||||||
});
|
import nixpkgs {
|
||||||
};
|
inherit system;
|
||||||
}
|
config = {
|
||||||
|
allowUnfree = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (system: {
|
||||||
|
default = (pkgsFor system).callPackage ./default.nix { };
|
||||||
|
});
|
||||||
|
devShells = forAllSystems (system: {
|
||||||
|
default = (pkgsFor system).callPackage ./shell.nix { };
|
||||||
|
});
|
||||||
|
dockerImages = forAllSystems (system: {
|
||||||
|
tryOutApi = (pkgsFor system).callPackage ./docker.nix { };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-backend"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "api"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "create_schema"
|
||||||
|
path = "src/bin/create_schema.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seeder"
|
||||||
|
path = "src/bin/seeder.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_events"
|
||||||
|
path = "src/bin/seed_events.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_gacha_rolls"
|
||||||
|
path = "src/bin/seed_gacha_rolls.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_mentor_user"
|
||||||
|
path = "src/bin/seed_mentor_user.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_permissions"
|
||||||
|
path = "src/bin/seed_permissions.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_roles"
|
||||||
|
path = "src/bin/seed_roles.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_roles_permissions"
|
||||||
|
path = "src/bin/seed_roles_permissions.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed_test_data"
|
||||||
|
path = "src/bin/seed_test_data.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "test_postgres"
|
||||||
|
path = "src/bin/test_postgres.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sea-orm.workspace = true
|
||||||
|
imphnen-libs.workspace = true
|
||||||
|
imphnen-utils.workspace = true
|
||||||
|
imphnen-gateway.workspace = true
|
||||||
|
imphnen-entities.workspace = true
|
||||||
|
imphnen-iam.workspace = true
|
||||||
|
imphnen-cms.workspace = true
|
||||||
|
imphnen-gacha.workspace = true
|
||||||
|
imphnen-dimentorin.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
uuid.workspace=true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// API entry point using PostgreSQL (SurrealDB migration complete)
|
||||||
|
// This file has been updated to use SeaORM with PostgreSQL instead of SurrealDB
|
||||||
|
use imphnen_gateway::gateway_service;
|
||||||
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
axum_init(|postgres_db| async {
|
||||||
|
// Gateway service now uses PostgreSQL exclusively (SeaORM)
|
||||||
|
// SurrealDB dependencies have been completely removed
|
||||||
|
gateway_service(postgres_db).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::{Statement, ConnectionTrait};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
// New default behavior: execute by default; use --dry-run to preview only.
|
||||||
|
let dry_run = args.iter().any(|s| s == "--dry-run" || s == "--no-exec" || s == "--dry");
|
||||||
|
let force = args.iter().any(|s| s == "--force" || s == "-f");
|
||||||
|
|
||||||
|
println!("🔎 Clear DB script - WARNING: This will remove data from tables\n");
|
||||||
|
println!("Note: script now runs by default (no --yes required). To preview without executing, use --dry-run.\n");
|
||||||
|
|
||||||
|
// List of tables to truncate (order doesn't matter with CASCADE)
|
||||||
|
let tables = vec![
|
||||||
|
"gacha_claims",
|
||||||
|
"gacha_rolls",
|
||||||
|
"gacha_items",
|
||||||
|
"gacha_credits",
|
||||||
|
"audit_logs",
|
||||||
|
"rate_limits",
|
||||||
|
"testimonials",
|
||||||
|
"events",
|
||||||
|
"app_mentors",
|
||||||
|
"app_sessions",
|
||||||
|
"app_roles_permissions",
|
||||||
|
"app_permissions",
|
||||||
|
"app_roles",
|
||||||
|
"app_users",
|
||||||
|
];
|
||||||
|
|
||||||
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
// Filter tables that actually exist in the database
|
||||||
|
let mut existing_tables: Vec<&str> = vec![];
|
||||||
|
for t in tables.iter() {
|
||||||
|
let check_sql = format!(
|
||||||
|
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{}') as exists;",
|
||||||
|
t
|
||||||
|
);
|
||||||
|
let stmt = Statement::from_string(db.get_database_backend(), check_sql);
|
||||||
|
if let Ok(Some(row)) = pg_conn.query_one(stmt).await {
|
||||||
|
let exists_val: Option<bool> = row.try_get("", "exists").ok();
|
||||||
|
if exists_val.unwrap_or(false) {
|
||||||
|
existing_tables.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing_tables.is_empty() {
|
||||||
|
println!("No configured tables found to clear - nothing to do.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let truncate_sql = format!(
|
||||||
|
"TRUNCATE TABLE {} RESTART IDENTITY CASCADE;",
|
||||||
|
existing_tables.join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("The script will run the following SQL (on the DB configured by env vars):\n\n{}", truncate_sql);
|
||||||
|
|
||||||
|
// Prevent accidental execution in production without explicit force flag
|
||||||
|
let env_name = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string());
|
||||||
|
if env_name == "production" && !force {
|
||||||
|
println!("Security: RUST_ENV=production; the script will NOT run without --force. Use --force to override.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
println!("Dry run enabled. No changes applied. To execute, re-run without --dry-run or use --force (in production).");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Executing truncate...\n");
|
||||||
|
|
||||||
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
let stmt = Statement::from_string(db.get_database_backend(), truncate_sql);
|
||||||
|
match pg_conn.execute(stmt).await {
|
||||||
|
Ok(_) => println!("✅ Successfully cleared DB tables"),
|
||||||
|
Err(e) => println!("❌ Failed to clear DB tables: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use sea_orm::{ConnectionTrait, Database, Schema, DbBackend, EntityTrait};
|
||||||
|
use imphnen_libs::postgres::PostgresConfig;
|
||||||
|
use imphnen_entities::seaorm::{auth, common, gacha};
|
||||||
|
use sea_orm::sea_query::Table;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🛠️ Creating database schema...");
|
||||||
|
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let db = Database::connect(&config.database_url).await?;
|
||||||
|
let builder = db.get_database_backend();
|
||||||
|
|
||||||
|
println!(" Database connected. Creating/updating tables...");
|
||||||
|
|
||||||
|
// Dropping and recreating tables to ensure schema is up-to-date
|
||||||
|
// This is safer for development/testing environments to prevent schema drift.
|
||||||
|
drop_and_create_table(&db, builder, "app_roles", auth::roles::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_permissions", auth::permissions::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_users", auth::users::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_roles_permissions", auth::roles_permissions::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_mentors", auth::mentors::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_sessions", auth::sessions::Entity).await?;
|
||||||
|
|
||||||
|
drop_and_create_table(&db, builder, "events", common::events::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "testimonials", common::testimonials::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "audit_logs", common::audit_log::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "rate_limits", common::rate_limit::Entity).await?;
|
||||||
|
|
||||||
|
drop_and_create_table(&db, builder, "gacha_credits", gacha::gacha_credits::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "gacha_items", gacha::gacha_items::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "gacha_rolls", gacha::gacha_rolls::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "gacha_claims", gacha::gacha_claims::Entity).await?;
|
||||||
|
|
||||||
|
println!("✅ Schema creation completed.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn drop_and_create_table<E>(
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
builder: DbBackend,
|
||||||
|
name: &str,
|
||||||
|
entity: E,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> // Return Result
|
||||||
|
where
|
||||||
|
E: EntityTrait,
|
||||||
|
{
|
||||||
|
let schema = Schema::new(builder);
|
||||||
|
|
||||||
|
// Drop table if it exists
|
||||||
|
let drop_stmt = Table::drop().table(entity).if_exists().cascade().to_owned(); // Added .cascade()
|
||||||
|
db.execute(builder.build(&drop_stmt)).await?; // Propagate error
|
||||||
|
println!(" Dropped table if exists: {}", name);
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
let mut create_stmt = schema.create_table_from_entity(entity);
|
||||||
|
create_stmt.if_not_exists();
|
||||||
|
|
||||||
|
db.execute(builder.build(&create_stmt)).await?; // Propagate error
|
||||||
|
println!(" ✅ Created table: {}", name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::common::events::{ActiveModel as EventsActiveModel, Entity as EventEntity};
|
||||||
|
use sea_orm::{ActiveValue::Set, ActiveModelTrait, EntityTrait, ColumnTrait, QueryFilter};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use chrono::Utc; // Removed NaiveDateTime as it was unused
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
let events = vec![
|
||||||
|
(
|
||||||
|
"Tech Conference 2025",
|
||||||
|
"Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.",
|
||||||
|
"https://techconf2025.example.com",
|
||||||
|
150.0,
|
||||||
|
Some("Jakarta Convention Center".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-06-15T09:00:00Z",
|
||||||
|
"2025-06-17T18:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Online Web Development Workshop",
|
||||||
|
"Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.",
|
||||||
|
"https://webdev-workshop.example.com",
|
||||||
|
75.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2025-07-10T14:00:00Z",
|
||||||
|
"2025-07-10T17:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Startup Pitch Competition",
|
||||||
|
"Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.",
|
||||||
|
"https://startup-pitch.example.com",
|
||||||
|
25.0,
|
||||||
|
Some("Innovation Hub Surabaya".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-08-05T10:00:00Z",
|
||||||
|
"2025-08-05T16:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Digital Marketing Masterclass",
|
||||||
|
"Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.",
|
||||||
|
"https://digital-marketing.example.com",
|
||||||
|
100.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2025-09-20T13:00:00Z",
|
||||||
|
"2025-09-22T15:00:00Z",
|
||||||
|
),
|
||||||
|
// Additional Events
|
||||||
|
(
|
||||||
|
"Rust Programming Bootcamp",
|
||||||
|
"Intensive 3-day bootcamp to master Rust fundamentals and advanced concepts.",
|
||||||
|
"https://rust-bootcamp.example.com",
|
||||||
|
200.0,
|
||||||
|
Some("Bandung Digital Valley".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-10-01T09:00:00Z",
|
||||||
|
"2025-10-03T17:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"AI & Machine Learning Summit",
|
||||||
|
"Global summit discussing the future of AI and its impact on industries.",
|
||||||
|
"https://ai-summit.example.com",
|
||||||
|
300.0,
|
||||||
|
Some("Bali Nusa Dua Convention Center".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-11-15T08:00:00Z",
|
||||||
|
"2025-11-17T18:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Cybersecurity Awareness Webinar",
|
||||||
|
"Free webinar on best practices for personal and corporate cybersecurity.",
|
||||||
|
"https://cybersecurity-webinar.example.com",
|
||||||
|
0.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2025-12-05T14:00:00Z",
|
||||||
|
"2025-12-05T16:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Cloud Computing Workshop",
|
||||||
|
"Hands-on workshop on deploying scalable applications using AWS and Azure.",
|
||||||
|
"https://cloud-workshop.example.com",
|
||||||
|
120.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2026-01-20T10:00:00Z",
|
||||||
|
"2026-01-22T15:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Blockchain for Finance",
|
||||||
|
"Exploring the applications of blockchain technology in the financial sector.",
|
||||||
|
"https://blockchain-finance.example.com",
|
||||||
|
180.0,
|
||||||
|
Some("Jakarta Ritz-Carlton".to_string()),
|
||||||
|
false,
|
||||||
|
"2026-02-10T09:00:00Z",
|
||||||
|
"2026-02-11T17:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Game Development Jam",
|
||||||
|
"48-hour game development marathon for indie developers.",
|
||||||
|
"https://game-jam.example.com",
|
||||||
|
50.0,
|
||||||
|
Some("Yogyakarta Creative Hub".to_string()),
|
||||||
|
false,
|
||||||
|
"2026-03-15T18:00:00Z",
|
||||||
|
"2026-03-17T18:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"UX/UI Design Principles",
|
||||||
|
"Masterclass on creating intuitive and user-friendly interfaces.",
|
||||||
|
"https://uxui-design.example.com",
|
||||||
|
90.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2026-04-05T13:00:00Z",
|
||||||
|
"2026-04-07T16:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Data Science Fundamentals",
|
||||||
|
"Introduction to data analysis, visualization, and statistical modeling.",
|
||||||
|
"https://data-science.example.com",
|
||||||
|
110.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2026-05-12T10:00:00Z",
|
||||||
|
"2026-05-14T15:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"IoT Innovation Expo",
|
||||||
|
"Showcase of the latest Internet of Things devices and solutions.",
|
||||||
|
"https://iot-expo.example.com",
|
||||||
|
50.0,
|
||||||
|
Some("Surabaya Expo Center".to_string()),
|
||||||
|
false,
|
||||||
|
"2026-06-20T09:00:00Z",
|
||||||
|
"2026-06-22T18:00:00Z",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
detail_link,
|
||||||
|
price,
|
||||||
|
location,
|
||||||
|
is_online,
|
||||||
|
start_date_str, // Renamed to avoid conflict
|
||||||
|
end_date_str, // Renamed to avoid conflict
|
||||||
|
) in events
|
||||||
|
{
|
||||||
|
// Check if event already exists by name
|
||||||
|
let existing = EventEntity::find().filter(<EventEntity as EntityTrait>::Column::Name.eq(name)).one(db).await?;
|
||||||
|
if existing.is_some() {
|
||||||
|
println!("ℹ️ Skipping (already exists): {name}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uuid = Uuid::new_v4(); // Generate a Uuid
|
||||||
|
let mut event_model: EventsActiveModel = Default::default();
|
||||||
|
event_model.id = Set(uuid);
|
||||||
|
event_model.name = Set(name.to_string());
|
||||||
|
event_model.description = Set(description.to_string());
|
||||||
|
event_model.detail_link = Set(detail_link.to_string());
|
||||||
|
event_model.price = Set(price);
|
||||||
|
event_model.is_online = Set(is_online);
|
||||||
|
event_model.location = Set(location.clone());
|
||||||
|
event_model.start_date = Set(chrono::DateTime::parse_from_rfc3339(start_date_str)?.with_timezone(&chrono::Utc));
|
||||||
|
event_model.end_date = Set(chrono::DateTime::parse_from_rfc3339(end_date_str)?.with_timezone(&chrono::Utc));
|
||||||
|
event_model.is_deleted = Set(false); // Explicitly set is_deleted
|
||||||
|
event_model.created_at = Set(Utc::now()); // Explicitly set created_at
|
||||||
|
event_model.updated_at = Set(Utc::now()); // Explicitly set updated_at
|
||||||
|
|
||||||
|
|
||||||
|
event_model.insert(db).await?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"✅ Inserted event: {} ({})",
|
||||||
|
name,
|
||||||
|
if is_online { "Online" } else { "In-person" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ All Events seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::gacha::gacha_items::ActiveModel as GachaItemActiveModel;
|
||||||
|
use imphnen_entities::seaorm::gacha::gacha_rolls::ActiveModel as GachaRollActiveModel;
|
||||||
|
use sea_orm::ActiveModelTrait;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use sea_orm::ConnectionTrait;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
// Check if gacha item already exists
|
||||||
|
let check_item_sql = "SELECT id FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1' LIMIT 1";
|
||||||
|
let item_result = pg_conn.query_one(sea_orm::Statement::from_string(db.get_database_backend(), check_item_sql)).await?;
|
||||||
|
let gacha_item_uuid = if let Some(ref row) = item_result {
|
||||||
|
// Item exists, get its ID
|
||||||
|
row.try_get("", "id")?
|
||||||
|
} else {
|
||||||
|
// Item doesn't exist, create it
|
||||||
|
// Note: We can't easily delete by a fixed ID since it's a UUID, but the insert will fail if there's a conflict
|
||||||
|
let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1'".to_string())).await.ok();
|
||||||
|
|
||||||
|
// Create gacha item via SeaORM
|
||||||
|
let new_uuid = Uuid::new_v4();
|
||||||
|
let mut item_model: GachaItemActiveModel = Default::default();
|
||||||
|
item_model.id = Set(new_uuid);
|
||||||
|
item_model.item_code = Set("ITEM_TEST_1".to_string());
|
||||||
|
item_model.name = Set("Test Gacha Item".to_string());
|
||||||
|
item_model.description = Set("Test item for gacha".to_string());
|
||||||
|
item_model.rarity = Set("common".to_string());
|
||||||
|
item_model.type_ = Set("item".to_string());
|
||||||
|
item_model.category = Set("test".to_string());
|
||||||
|
item_model.value = Set(1);
|
||||||
|
item_model.weight = Set(1.0);
|
||||||
|
item_model.stock = Set(10);
|
||||||
|
item_model.is_limited = Set(false);
|
||||||
|
item_model.created_at = Set(chrono::Utc::now());
|
||||||
|
item_model.updated_at = Set(chrono::Utc::now());
|
||||||
|
item_model.insert(db).await?;
|
||||||
|
println!("Gacha Item seeded successfully!");
|
||||||
|
new_uuid
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always try to insert the roll, relying on the database constraints to prevent duplicates if needed
|
||||||
|
let gacha_roll_id = Uuid::new_v4();
|
||||||
|
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||||
|
roll_model.id = Set(gacha_roll_id);
|
||||||
|
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
|
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||||
|
roll_model.item_id = Set(gacha_item_uuid);
|
||||||
|
roll_model.weight = Set(1.0);
|
||||||
|
roll_model.quantity = Set(10);
|
||||||
|
roll_model.is_deleted = Set(false);
|
||||||
|
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.insert(db).await?;
|
||||||
|
println!("Gacha Roll seeded successfully!");
|
||||||
|
let gacha_roll_id = Uuid::new_v4();
|
||||||
|
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||||
|
roll_model.id = Set(gacha_roll_id);
|
||||||
|
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
|
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||||
|
roll_model.item_id = Set(gacha_item_uuid);
|
||||||
|
roll_model.weight = Set(1.0);
|
||||||
|
roll_model.quantity = Set(10);
|
||||||
|
roll_model.is_deleted = Set(false);
|
||||||
|
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.insert(db).await?;
|
||||||
|
println!("✅ Gacha items and rolls seeded.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_libs::hash_password;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::error::Error;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::roles::{Entity as RoleEntity, Column as RoleColumn};
|
||||||
|
use sea_orm::{ActiveModelTrait, ConnectionTrait, ActiveValue::Set, EntityTrait, QueryFilter, ColumnTrait};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_mentors WHERE id = 'e6f78d23-83bf-5c2b-bcd4-001345678901'".to_string())).await.ok();
|
||||||
|
let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_users WHERE email = 'mentor@example.com'".to_string())).await.ok();
|
||||||
|
|
||||||
|
// Find Mentor role
|
||||||
|
let role = RoleEntity::find()
|
||||||
|
.filter(RoleColumn::Name.eq("Mentor"))
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
.ok_or("Role 'Mentor' not found")?;
|
||||||
|
|
||||||
|
// Insert user with Mentor role
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
|
let mut user_model: UsersActiveModel = Default::default();
|
||||||
|
user_model.id = Set(user_id);
|
||||||
|
user_model.email = Set("mentor@example.com".to_string());
|
||||||
|
user_model.password_hash = Set(hash_password("password").unwrap());
|
||||||
|
user_model.username = Set("mentor@example.com".to_string());
|
||||||
|
user_model.first_name = Set(Some("Mentor".to_string()));
|
||||||
|
user_model.last_name = Set(Some("User".to_string()));
|
||||||
|
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
||||||
|
user_model.is_active = Set(true);
|
||||||
|
user_model.is_verified = Set(true);
|
||||||
|
user_model.role_id = Set(Some(role.id));
|
||||||
|
user_model.created_at = Set(chrono::Utc::now());
|
||||||
|
user_model.updated_at = Set(chrono::Utc::now());
|
||||||
|
user_model.insert(db).await?;
|
||||||
|
|
||||||
|
// Insert mentor
|
||||||
|
let mentor_id = Uuid::new_v4();
|
||||||
|
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||||
|
mentor_model.id = Set(mentor_id);
|
||||||
|
mentor_model.user_id = Set(user_id);
|
||||||
|
mentor_model.industries = Set(Some(json!( ["Software", "Education"] )));
|
||||||
|
mentor_model.expertise = Set(Some(json!( ["Rust", "Microservices"] )));
|
||||||
|
mentor_model.languages = Set(Some(json!( ["Indonesian", "English"] )));
|
||||||
|
mentor_model.current_company = Set(Some("PT Contoh".to_string()));
|
||||||
|
mentor_model.current_role = Set(Some("Senior Backend Engineer".to_string()));
|
||||||
|
mentor_model.years_of_experience = Set(Some(5));
|
||||||
|
mentor_model.topics_of_interest = Set(Some(json!( ["Rust Programming", "Backend Development"] )));
|
||||||
|
mentor_model.preferred_mentee_level = Set(Some("beginner".to_string()));
|
||||||
|
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["online", "offline"] )));
|
||||||
|
mentor_model.availability_commitment = Set(Some("2 jam per minggu untuk mentoring online dan offline".to_string()));
|
||||||
|
mentor_model.mentoring_rate = Set(Some(100000.0));
|
||||||
|
mentor_model.status = Set(Some("verified".to_string()));
|
||||||
|
mentor_model.is_deleted = Set(false);
|
||||||
|
mentor_model.created_at = Set(chrono::Utc::now());
|
||||||
|
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||||
|
mentor_model.insert(db).await?;
|
||||||
|
println!("Mentor created successfully!");
|
||||||
|
|
||||||
|
println!("✅ Inserted mentor user: mentor@example.com");
|
||||||
|
println!("✅ Mentor user seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_iam::PermissionsEnum;
|
||||||
|
use std::error::Error;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::auth::permissions::ActiveModel as PermissionActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::permissions::Entity as PermissionEntity;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::{ActiveModelTrait};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
for permission in [
|
||||||
|
PermissionsEnum::ReadListUsers,
|
||||||
|
PermissionsEnum::ReadDetailUsers,
|
||||||
|
PermissionsEnum::CreateUsers,
|
||||||
|
PermissionsEnum::DeleteUsers,
|
||||||
|
PermissionsEnum::UpdateUsers,
|
||||||
|
PermissionsEnum::ActivateUsers,
|
||||||
|
PermissionsEnum::ReadListRoles,
|
||||||
|
PermissionsEnum::ReadDetailRoles,
|
||||||
|
PermissionsEnum::CreateRoles,
|
||||||
|
PermissionsEnum::DeleteRoles,
|
||||||
|
PermissionsEnum::UpdateRoles,
|
||||||
|
PermissionsEnum::ReadListPermissions,
|
||||||
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
|
PermissionsEnum::CreatePermissions,
|
||||||
|
PermissionsEnum::DeletePermissions,
|
||||||
|
PermissionsEnum::UpdatePermissions,
|
||||||
|
PermissionsEnum::CreateGachaClaims,
|
||||||
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
|
PermissionsEnum::ReadListGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::CreateGachaItems,
|
||||||
|
PermissionsEnum::DeleteGachaItems,
|
||||||
|
PermissionsEnum::UpdateGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
PermissionsEnum::CreateGachaRolls,
|
||||||
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
PermissionsEnum::ReadListMentors,
|
||||||
|
PermissionsEnum::ReadDetailMentors,
|
||||||
|
PermissionsEnum::RegisterMentors,
|
||||||
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
|
PermissionsEnum::UpdateMentors,
|
||||||
|
PermissionsEnum::VerifyMentors,
|
||||||
|
PermissionsEnum::DeleteMentors,
|
||||||
|
PermissionsEnum::Administrator,
|
||||||
|
] {
|
||||||
|
// permission.id() returns a string, try parse to uuid
|
||||||
|
let parsed_id = Uuid::parse_str(&permission.id()).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
|
|
||||||
|
// Check if permission already exists
|
||||||
|
let existing = PermissionEntity::find_by_id(parsed_id).one(db).await?;
|
||||||
|
if existing.is_some() {
|
||||||
|
println!("ℹ️ Skipping (already exists): {permission}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert permission using active model
|
||||||
|
let mut perm_model: PermissionActiveModel = Default::default();
|
||||||
|
perm_model.id = Set(parsed_id);
|
||||||
|
perm_model.name = Set(permission.to_string());
|
||||||
|
perm_model.is_deleted = Set(false);
|
||||||
|
perm_model.created_at = Set(Utc::now());
|
||||||
|
perm_model.updated_at = Set(Utc::now());
|
||||||
|
perm_model.insert(db).await?;
|
||||||
|
println!("✅ Inserted: {permission}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ All Permissions seeded");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::auth::roles::{RoleBuilder, Entity as RoleEntity};
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use chrono::Utc; // Added chrono
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
let roles = vec![
|
||||||
|
(
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
|
"Staf",
|
||||||
|
Some("2025-02-24T16:52:27.630453+00"),
|
||||||
|
Some("2025-02-24T16:52:27.630461+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
"User",
|
||||||
|
Some("2025-02-28T14:53:58.576688+00"),
|
||||||
|
Some("2025-02-28T14:53:58.576688+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"60f1aeb7-dad2-4e06-bcb5-be1ba510c906",
|
||||||
|
"Staff Aktivasi User",
|
||||||
|
Some("2025-02-20T02:47:09.660640+00"),
|
||||||
|
Some("2025-02-20T02:48:30.083283+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0",
|
||||||
|
"Admin Pembayaran",
|
||||||
|
Some("2025-01-29T05:39:28.562667+00"),
|
||||||
|
Some("2025-03-12T22:56:29.597416+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
|
"Admin",
|
||||||
|
Some("2025-02-22T15:38:39.868306+00"),
|
||||||
|
Some("2025-02-22T15:38:39.868306+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
|
"Mentor",
|
||||||
|
Some("2025-07-06T10:00:00.000000+00"),
|
||||||
|
Some("2025-07-06T10:00:00.000000+00"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (id, name, _created_at_str, _updated_at_str) in roles { // Renamed to avoid conflict
|
||||||
|
let uuid = Uuid::parse_str(id).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
|
|
||||||
|
// Check if role already exists
|
||||||
|
let existing = RoleEntity::find_by_id(uuid).one(db).await?;
|
||||||
|
if existing.is_some() {
|
||||||
|
println!("ℹ️ Skipping (already exists): {name}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing by id to avoid duplicates (original logic, replaced by existence check)
|
||||||
|
// let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), format!("DELETE FROM app_roles WHERE id = '{}'", uuid))).await.ok();
|
||||||
|
|
||||||
|
let role_model = RoleBuilder::new()
|
||||||
|
.name(name.to_string())
|
||||||
|
.description("System generated role".to_string())
|
||||||
|
.permissions(vec![])
|
||||||
|
.is_default(false)
|
||||||
|
.build()?;
|
||||||
|
let mut role_model = role_model;
|
||||||
|
role_model.id = Set(uuid);
|
||||||
|
role_model.is_system_role = Set(true); // Set the missing field
|
||||||
|
role_model.created_at = Set(Utc::now()); // Set created_at
|
||||||
|
role_model.updated_at = Set(Utc::now()); // Set updated_at
|
||||||
|
|
||||||
|
role_model.insert(db).await?;
|
||||||
|
println!("✅ Inserted role: {name}");
|
||||||
|
}
|
||||||
|
println!("✅ All Roles seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
use imphnen_iam::PermissionsEnum;
|
||||||
|
use std::error::Error;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::auth::roles::Entity as RolesEntity;
|
||||||
|
use imphnen_entities::seaorm::auth::roles::ActiveModel as RoleActiveModel;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
use sea_orm::ActiveModelTrait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
// Ensure indexes are present if needed (placeholders) - we don't modify schema here
|
||||||
|
|
||||||
|
println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
|
||||||
|
|
||||||
|
let roles_permissions = vec![
|
||||||
|
(
|
||||||
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
|
vec![
|
||||||
|
// Only Administrator permission - grants access to everything
|
||||||
|
PermissionsEnum::Administrator,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
|
vec![
|
||||||
|
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
|
||||||
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
|
PermissionsEnum::ReadListMentors,
|
||||||
|
PermissionsEnum::ReadDetailMentors,
|
||||||
|
PermissionsEnum::ReadListGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
PermissionsEnum::CreateGachaRolls,
|
||||||
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
vec![
|
||||||
|
PermissionsEnum::ReadListGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::ReadListUsers,
|
||||||
|
PermissionsEnum::ReadDetailUsers,
|
||||||
|
PermissionsEnum::CreateGachaClaims,
|
||||||
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
PermissionsEnum::CreateGachaRolls,
|
||||||
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
PermissionsEnum::RegisterMentors,
|
||||||
|
PermissionsEnum::ReadListMentors,
|
||||||
|
PermissionsEnum::ReadDetailMentors,
|
||||||
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
|
vec![
|
||||||
|
// Staff should be able to list roles and permissions in tests
|
||||||
|
PermissionsEnum::ReadListRoles,
|
||||||
|
PermissionsEnum::ReadListPermissions,
|
||||||
|
PermissionsEnum::ReadListUsers,
|
||||||
|
PermissionsEnum::ReadListMentors,
|
||||||
|
PermissionsEnum::ReadDetailUsers,
|
||||||
|
PermissionsEnum::ActivateUsers,
|
||||||
|
PermissionsEnum::ReadDetailRoles,
|
||||||
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
|
PermissionsEnum::ReadListGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::ReadListMentors,
|
||||||
|
PermissionsEnum::ReadDetailMentors,
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
PermissionsEnum::CreateGachaRolls,
|
||||||
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"60f1aeb7-dad2-4e06-bcb5-be1ba510c906",
|
||||||
|
vec![PermissionsEnum::ActivateUsers],
|
||||||
|
),
|
||||||
|
("6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0", vec![]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (role_id, permissions) in roles_permissions {
|
||||||
|
let role_uuid = Uuid::parse_str(role_id).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
|
// Map permissions enum to JSON array of permission ids
|
||||||
|
let json_permissions = JsonValue::Array(
|
||||||
|
permissions.iter().map(|p| JsonValue::String(p.id())).collect()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find role and update permissions
|
||||||
|
if let Some(role_model) = RolesEntity::find_by_id(role_uuid).one(db).await? {
|
||||||
|
let mut am: RoleActiveModel = role_model.into();
|
||||||
|
am.permissions = Set(Some(json_permissions));
|
||||||
|
am.update(db).await?;
|
||||||
|
println!("✅ Permissions updated for role: {role_id}");
|
||||||
|
} else {
|
||||||
|
println!("⚠️ Role with id {role_id} not found, skipping permissions update");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ All roles permissions updated!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::common::events::ActiveModel as EventsActiveModel;
|
||||||
|
use imphnen_entities::seaorm::common::testimonials::ActiveModel as TestimonialsActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::ActiveModelTrait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use serde_json::json;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
// Seed Events - handle existing data
|
||||||
|
let uuid = Uuid::new_v4().to_string();
|
||||||
|
let mut event_model: EventsActiveModel = Default::default();
|
||||||
|
event_model.id = Set(Uuid::parse_str(&uuid)?);
|
||||||
|
event_model.name = Set("Test Event".to_string());
|
||||||
|
event_model.description = Set("Test event description".to_string());
|
||||||
|
event_model.detail_link = Set("https://example.com/event".to_string());
|
||||||
|
event_model.price = Set(50.0);
|
||||||
|
event_model.is_online = Set(true);
|
||||||
|
event_model.start_date = Set(Utc::now());
|
||||||
|
event_model.end_date = Set(Utc::now() + chrono::Duration::days(1));
|
||||||
|
event_model.location = Set(None);
|
||||||
|
event_model.is_deleted = Set(false);
|
||||||
|
match event_model.insert(db).await {
|
||||||
|
Ok(_) => println!("✅ Inserted test event"),
|
||||||
|
Err(_) => println!("⚠️ Test event already exists or could not be inserted, skipping"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Seed Testimonials - handle existing data
|
||||||
|
let mut testimonial_model: TestimonialsActiveModel = Default::default();
|
||||||
|
testimonial_model.id = Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
||||||
|
testimonial_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
|
testimonial_model.role = Set("Student".to_string());
|
||||||
|
testimonial_model.content = Set("This is a great platform!".to_string());
|
||||||
|
testimonial_model.is_deleted = Set(false);
|
||||||
|
match testimonial_model.insert(db).await {
|
||||||
|
Ok(_) => println!("✅ Inserted test testimonial"),
|
||||||
|
Err(_) => println!("⚠️ Test testimonial already exists or could not be inserted, skipping"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Seed Mentor - handle existing data
|
||||||
|
let mentor_id = Uuid::new_v4();
|
||||||
|
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||||
|
mentor_model.id = Set(mentor_id);
|
||||||
|
// Use the admin user ID instead of a random one
|
||||||
|
mentor_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
|
mentor_model.industries = Set(Some(json!( ["Technology", "Education"] )));
|
||||||
|
mentor_model.expertise = Set(Some(json!( ["Software Development"] )));
|
||||||
|
mentor_model.languages = Set(Some(json!( ["English", "Indonesian"] )));
|
||||||
|
mentor_model.current_company = Set(Some("Tech Corp".to_string()));
|
||||||
|
mentor_model.current_role = Set(Some("Senior Engineer".to_string()));
|
||||||
|
mentor_model.years_of_experience = Set(Some(5));
|
||||||
|
mentor_model.topics_of_interest = Set(Some(json!( ["Rust", "Web Development"] )));
|
||||||
|
mentor_model.preferred_mentee_level = Set(Some("Beginner".to_string()));
|
||||||
|
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["1:1", "Group"] )));
|
||||||
|
mentor_model.availability_commitment = Set(Some("Weekly".to_string()));
|
||||||
|
mentor_model.mentoring_rate = Set(Some(100.0));
|
||||||
|
mentor_model.status = Set(Some("active".to_string()));
|
||||||
|
mentor_model.is_deleted = Set(false);
|
||||||
|
mentor_model.created_at = Set(chrono::Utc::now());
|
||||||
|
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||||
|
// Create mentor record via SeaORM active model
|
||||||
|
mentor_model.insert(db).await?;
|
||||||
|
println!("✅ Inserted test mentor via SeaORM");
|
||||||
|
|
||||||
|
println!("✅ All test data seeded successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_libs::hash_password;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use imphnen_entities::seaorm::auth::users::Entity as UserEntity; // Added for dynamic role lookup
|
||||||
|
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||||
|
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, IntoActiveModel};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use std::error::Error;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
let users = vec![
|
||||||
|
(
|
||||||
|
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2",
|
||||||
|
"admin@example.com",
|
||||||
|
"Admin",
|
||||||
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a4d23fb5-9e31-423c-9842-fbd6e75a5298",
|
||||||
|
"staff@example.com",
|
||||||
|
"Staff",
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"d5e89c12-72af-4b1a-abc3-ff1234567890",
|
||||||
|
"user@example.com",
|
||||||
|
"User",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"665a3cfc-ea5f-4bcd-8769-4a6d8d1451d4",
|
||||||
|
"testuser1@example.com",
|
||||||
|
"Test User 1",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059", // Fixed UUID
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"3972c139-a450-416c-93b0-c42539dc780f",
|
||||||
|
"testuser2@example.com",
|
||||||
|
"Test User 2",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"b426c0a9-0efb-4e26-b078-4f18767255f3",
|
||||||
|
"testuser3@example.com",
|
||||||
|
"Test User 3",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059", // Fixed UUID
|
||||||
|
),
|
||||||
|
// Additional Users for Volume and Variety
|
||||||
|
(
|
||||||
|
"11111111-1111-1111-1111-111111111111",
|
||||||
|
"user4@example.com",
|
||||||
|
"User Four",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"22222222-2222-2222-2222-222222222222",
|
||||||
|
"user5@example.com",
|
||||||
|
"User Five",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"33333333-3333-3333-3333-333333333333",
|
||||||
|
"mentor2@example.com",
|
||||||
|
"Mentor Two",
|
||||||
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a", // Mentor Role
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"44444444-4444-4444-4444-444444444444",
|
||||||
|
"staff2@example.com",
|
||||||
|
"Staff Two",
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86", // Staff Role
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"55555555-5555-5555-5555-555555555555",
|
||||||
|
"user6@example.com",
|
||||||
|
"User Six",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"66666666-6666-6666-6666-666666666666",
|
||||||
|
"user7@example.com",
|
||||||
|
"User Seven",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"77777777-7777-7777-7777-777777777777",
|
||||||
|
"user8@example.com",
|
||||||
|
"User Eight",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"88888888-8888-8888-8888-888888888888",
|
||||||
|
"user9@example.com",
|
||||||
|
"User Nine",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
"user10@example.com",
|
||||||
|
"User Ten",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (id, email, fullname, role_id_str) in users { // role_id_str directly contains UUID
|
||||||
|
let role_uuid = Some(Uuid::parse_str(role_id_str)
|
||||||
|
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?);
|
||||||
|
|
||||||
|
// Build SeaORM ActiveModel for users
|
||||||
|
let uid = Uuid::parse_str(id)?; // Should always be valid UUID strings from test data
|
||||||
|
|
||||||
|
let names: Vec<&str> = fullname.split_whitespace().collect();
|
||||||
|
let first_name = names.first().map(|s| s.to_string());
|
||||||
|
let last_name = if names.len() > 1 { Some(names[1..].join(" ")) } else { None };
|
||||||
|
|
||||||
|
let password = "password";
|
||||||
|
let hashed = hash_password(password).unwrap();
|
||||||
|
|
||||||
|
// Explicit Upsert Logic
|
||||||
|
let existing_user = UserEntity::find_by_id(uid).one(db).await?;
|
||||||
|
let is_update = existing_user.is_some();
|
||||||
|
|
||||||
|
let mut user_model: UsersActiveModel = if let Some(existing) = existing_user {
|
||||||
|
println!("🔄 Updating user: {fullname} ({email})");
|
||||||
|
existing.into_active_model()
|
||||||
|
} else {
|
||||||
|
println!("✅ Inserting user: {fullname} ({email})");
|
||||||
|
let mut active: UsersActiveModel = Default::default();
|
||||||
|
active.id = Set(uid);
|
||||||
|
active.created_at = Set(Utc::now());
|
||||||
|
active
|
||||||
|
};
|
||||||
|
|
||||||
|
user_model.email = Set(email.to_string());
|
||||||
|
user_model.password_hash = Set(hashed);
|
||||||
|
user_model.username = Set(email.to_string());
|
||||||
|
user_model.first_name = Set(first_name);
|
||||||
|
user_model.last_name = Set(last_name);
|
||||||
|
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
||||||
|
user_model.is_verified = Set(true);
|
||||||
|
user_model.is_active = Set(true);
|
||||||
|
user_model.role_id = Set(role_uuid);
|
||||||
|
user_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
|
if is_update {
|
||||||
|
user_model.update(db).await?;
|
||||||
|
} else {
|
||||||
|
user_model.insert(db).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("✅ All Users seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
println!("🔧 Seeding: {bin}");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let status = Command::new(format!("./target/release/{}.exe", bin)).status()?;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let status = Command::new(format!("./target/release/{}", bin)).status()?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
Err(format!("❌ Failed to run seed: {bin}").into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
println!("🚀 Running all seeders...\n");
|
||||||
|
|
||||||
|
run_seed("seed_permissions")?;
|
||||||
|
run_seed("seed_roles")?;
|
||||||
|
run_seed("seed_roles_permissions")?;
|
||||||
|
run_seed("seed_users")?;
|
||||||
|
run_seed("seed_events")?;
|
||||||
|
run_seed("seed_gacha_rolls")?;
|
||||||
|
run_seed("seed_mentor_user")?;
|
||||||
|
run_seed("seed_test_data")?;
|
||||||
|
println!("\n✅ All seeding completed successfully.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
//! PostgreSQL Connection Test Program
|
||||||
|
//! This program tests the PostgreSQL integration with SeaORM
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection, PostgresError};
|
||||||
|
use imphnen_entities::seaorm::auth::users::{Entity as UsersEntity, Model as UserModel};
|
||||||
|
use imphnen_entities::seaorm::auth::roles::{Entity as RolesEntity, Model as RoleModel};
|
||||||
|
use sea_orm::{EntityTrait, ActiveModelTrait, Set, TransactionTrait, DbErr, PaginatorTrait};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🚀 Starting PostgreSQL Connection Test");
|
||||||
|
println!("=====================================");
|
||||||
|
|
||||||
|
// Load configuration from environment
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
println!("✅ Configuration loaded successfully");
|
||||||
|
println!(" Database URL: {}", config.database_url.replace("postgres://", "postgres://****:****@"));
|
||||||
|
println!(" Pool size: {}", config.pool_size);
|
||||||
|
println!(" Connect timeout: {}s", config.connect_timeout);
|
||||||
|
println!(" Retry attempts: {}", config.retry_attempts);
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
println!("\n🔌 Testing PostgreSQL connection...");
|
||||||
|
match test_connection(config).await {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("✅ All PostgreSQL tests passed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("❌ PostgreSQL test failed: {}", e);
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_connection(config: PostgresConfig) -> Result<(), PostgresError> {
|
||||||
|
// Create connection
|
||||||
|
println!(" Creating PostgreSQL connection...");
|
||||||
|
let postgres_conn = PostgresConnection::new(config).await?;
|
||||||
|
let connection = Arc::new(postgres_conn);
|
||||||
|
println!(" ✅ Connection established successfully");
|
||||||
|
|
||||||
|
// Test basic connectivity
|
||||||
|
println!(" Testing basic connectivity...");
|
||||||
|
test_basic_connectivity(&connection).await?;
|
||||||
|
println!(" ✅ Basic connectivity test passed");
|
||||||
|
|
||||||
|
// Test table existence
|
||||||
|
println!(" Testing table existence...");
|
||||||
|
test_table_existence(&connection).await?;
|
||||||
|
println!(" ✅ Table existence test passed");
|
||||||
|
|
||||||
|
// Test CRUD operations
|
||||||
|
println!(" Testing CRUD operations...");
|
||||||
|
test_crud_operations(&connection).await?;
|
||||||
|
println!(" ✅ CRUD operations test passed");
|
||||||
|
|
||||||
|
// Test transaction support
|
||||||
|
println!(" Testing transaction support...");
|
||||||
|
test_transactions(&connection).await?;
|
||||||
|
println!(" ✅ Transaction support test passed");
|
||||||
|
|
||||||
|
// Test error handling
|
||||||
|
println!(" Testing error handling...");
|
||||||
|
test_error_handling(&connection).await?;
|
||||||
|
println!(" ✅ Error handling test passed");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_basic_connectivity(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||||
|
// Execute a simple query
|
||||||
|
let statement = sea_orm::Statement::from_string(
|
||||||
|
connection.get_database_backend(),
|
||||||
|
"SELECT 1 as test_value, current_timestamp as current_time".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = connection.query_one(statement).await?
|
||||||
|
.ok_or_else(|| PostgresError::ConnectionError(sea_orm::DbErr::Custom("No results returned".to_string())))?;
|
||||||
|
|
||||||
|
// Verify we got expected results
|
||||||
|
let test_value: Option<i32> = result.try_get("", "test_value").ok();
|
||||||
|
let current_time: Option<String> = result.try_get("", "current_time").ok();
|
||||||
|
|
||||||
|
if test_value != Some(1) {
|
||||||
|
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
|
format!("Expected test_value=1, got {:?}", test_value)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_time.is_none() {
|
||||||
|
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
|
"Expected current_time to be set".to_string()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" 📝 Query result: test_value={:?}, current_time={:?}", test_value, current_time);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_table_existence(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||||
|
// Test if our tables exist
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
|
||||||
|
println!(" 📋 Checking users table...");
|
||||||
|
let user_count = UsersEntity::find()
|
||||||
|
.count(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
println!(" 📊 Users table accessible, current count: {}", user_count);
|
||||||
|
|
||||||
|
println!(" 📋 Checking roles table...");
|
||||||
|
let role_count = RolesEntity::find()
|
||||||
|
.count(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
println!(" 📊 Roles table accessible, current count: {}", role_count);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_crud_operations(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||||
|
use sea_orm::{ActiveModelTrait, Set};
|
||||||
|
|
||||||
|
// Create test user
|
||||||
|
println!(" ➕ Creating test user...");
|
||||||
|
let test_user_id = Uuid::new_v4();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||||
|
id: Set(test_user_id),
|
||||||
|
email: Set(format!("test_user_{}@example.com", test_user_id)),
|
||||||
|
password_hash: Set("test_password_hash".to_string()),
|
||||||
|
username: Set(format!("testuser_{}", test_user_id)),
|
||||||
|
first_name: Set(Some("Test".to_string())),
|
||||||
|
last_name: Set(Some("User".to_string())),
|
||||||
|
avatar_url: Set(None),
|
||||||
|
is_verified: Set(false),
|
||||||
|
is_active: Set(true),
|
||||||
|
metadata: Set(None),
|
||||||
|
role_id: Set(None),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
|
deleted_at: Set(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let created_user = user_model.insert(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
|
||||||
|
println!(" ✅ Created user with ID: {}", created_user.id);
|
||||||
|
|
||||||
|
// Read user
|
||||||
|
println!(" 🔍 Reading test user...");
|
||||||
|
let found_user = UsersEntity::find_by_id(test_user_id)
|
||||||
|
.one(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?
|
||||||
|
.ok_or_else(|| PostgresError::ConnectionError(sea_orm::DbErr::Custom("User not found after creation".to_string())))?;
|
||||||
|
|
||||||
|
println!(" ✅ Found user: {} ({})", found_user.username, found_user.email);
|
||||||
|
|
||||||
|
// Update user
|
||||||
|
println!(" ✏️ Updating test user...");
|
||||||
|
let mut update_model: imphnen_entities::seaorm::auth::users::ActiveModel = found_user.into();
|
||||||
|
update_model.first_name = Set(Some("Updated".to_string()));
|
||||||
|
update_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
|
let updated_user = update_model.update(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
|
||||||
|
println!(" ✅ Updated user first name to: {:?}", updated_user.first_name);
|
||||||
|
|
||||||
|
// Delete user
|
||||||
|
println!(" 🗑️ Deleting test user...");
|
||||||
|
UsersEntity::delete_by_id(updated_user.id)
|
||||||
|
.exec(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
|
||||||
|
println!(" ✅ Test user deleted successfully");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_transactions(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||||
|
println!(" 💰 Testing transaction support...");
|
||||||
|
|
||||||
|
// Test transaction with rollback
|
||||||
|
let transaction_result = connection.conn.transaction(|txn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Create a test user within transaction
|
||||||
|
let test_user_id = Uuid::new_v4();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||||
|
id: Set(test_user_id),
|
||||||
|
email: Set(format!("transaction_test_{}@example.com", test_user_id)),
|
||||||
|
password_hash: Set("transaction_password_hash".to_string()),
|
||||||
|
username: Set(format!("transaction_user_{}", test_user_id)),
|
||||||
|
first_name: Set(Some("Transaction".to_string())),
|
||||||
|
last_name: Set(Some("Test".to_string())),
|
||||||
|
avatar_url: Set(None),
|
||||||
|
is_verified: Set(false),
|
||||||
|
is_active: Set(true),
|
||||||
|
metadata: Set(None),
|
||||||
|
role_id: Set(None),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
|
deleted_at: Set(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _created_user = user_model.insert(txn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Simulate an error to trigger rollback (return a sea_orm::DbErr so the TransactionError matches)
|
||||||
|
Err::<(), DbErr>(DbErr::Custom("Simulated transaction failure".to_string()))
|
||||||
|
})
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
// Transaction should fail and rollback
|
||||||
|
match transaction_result {
|
||||||
|
Err(e) => {
|
||||||
|
let e_text = format!("{:?}", e);
|
||||||
|
if e_text.contains("Simulated transaction failure") {
|
||||||
|
println!(" ✅ Transaction failed as expected, rollback successful");
|
||||||
|
} else {
|
||||||
|
return Err(PostgresError::OperationFailed(format!("Unexpected transaction result: {}", e_text)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
return Err(PostgresError::OperationFailed("Unexpected transaction result: transaction unexpectedly succeeded".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user was not created (due to rollback)
|
||||||
|
let user_exists = UsersEntity::find_by_id(Uuid::nil()) // Use nil UUID as we don't know the actual ID
|
||||||
|
.one(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
if user_exists {
|
||||||
|
println!(" ⚠️ User found despite rollback - this might indicate an issue");
|
||||||
|
} else {
|
||||||
|
println!(" ✅ Transaction rollback verified - user not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_error_handling(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||||
|
println!(" ⚠️ Testing error handling...");
|
||||||
|
|
||||||
|
// Test invalid UUID
|
||||||
|
println!(" 🔍 Testing invalid UUID handling...");
|
||||||
|
let invalid_uuid = Uuid::nil(); // This should exist or be handled gracefully
|
||||||
|
|
||||||
|
match UsersEntity::find_by_id(invalid_uuid)
|
||||||
|
.one(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?
|
||||||
|
{
|
||||||
|
Some(_) => println!(" ✅ Found user with nil UUID (expected in some cases)"),
|
||||||
|
None => println!(" ✅ No user found with nil UUID (expected)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test invalid query
|
||||||
|
println!(" 🔍 Testing invalid query handling...");
|
||||||
|
let invalid_statement = sea_orm::Statement::from_string(
|
||||||
|
connection.get_database_backend(),
|
||||||
|
"SELECT * FROM non_existent_table".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
match connection.execute(invalid_statement).await {
|
||||||
|
Err(_) => println!(" ✅ Invalid query properly handled with error"),
|
||||||
|
Ok(_) => println!(" ⚠️ Invalid query unexpectedly succeeded"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Additional utility functions for comprehensive testing
|
||||||
|
pub mod test_utils {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Create a test PostgreSQL configuration
|
||||||
|
pub fn create_test_config() -> PostgresConfig {
|
||||||
|
PostgresConfig {
|
||||||
|
database_url: "postgres://postgres:postgres@localhost:5432/imphnen_test".to_string(),
|
||||||
|
pool_size: 5,
|
||||||
|
connect_timeout: 10,
|
||||||
|
idle_timeout: 30,
|
||||||
|
max_lifetime: Some(600),
|
||||||
|
retry_attempts: 2,
|
||||||
|
retry_delay: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a test user model
|
||||||
|
pub fn create_test_user_model() -> UserModel {
|
||||||
|
UserModel {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
||||||
|
password_hash: "test_password_hash".to_string(),
|
||||||
|
username: format!("testuser_{}", Uuid::new_v4()),
|
||||||
|
first_name: Some("Test".to_string()),
|
||||||
|
last_name: Some("User".to_string()),
|
||||||
|
avatar_url: None,
|
||||||
|
is_verified: false,
|
||||||
|
is_active: true,
|
||||||
|
metadata: None,
|
||||||
|
role_id: None,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
deleted_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a test role model
|
||||||
|
pub fn create_test_role_model() -> RoleModel {
|
||||||
|
RoleModel {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: format!("test_role_{}", Uuid::new_v4()),
|
||||||
|
description: "Test role description".to_string(),
|
||||||
|
permissions: Some(serde_json::json!(["test.permission"])),
|
||||||
|
is_system_role: false,
|
||||||
|
is_default: false,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
deleted_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_test_config() {
|
||||||
|
let config = test_utils::create_test_config();
|
||||||
|
assert_eq!(config.pool_size, 5);
|
||||||
|
assert_eq!(config.connect_timeout, 10);
|
||||||
|
assert!(config.database_url.contains("imphnen_test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_test_user_model() {
|
||||||
|
let user = test_utils::create_test_user_model();
|
||||||
|
assert!(!user.email.is_empty());
|
||||||
|
assert!(!user.username.is_empty());
|
||||||
|
assert!(user.is_active);
|
||||||
|
// is_admin field removed; instead, check role-based permission or is_active
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_test_role_model() {
|
||||||
|
let role = test_utils::create_test_role_model();
|
||||||
|
assert!(!role.name.is_empty());
|
||||||
|
assert!(role.permissions.is_some());
|
||||||
|
assert!(!role.is_system_role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use imphnen_gateway::gateway_service;
|
||||||
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let _ = axum_init(|postgres_conn| async {
|
||||||
|
// PostgreSQL is now the primary database - SurrealDB has been completely removed
|
||||||
|
gateway_service(postgres_conn).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-cms"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait.workspace = true
|
||||||
|
imphnen-iam.workspace = true
|
||||||
|
imphnen-libs.workspace = true
|
||||||
|
imphnen-utils.workspace = true
|
||||||
|
imphnen-entities.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
zod-rs.workspace = true
|
||||||
|
zod-rs-util.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
sea-orm.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
|
||||||
|
paginator-rs.workspace = true
|
||||||
|
paginator-utils.workspace = true
|
||||||
|
paginator-sea-orm.workspace = true
|
||||||
|
paginator-axum.workspace = true
|
||||||
|
|
||||||
|
[package.metadata.validator.regex]
|
||||||
|
VALID_URL_REGEX = "^https?://"
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use crate::events::domain::{EventEntity, EventRepository, EventService};
|
||||||
|
|
||||||
|
pub struct EventServiceImpl {
|
||||||
|
repo: Arc<dyn EventRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn EventRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventService for EventServiceImpl {
|
||||||
|
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||||
|
self.repo.find_all(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||||
|
self.repo.find_by_id(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
self.repo.create(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
self.repo.update(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.delete(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod event_service;
|
||||||
|
|
||||||
|
pub use event_service::EventServiceImpl;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct EventEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod event;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use event::EventEntity;
|
||||||
|
pub use repository::EventRepository;
|
||||||
|
pub use service::EventService;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::event::EventEntity;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait EventRepository: Send + Sync {
|
||||||
|
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::event::EventEntity;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait EventService: Send + Sync {
|
||||||
|
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||||
|
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use imphnen_libs::ZodValidate;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::events::domain::event::EventEntity;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsCreateRequestDto {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub is_online: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for EventsCreateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EventsCreateRequestDto> for EventEntity {
|
||||||
|
fn from(dto: EventsCreateRequestDto) -> Self {
|
||||||
|
EventEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description,
|
||||||
|
detail_link: dto.detail_link,
|
||||||
|
price: dto.price,
|
||||||
|
is_online: dto.is_online,
|
||||||
|
is_deleted: false,
|
||||||
|
location: dto.location,
|
||||||
|
start_date: dto.start_date,
|
||||||
|
end_date: dto.end_date,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsUpdateRequestDto {
|
||||||
|
pub name: String,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for EventsUpdateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub start_date: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EventEntity> for EventsListItemDto {
|
||||||
|
fn from(e: EventEntity) -> Self {
|
||||||
|
EventsListItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
name: e.name,
|
||||||
|
description: e.description,
|
||||||
|
detail_link: e.detail_link,
|
||||||
|
price: e.price,
|
||||||
|
is_online: e.is_online,
|
||||||
|
start_date: e.start_date.to_rfc3339(),
|
||||||
|
end_date: e.end_date.to_rfc3339(),
|
||||||
|
created_at: e.created_at.to_rfc3339(),
|
||||||
|
location: e.location,
|
||||||
|
is_deleted: e.is_deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub start_date: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EventEntity> for EventsDetailItemDto {
|
||||||
|
fn from(e: EventEntity) -> Self {
|
||||||
|
EventsDetailItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
name: e.name,
|
||||||
|
description: e.description,
|
||||||
|
detail_link: e.detail_link,
|
||||||
|
price: e.price,
|
||||||
|
is_online: e.is_online,
|
||||||
|
start_date: e.start_date.to_rfc3339(),
|
||||||
|
end_date: e.end_date.to_rfc3339(),
|
||||||
|
created_at: e.created_at.to_rfc3339(),
|
||||||
|
updated_at: e.updated_at.to_rfc3339(),
|
||||||
|
location: e.location,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{Extension, extract::Path, http::HeaderMap, response::{IntoResponse, Response}};
|
||||||
|
use paginator_axum::PaginationQuery;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
|
use imphnen_utils::{ApiSuccess, ApiPaginated, ApiMessage};
|
||||||
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsUpdateRequestDto};
|
||||||
|
use crate::events::domain::EventService;
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/events",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get event list")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn get_event_list(
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
PaginationQuery(params): PaginationQuery,
|
||||||
|
) -> Response {
|
||||||
|
match service.list(params).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let mapped = PaginatorResponse {
|
||||||
|
data: result.data.into_iter().map(EventsListItemDto::from).collect::<Vec<_>>(),
|
||||||
|
meta: result.meta,
|
||||||
|
};
|
||||||
|
ApiPaginated(mapped).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/events/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn get_event_by_id(
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
let uuid = match Uuid::parse_str(&id) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => return ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, format!("Invalid UUID: {e}")).into_response(),
|
||||||
|
};
|
||||||
|
match service.get(uuid).await {
|
||||||
|
Ok(event) => ApiSuccess(EventsDetailItemDto::from(event)).into_response(),
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/cms/landing/events/create",
|
||||||
|
request_body = EventsCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "[ADMIN] Create new event")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_event(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let entity = payload.into();
|
||||||
|
service.create(entity).await?;
|
||||||
|
Ok(ApiMessage::created("Event created"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/cms/landing/events/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
request_body = EventsUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Update event")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn patch_update_event(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
let existing = service.get(uuid).await?;
|
||||||
|
let entity = crate::events::domain::EventEntity {
|
||||||
|
id: existing.id,
|
||||||
|
name: payload.name,
|
||||||
|
description: payload.description,
|
||||||
|
detail_link: payload.detail_link,
|
||||||
|
price: payload.price,
|
||||||
|
is_online: payload.is_online,
|
||||||
|
location: payload.location,
|
||||||
|
start_date: payload.start_date,
|
||||||
|
end_date: payload.end_date,
|
||||||
|
is_deleted: existing.is_deleted,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
service.update(entity).await?;
|
||||||
|
Ok(ApiMessage::ok("Event updated"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/cms/landing/events/delete/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Soft delete event")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn delete_event(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
service.delete(uuid).await?;
|
||||||
|
Ok(ApiMessage::ok("Event deleted"))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::{events_public_routes, events_protected_routes};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{Router, routing::{delete, get, patch, post}, Extension};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use crate::events::application::EventServiceImpl;
|
||||||
|
use crate::events::domain::EventService;
|
||||||
|
use crate::events::infrastructure::persistence::PostgresEventRepository;
|
||||||
|
use super::handlers::{
|
||||||
|
delete_event, get_event_by_id, get_event_list, patch_update_event, post_create_event,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn build_service(db: DatabaseConnection) -> Arc<dyn EventService> {
|
||||||
|
let repo = Arc::new(PostgresEventRepository::new(db));
|
||||||
|
Arc::new(EventServiceImpl::new(repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events_public_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/cms/landing/events", get(get_event_list))
|
||||||
|
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events_protected_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/cms/landing/events/create", post(post_create_event))
|
||||||
|
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||||
|
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod postgres_event_repository;
|
||||||
|
|
||||||
|
pub use postgres_event_repository::PostgresEventRepository;
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, Order, QueryOrder, PaginatorTrait};
|
||||||
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_entities::seaorm::common::events::{
|
||||||
|
Entity as EventsEntity, Column as EventsColumn,
|
||||||
|
ActiveModel as EventsActiveModel, Model as EventsModel,
|
||||||
|
};
|
||||||
|
use crate::events::domain::{event::EventEntity, repository::EventRepository};
|
||||||
|
|
||||||
|
fn to_entity(model: EventsModel) -> EventEntity {
|
||||||
|
EventEntity {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name,
|
||||||
|
description: model.description,
|
||||||
|
detail_link: model.detail_link,
|
||||||
|
price: model.price,
|
||||||
|
is_online: model.is_online,
|
||||||
|
is_deleted: model.is_deleted,
|
||||||
|
location: model.location,
|
||||||
|
start_date: model.start_date,
|
||||||
|
end_date: model.end_date,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresEventRepository {
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresEventRepository {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db: Arc::new(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventRepository for PostgresEventRepository {
|
||||||
|
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
|
let mut query = EventsEntity::find()
|
||||||
|
.filter(EventsColumn::IsDeleted.eq(false));
|
||||||
|
|
||||||
|
if let Some(ref search) = params.search {
|
||||||
|
query = query.filter(EventsColumn::Name.contains(&search.query));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = match params.sort_by.as_deref() {
|
||||||
|
Some("name") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Desc) => query.order_by(EventsColumn::Name, Order::Desc),
|
||||||
|
_ => query.order_by(EventsColumn::Name, Order::Asc),
|
||||||
|
},
|
||||||
|
_ => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => query.order_by(EventsColumn::CreatedAt, Order::Asc),
|
||||||
|
_ => query.order_by(EventsColumn::CreatedAt, Order::Desc),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
|
let total = paginator.num_items().await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let events = paginator.fetch_page((page - 1) as u64).await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let data = events.into_iter().map(to_entity).collect();
|
||||||
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
|
Ok(PaginatorResponse { data, meta })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||||
|
let event = EventsEntity::find_by_id(id)
|
||||||
|
.filter(EventsColumn::IsDeleted.eq(false))
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(to_entity(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
let active_model = EventsActiveModel {
|
||||||
|
id: ActiveValue::Set(entity.id),
|
||||||
|
name: ActiveValue::Set(entity.name),
|
||||||
|
description: ActiveValue::Set(entity.description),
|
||||||
|
detail_link: ActiveValue::Set(entity.detail_link),
|
||||||
|
price: ActiveValue::Set(entity.price),
|
||||||
|
is_online: ActiveValue::Set(entity.is_online),
|
||||||
|
is_deleted: ActiveValue::Set(false),
|
||||||
|
location: ActiveValue::Set(entity.location),
|
||||||
|
start_date: ActiveValue::Set(entity.start_date),
|
||||||
|
end_date: ActiveValue::Set(entity.end_date),
|
||||||
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
EventsEntity::insert(active_model)
|
||||||
|
.exec(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(entity.id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.name = ActiveValue::Set(entity.name);
|
||||||
|
active_model.description = ActiveValue::Set(entity.description);
|
||||||
|
active_model.detail_link = ActiveValue::Set(entity.detail_link);
|
||||||
|
active_model.price = ActiveValue::Set(entity.price);
|
||||||
|
active_model.is_online = ActiveValue::Set(entity.is_online);
|
||||||
|
active_model.location = ActiveValue::Set(entity.location);
|
||||||
|
active_model.start_date = ActiveValue::Set(entity.start_date);
|
||||||
|
active_model.end_date = ActiveValue::Set(entity.end_date);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
|
active_model.update(self.db.as_ref()).await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
active_model.update(self.db.as_ref()).await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
|
||||||
|
pub use infrastructure::http::{events_public_routes, events_protected_routes};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod events;
|
||||||
|
pub mod testimonials;
|
||||||
|
|
||||||
|
pub use events::{events_public_routes, events_protected_routes};
|
||||||
|
pub use testimonials::{testimonials_public_routes, testimonials_protected_routes};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod testimonial_service;
|
||||||
|
|
||||||
|
pub use testimonial_service::TestimonialServiceImpl;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use crate::testimonials::domain::{TestimonialEntity, TestimonialRepository, TestimonialService};
|
||||||
|
|
||||||
|
pub struct TestimonialServiceImpl {
|
||||||
|
repo: Arc<dyn TestimonialRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestimonialServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn TestimonialRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TestimonialService for TestimonialServiceImpl {
|
||||||
|
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
||||||
|
self.repo.find_all(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
||||||
|
self.repo.find_by_id(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError> {
|
||||||
|
self.repo.create(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
||||||
|
self.repo.update(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.delete(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod testimonial;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use testimonial::TestimonialEntity;
|
||||||
|
pub use repository::TestimonialRepository;
|
||||||
|
pub use service::TestimonialService;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::testimonial::TestimonialEntity;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TestimonialRepository: Send + Sync {
|
||||||
|
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::testimonial::TestimonialEntity;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TestimonialService: Send + Sync {
|
||||||
|
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
||||||
|
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TestimonialEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub user_fullname: String,
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use imphnen_libs::ZodValidate;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use zod_rs::prelude::*;
|
||||||
|
use crate::testimonials::domain::testimonial::TestimonialEntity;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct TestimonialsCreateRequestDto {
|
||||||
|
#[zod(min_length(1), max_length(100))]
|
||||||
|
pub role: String,
|
||||||
|
#[zod(min_length(1), max_length(1000))]
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for TestimonialsCreateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct TestimonialsUpdateRequestDto {
|
||||||
|
#[zod(min_length(1), max_length(100))]
|
||||||
|
pub role: String,
|
||||||
|
#[zod(min_length(1), max_length(1000))]
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for TestimonialsUpdateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TestimonialsListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub user_fullname: String,
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TestimonialEntity> for TestimonialsListItemDto {
|
||||||
|
fn from(e: TestimonialEntity) -> Self {
|
||||||
|
TestimonialsListItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
user_id: e.user_id.to_string(),
|
||||||
|
user_fullname: e.user_fullname,
|
||||||
|
role: e.role,
|
||||||
|
content: e.content,
|
||||||
|
created_at: e.created_at,
|
||||||
|
is_deleted: e.is_deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TestimonialsDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub user_fullname: String,
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TestimonialEntity> for TestimonialsDetailItemDto {
|
||||||
|
fn from(e: TestimonialEntity) -> Self {
|
||||||
|
TestimonialsDetailItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
user_id: e.user_id.to_string(),
|
||||||
|
user_fullname: e.user_fullname,
|
||||||
|
role: e.role,
|
||||||
|
content: e.content,
|
||||||
|
created_at: e.created_at,
|
||||||
|
updated_at: e.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{Extension, extract::Path, http::HeaderMap, http::StatusCode, response::{IntoResponse, Response}};
|
||||||
|
use paginator_axum::PaginationQuery;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
|
use imphnen_utils::{ApiSuccess, ApiCreated, ApiPaginated, ApiMessage, extract_email};
|
||||||
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
|
use imphnen_iam::require_auth;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::dto::{
|
||||||
|
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||||
|
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||||
|
};
|
||||||
|
use crate::testimonials::domain::{TestimonialEntity, TestimonialService};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/testimonials",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get testimonial list")
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn get_testimonial_list(
|
||||||
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
|
PaginationQuery(params): PaginationQuery,
|
||||||
|
) -> Response {
|
||||||
|
match service.list(params).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let mapped = PaginatorResponse {
|
||||||
|
data: result.data.into_iter()
|
||||||
|
.filter(|e| !e.is_deleted)
|
||||||
|
.map(TestimonialsListItemDto::from)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
meta: result.meta,
|
||||||
|
};
|
||||||
|
ApiPaginated(mapped).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => ApiMessage::new(StatusCode::BAD_REQUEST, e.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/testimonials/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn get_testimonial_by_id(
|
||||||
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
let uuid = match Uuid::parse_str(&id) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => return ApiMessage::new(StatusCode::BAD_REQUEST, format!("Invalid UUID: {e}")).into_response(),
|
||||||
|
};
|
||||||
|
match service.get(uuid).await {
|
||||||
|
Ok(t) if !t.is_deleted => {
|
||||||
|
ApiSuccess(TestimonialsDetailItemDto::from(t)).into_response()
|
||||||
|
}
|
||||||
|
Ok(_) => ApiMessage::new(StatusCode::NOT_FOUND, "Testimonial not found").into_response(),
|
||||||
|
Err(e) => ApiMessage::new(StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/cms/landing/testimonials/create",
|
||||||
|
request_body = TestimonialsCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "[USER] Create new testimonial", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_testimonial(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_auth!(headers.clone(), state, {
|
||||||
|
let email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let user_info = state.user_lookup_service.get_user_by_email(&email, &state).await
|
||||||
|
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||||
|
let user = user_info.basic_info;
|
||||||
|
let user_id = Uuid::parse_str(&user.id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {e}")))?;
|
||||||
|
let entity = TestimonialEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
user_id,
|
||||||
|
user_fullname: user.fullname.clone(),
|
||||||
|
role: payload.role,
|
||||||
|
content: payload.content,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
let created = service.create(entity).await?;
|
||||||
|
Ok(ApiCreated(TestimonialsDetailItemDto::from(created)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/cms/landing/testimonials/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
|
),
|
||||||
|
request_body = TestimonialsUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[USER] Update testimonial")
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn patch_update_testimonial(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_auth!(headers, state, {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
let existing = service.get(uuid).await?;
|
||||||
|
let entity = TestimonialEntity {
|
||||||
|
id: existing.id,
|
||||||
|
user_id: existing.user_id,
|
||||||
|
user_fullname: existing.user_fullname,
|
||||||
|
role: payload.role,
|
||||||
|
content: payload.content,
|
||||||
|
is_deleted: existing.is_deleted,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
service.update(entity).await?;
|
||||||
|
Ok(ApiMessage::ok("Testimonial updated"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/cms/landing/testimonials/delete/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[USER] Soft delete testimonial")
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn delete_testimonial(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_auth!(headers, state, {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
service.delete(uuid).await?;
|
||||||
|
Ok(ApiMessage::ok("Testimonial deleted"))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::{testimonials_public_routes, testimonials_protected_routes};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{Router, routing::{delete, get, patch, post}, Extension};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use crate::testimonials::application::TestimonialServiceImpl;
|
||||||
|
use crate::testimonials::domain::TestimonialService;
|
||||||
|
use crate::testimonials::infrastructure::persistence::PostgresTestimonialRepository;
|
||||||
|
use super::handlers::{
|
||||||
|
delete_testimonial, get_testimonial_by_id, get_testimonial_list,
|
||||||
|
patch_update_testimonial, post_create_testimonial,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn build_service(db: DatabaseConnection) -> Arc<dyn TestimonialService> {
|
||||||
|
let repo = Arc::new(PostgresTestimonialRepository::new(db));
|
||||||
|
Arc::new(TestimonialServiceImpl::new(repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn testimonials_public_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
||||||
|
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn testimonials_protected_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
||||||
|
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
||||||
|
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod postgres_testimonial_repository;
|
||||||
|
|
||||||
|
pub use postgres_testimonial_repository::PostgresTestimonialRepository;
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, QueryOrder, PaginatorTrait};
|
||||||
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_entities::seaorm::common::testimonials::{
|
||||||
|
Entity as TestimonialsEntity, Column as TestimonialsColumn, ActiveModel as TestimonialsActiveModel,
|
||||||
|
};
|
||||||
|
use imphnen_entities::seaorm::auth::users::Entity as UsersEntity;
|
||||||
|
use crate::testimonials::domain::{testimonial::TestimonialEntity, repository::TestimonialRepository};
|
||||||
|
|
||||||
|
pub struct PostgresTestimonialRepository {
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresTestimonialRepository {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db: Arc::new(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TestimonialRepository for PostgresTestimonialRepository {
|
||||||
|
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
|
let mut query = TestimonialsEntity::find()
|
||||||
|
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||||
|
.find_also_related(UsersEntity);
|
||||||
|
|
||||||
|
query = match params.sort_by.as_deref() {
|
||||||
|
Some("updated_at") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => query.order_by_asc(TestimonialsColumn::UpdatedAt),
|
||||||
|
_ => query.order_by_desc(TestimonialsColumn::UpdatedAt),
|
||||||
|
},
|
||||||
|
_ => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => query.order_by_asc(TestimonialsColumn::CreatedAt),
|
||||||
|
_ => query.order_by_desc(TestimonialsColumn::CreatedAt),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
|
let total = paginator.num_items().await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let rows = paginator.fetch_page((page - 1) as u64).await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let data: Vec<TestimonialEntity> = rows.into_iter()
|
||||||
|
.filter_map(|(t, u)| {
|
||||||
|
u.map(|user| TestimonialEntity {
|
||||||
|
id: t.id,
|
||||||
|
user_id: t.user_id,
|
||||||
|
user_fullname: format!(
|
||||||
|
"{} {}",
|
||||||
|
user.first_name.as_deref().unwrap_or(""),
|
||||||
|
user.last_name.as_deref().unwrap_or("")
|
||||||
|
).trim().to_string(),
|
||||||
|
role: t.role,
|
||||||
|
content: t.content,
|
||||||
|
is_deleted: t.is_deleted,
|
||||||
|
created_at: t.created_at.to_rfc3339(),
|
||||||
|
updated_at: t.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
|
Ok(PaginatorResponse { data, meta })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
||||||
|
let (testimonial, user) = TestimonialsEntity::find_by_id(id)
|
||||||
|
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||||
|
.find_also_related(UsersEntity)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?;
|
||||||
|
|
||||||
|
let user = user.ok_or_else(|| AppError::NotFoundError("User not found for testimonial".to_string()))?;
|
||||||
|
|
||||||
|
Ok(TestimonialEntity {
|
||||||
|
id: testimonial.id,
|
||||||
|
user_id: testimonial.user_id,
|
||||||
|
user_fullname: format!(
|
||||||
|
"{} {}",
|
||||||
|
user.first_name.as_deref().unwrap_or(""),
|
||||||
|
user.last_name.as_deref().unwrap_or("")
|
||||||
|
).trim().to_string(),
|
||||||
|
role: testimonial.role,
|
||||||
|
content: testimonial.content,
|
||||||
|
is_deleted: testimonial.is_deleted,
|
||||||
|
created_at: testimonial.created_at.to_rfc3339(),
|
||||||
|
updated_at: testimonial.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError> {
|
||||||
|
let active_model = TestimonialsActiveModel {
|
||||||
|
id: ActiveValue::Set(entity.id),
|
||||||
|
user_id: ActiveValue::Set(entity.user_id),
|
||||||
|
role: ActiveValue::Set(entity.role.clone()),
|
||||||
|
content: ActiveValue::Set(entity.content.clone()),
|
||||||
|
is_deleted: ActiveValue::Set(false),
|
||||||
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted = active_model.insert(self.db.as_ref()).await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(TestimonialEntity {
|
||||||
|
id: inserted.id,
|
||||||
|
user_id: inserted.user_id,
|
||||||
|
user_fullname: entity.user_fullname,
|
||||||
|
role: inserted.role,
|
||||||
|
content: inserted.content,
|
||||||
|
is_deleted: inserted.is_deleted,
|
||||||
|
created_at: inserted.created_at.to_rfc3339(),
|
||||||
|
updated_at: inserted.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
||||||
|
let mut active_model: TestimonialsActiveModel = TestimonialsEntity::find_by_id(entity.id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.role = ActiveValue::Set(entity.role);
|
||||||
|
active_model.content = ActiveValue::Set(entity.content);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
|
active_model.update(self.db.as_ref()).await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let mut active_model: TestimonialsActiveModel = TestimonialsEntity::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
active_model.update(self.db.as_ref()).await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
|
||||||
|
pub use infrastructure::http::{testimonials_public_routes, testimonials_protected_routes};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-dimentorin"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-libs.workspace = true
|
||||||
|
imphnen-utils.workspace = true
|
||||||
|
imphnen-entities.workspace = true
|
||||||
|
imphnen-iam.workspace = true
|
||||||
|
imphnen-middleware.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
zod-rs.workspace = true
|
||||||
|
zod-rs-util.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
sea-orm.workspace = true
|
||||||
|
paginator-rs.workspace = true
|
||||||
|
paginator-utils.workspace = true
|
||||||
|
paginator-sea-orm.workspace = true
|
||||||
|
paginator-axum.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dotenvy.workspace = true
|
||||||
|
http-body-util.workspace = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod mentors;
|
||||||
|
pub mod sessions;
|
||||||
|
|
||||||
|
pub use mentors::{mentors_public_routes, mentors_protected_routes};
|
||||||
|
pub use sessions::{sessions_public_routes, sessions_protected_routes};
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_libs::{AppState, hash_password};
|
||||||
|
use imphnen_entities::{RolesDetailQueryDto, users::UserProfileExtensionDto};
|
||||||
|
use imphnen_iam::users::domain::{UserRepository, UserEntity};
|
||||||
|
use imphnen_iam::roles::domain::RoleRepository;
|
||||||
|
use tracing::error;
|
||||||
|
use crate::mentors::domain::{MentorEntity, MentorRepository, MentorService};
|
||||||
|
use crate::mentors::infrastructure::http::dto::{
|
||||||
|
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||||
|
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct MentorServiceImpl {
|
||||||
|
repo: Arc<dyn MentorRepository>,
|
||||||
|
state: Arc<AppState>,
|
||||||
|
user_repo: Arc<dyn UserRepository>,
|
||||||
|
role_repo: Arc<dyn RoleRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MentorServiceImpl {
|
||||||
|
pub fn new(
|
||||||
|
repo: Arc<dyn MentorRepository>,
|
||||||
|
state: Arc<AppState>,
|
||||||
|
user_repo: Arc<dyn UserRepository>,
|
||||||
|
role_repo: Arc<dyn RoleRepository>,
|
||||||
|
) -> Self {
|
||||||
|
Self { repo, state, user_repo, role_repo }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_detail_response(
|
||||||
|
entity: &MentorEntity,
|
||||||
|
user: Option<&imphnen_entities::UsersDetailQueryDto>,
|
||||||
|
) -> MentorDetailResponseDto {
|
||||||
|
MentorDetailResponseDto {
|
||||||
|
id: entity.id.to_string(),
|
||||||
|
user_id: entity.user_id.to_string(),
|
||||||
|
fullname: user.map(|u| u.fullname.clone()),
|
||||||
|
email: user.map(|u| u.email.clone()),
|
||||||
|
legal_name: user.and_then(|u| u.legal_name.clone()),
|
||||||
|
gender: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.gender.clone()),
|
||||||
|
domicile: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.domicile.clone()),
|
||||||
|
phone_for_verification: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.phone_for_verification.clone()),
|
||||||
|
bio: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.bio.clone()),
|
||||||
|
last_education: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.last_education.clone()),
|
||||||
|
linkedin_url: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.linkedin_url.clone()),
|
||||||
|
github_url: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.github_url.clone()),
|
||||||
|
cv_url: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.cv_url.clone()),
|
||||||
|
portfolio_url: user
|
||||||
|
.and_then(|u| u.profile_extension.as_ref())
|
||||||
|
.and_then(|ext| ext.portfolio_url.clone()),
|
||||||
|
industries: entity.industries.clone(),
|
||||||
|
expertise: entity.expertise.clone(),
|
||||||
|
languages: entity.languages.clone(),
|
||||||
|
current_company: entity.current_company.clone(),
|
||||||
|
current_role: entity.current_role.clone(),
|
||||||
|
years_of_experience: entity.years_of_experience,
|
||||||
|
topics_of_interest: entity.topics_of_interest.clone(),
|
||||||
|
preferred_mentee_level: entity.preferred_mentee_level.clone(),
|
||||||
|
preferred_mentoring_formats: entity.preferred_mentoring_formats.clone(),
|
||||||
|
availability_commitment: entity.availability_commitment.clone(),
|
||||||
|
mentoring_rate: entity.mentoring_rate,
|
||||||
|
status: entity.status.clone(),
|
||||||
|
created_at: entity.created_at.to_rfc3339(),
|
||||||
|
updated_at: entity.updated_at.to_rfc3339(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MentorService for MentorServiceImpl {
|
||||||
|
async fn list(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<MentorListResponseDto>, AppError> {
|
||||||
|
let result = self.repo.find_all(params).await?;
|
||||||
|
|
||||||
|
let mut items: Vec<MentorListResponseDto> = Vec::with_capacity(result.data.len());
|
||||||
|
for entity in &result.data {
|
||||||
|
let mut item = MentorListResponseDto {
|
||||||
|
id: entity.id.to_string(),
|
||||||
|
user_id: entity.user_id.to_string(),
|
||||||
|
fullname: None,
|
||||||
|
email: None,
|
||||||
|
status: entity.status.clone(),
|
||||||
|
created_at: entity.created_at.to_rfc3339(),
|
||||||
|
updated_at: entity.updated_at.to_rfc3339(),
|
||||||
|
};
|
||||||
|
if let Ok(info) = self.state.user_lookup_service
|
||||||
|
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
item.fullname = Some(info.basic_info.fullname);
|
||||||
|
item.email = Some(info.basic_info.email);
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PaginatorResponse { data: items, meta: result.meta })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetailResponseDto, AppError> {
|
||||||
|
let entity = self.repo.find_by_id(id, false).await?;
|
||||||
|
let user = self.state.user_lookup_service
|
||||||
|
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|i| i.basic_info);
|
||||||
|
Ok(Self::build_detail_response(&entity, user.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_by_email(&self, email: &str) -> Result<MentorDetailResponseDto, AppError> {
|
||||||
|
let user_dto = self.state.user_lookup_service
|
||||||
|
.get_user_by_email(email, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|i| i.basic_info)
|
||||||
|
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||||
|
|
||||||
|
let user_id = Uuid::parse_str(&user_dto.id)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||||
|
Ok(Self::build_detail_response(&entity, Some(&user_dto)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register(
|
||||||
|
&self,
|
||||||
|
dto: MentorUserRegisterRequestDto,
|
||||||
|
) -> Result<MentorRegisterResponseDto, AppError> {
|
||||||
|
let user_email = dto.email.clone();
|
||||||
|
|
||||||
|
let user_id: Uuid = match self.user_repo.find_by_email(user_email.clone()).await {
|
||||||
|
Ok(mut entity) => {
|
||||||
|
let existing_user_id = Uuid::parse_str(&entity.id)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
if self.repo.find_by_user_id(existing_user_id, false).await.is_ok() {
|
||||||
|
return Err(AppError::ConflictError(
|
||||||
|
"Mentor profile already exists for this user".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mentor_role = self.role_repo
|
||||||
|
.find_by_name("Mentor".to_string())
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::BadRequestError("Mentor Role Not Found".to_string()))?;
|
||||||
|
|
||||||
|
entity.fullname = dto.fullname.clone();
|
||||||
|
entity.is_active = false;
|
||||||
|
entity.role = RolesDetailQueryDto {
|
||||||
|
id: mentor_role.id.to_string(),
|
||||||
|
name: mentor_role.name.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
entity.password = hash_password(&dto.password).map_err(|e| {
|
||||||
|
error!("Failed to hash password for {}: {}", user_email, e);
|
||||||
|
AppError::InternalServerError("Failed to hash password".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut profile_ext = entity.profile_extension.clone().unwrap_or_default();
|
||||||
|
profile_ext.phone_number = dto.phone_number.clone();
|
||||||
|
profile_ext.phone_for_verification = dto.identity_and_verification.phone_for_verification.clone();
|
||||||
|
profile_ext.gender = dto.identity_and_verification.gender.clone();
|
||||||
|
profile_ext.domicile = dto.identity_and_verification.domicile.clone();
|
||||||
|
profile_ext.bio = Some(dto.professional_profile.bio.clone());
|
||||||
|
profile_ext.last_education = dto.professional_profile.last_education.clone();
|
||||||
|
profile_ext.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||||
|
profile_ext.github_url = dto.professional_profile.github_url.clone();
|
||||||
|
profile_ext.cv_url = dto.professional_profile.cv_url.clone();
|
||||||
|
profile_ext.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||||
|
entity.profile_extension = Some(profile_ext);
|
||||||
|
|
||||||
|
let uid_str = entity.id.clone();
|
||||||
|
self.user_repo.update(entity).await.map_err(|e| {
|
||||||
|
error!("Failed to update user {} to mentor role: {}", user_email, e);
|
||||||
|
AppError::InternalServerError(e.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Uuid::parse_str(&uid_str)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let mentor_role = self.role_repo
|
||||||
|
.find_by_name("Mentor".to_string())
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::BadRequestError("Mentor Role Not Found".to_string()))?;
|
||||||
|
|
||||||
|
let hashed_password = hash_password(&dto.password).map_err(|e| {
|
||||||
|
error!("Failed to hash password for new user {}: {}", user_email, e);
|
||||||
|
AppError::InternalServerError("Failed to hash password".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let new_user_id = Uuid::new_v4();
|
||||||
|
let profile_ext = UserProfileExtensionDto {
|
||||||
|
phone_number: dto.phone_number.clone(),
|
||||||
|
phone_for_verification: dto.identity_and_verification.phone_for_verification.clone(),
|
||||||
|
gender: dto.identity_and_verification.gender.clone(),
|
||||||
|
domicile: dto.identity_and_verification.domicile.clone(),
|
||||||
|
bio: Some(dto.professional_profile.bio.clone()),
|
||||||
|
last_education: dto.professional_profile.last_education.clone(),
|
||||||
|
linkedin_url: dto.professional_profile.linkedin_url.clone(),
|
||||||
|
github_url: dto.professional_profile.github_url.clone(),
|
||||||
|
cv_url: dto.professional_profile.cv_url.clone(),
|
||||||
|
portfolio_url: dto.professional_profile.portfolio_url.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let new_entity = UserEntity {
|
||||||
|
id: new_user_id.to_string(),
|
||||||
|
email: dto.email.clone(),
|
||||||
|
fullname: dto.fullname.clone(),
|
||||||
|
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||||
|
password: hashed_password,
|
||||||
|
is_active: false,
|
||||||
|
role: RolesDetailQueryDto {
|
||||||
|
id: mentor_role.id.to_string(),
|
||||||
|
name: mentor_role.name.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
profile_extension: Some(profile_ext),
|
||||||
|
created_at: imphnen_utils::get_iso_date(),
|
||||||
|
updated_at: imphnen_utils::get_iso_date(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.user_repo.create(new_entity).await.map_err(|e| {
|
||||||
|
error!("Failed to create new user {}: {}", user_email, e);
|
||||||
|
AppError::InternalServerError(e.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
new_user_id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_entity = MentorEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
user_id,
|
||||||
|
industries: dto.professional_profile.industries.clone(),
|
||||||
|
expertise: dto.professional_profile.expertise.clone(),
|
||||||
|
languages: dto.professional_profile.languages.clone(),
|
||||||
|
current_company: dto.professional_profile.current_company.clone(),
|
||||||
|
current_role: dto.professional_profile.current_role.clone(),
|
||||||
|
years_of_experience: dto.professional_profile.years_of_experience,
|
||||||
|
topics_of_interest: dto.mentoring_logistics.topics_of_interest.clone(),
|
||||||
|
preferred_mentee_level: dto.mentoring_logistics.preferred_mentee_level.clone(),
|
||||||
|
preferred_mentoring_formats: dto.mentoring_logistics.preferred_mentoring_formats.clone(),
|
||||||
|
availability_commitment: dto.mentoring_logistics.availability_commitment.clone(),
|
||||||
|
mentoring_rate: dto.mentoring_logistics.mentoring_rate_amount as f64,
|
||||||
|
status: "pending".to_string(),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mentor_id = self.repo.create(new_entity.clone()).await.map_err(|e| {
|
||||||
|
error!("Failed to create mentor profile for {}: {}", user_email, e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(MentorRegisterResponseDto {
|
||||||
|
id: mentor_id.to_string(),
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
email: Some(user_email),
|
||||||
|
status: "pending".to_string(),
|
||||||
|
created_at: new_entity.created_at.to_rfc3339(),
|
||||||
|
updated_at: new_entity.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
dto: MentorUpdateRequestDto,
|
||||||
|
) -> Result<MentorDetailResponseDto, AppError> {
|
||||||
|
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||||
|
|
||||||
|
if let Some(val) = dto.industries { entity.industries = val; }
|
||||||
|
if let Some(val) = dto.expertise { entity.expertise = val; }
|
||||||
|
if let Some(val) = dto.languages { entity.languages = val; }
|
||||||
|
if let Some(val) = dto.current_company { entity.current_company = val; }
|
||||||
|
if let Some(val) = dto.current_role { entity.current_role = val; }
|
||||||
|
if let Some(val) = dto.years_of_experience { entity.years_of_experience = val; }
|
||||||
|
if let Some(val) = dto.topics_of_interest { entity.topics_of_interest = val; }
|
||||||
|
if let Some(val) = dto.preferred_mentee_level { entity.preferred_mentee_level = val; }
|
||||||
|
if let Some(val) = dto.preferred_mentoring_formats { entity.preferred_mentoring_formats = val; }
|
||||||
|
if let Some(val) = dto.availability_commitment { entity.availability_commitment = val; }
|
||||||
|
if let Some(val) = dto.mentoring_rate_amount { entity.mentoring_rate = val as f64; }
|
||||||
|
entity.updated_at = chrono::Utc::now();
|
||||||
|
|
||||||
|
self.repo.update(entity).await?;
|
||||||
|
|
||||||
|
let updated = self.repo.find_by_id(id, false).await?;
|
||||||
|
let user = self.state.user_lookup_service
|
||||||
|
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|i| i.basic_info);
|
||||||
|
Ok(Self::build_detail_response(&updated, user.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_me(
|
||||||
|
&self,
|
||||||
|
email: &str,
|
||||||
|
dto: MentorUpdateRequestDto,
|
||||||
|
) -> Result<MentorDetailResponseDto, AppError> {
|
||||||
|
let user_dto = self.state.user_lookup_service
|
||||||
|
.get_user_by_email(email, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|i| i.basic_info)
|
||||||
|
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||||
|
|
||||||
|
let user_id = Uuid::parse_str(&user_dto.id)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||||
|
|
||||||
|
if let Some(val) = dto.industries { entity.industries = val; }
|
||||||
|
if let Some(val) = dto.expertise { entity.expertise = val; }
|
||||||
|
if let Some(val) = dto.languages { entity.languages = val; }
|
||||||
|
if let Some(val) = dto.current_company { entity.current_company = val; }
|
||||||
|
if let Some(val) = dto.current_role { entity.current_role = val; }
|
||||||
|
if let Some(val) = dto.years_of_experience { entity.years_of_experience = val; }
|
||||||
|
if let Some(val) = dto.topics_of_interest { entity.topics_of_interest = val; }
|
||||||
|
if let Some(val) = dto.preferred_mentee_level { entity.preferred_mentee_level = val; }
|
||||||
|
if let Some(val) = dto.preferred_mentoring_formats { entity.preferred_mentoring_formats = val; }
|
||||||
|
if let Some(val) = dto.availability_commitment { entity.availability_commitment = val; }
|
||||||
|
if let Some(val) = dto.mentoring_rate_amount { entity.mentoring_rate = val as f64; }
|
||||||
|
entity.updated_at = chrono::Utc::now();
|
||||||
|
|
||||||
|
let entity_id = entity.id;
|
||||||
|
self.repo.update(entity).await?;
|
||||||
|
|
||||||
|
let updated = self.repo.find_by_id(entity_id, false).await?;
|
||||||
|
let refreshed_user = self.state.user_lookup_service
|
||||||
|
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|i| i.basic_info);
|
||||||
|
Ok(Self::build_detail_response(&updated, refreshed_user.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.soft_delete(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
dto: MentorVerifyRequestDto,
|
||||||
|
) -> Result<MentorDetailResponseDto, AppError> {
|
||||||
|
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||||
|
entity.status = dto.status;
|
||||||
|
entity.updated_at = chrono::Utc::now();
|
||||||
|
|
||||||
|
self.repo.update(entity).await?;
|
||||||
|
|
||||||
|
let updated = self.repo.find_by_id(id, false).await?;
|
||||||
|
let user = self.state.user_lookup_service
|
||||||
|
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|i| i.basic_info);
|
||||||
|
Ok(Self::build_detail_response(&updated, user.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_status(&self, email: &str) -> Result<String, AppError> {
|
||||||
|
let user_dto = self.state.user_lookup_service
|
||||||
|
.get_user_by_email(email, self.state.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|i| i.basic_info)
|
||||||
|
.map_err(|_| {
|
||||||
|
AppError::NotFoundError("No mentor application found for current user".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let user_id = Uuid::parse_str(&user_dto.id)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let entity = self.repo.find_by_user_id(user_id, false).await.map_err(|_| {
|
||||||
|
AppError::NotFoundError("No mentor application found for current user".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(entity.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_entity_by_id(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
include_deleted: bool,
|
||||||
|
) -> Result<MentorEntity, AppError> {
|
||||||
|
self.repo.find_by_id(id, include_deleted).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod mentor_service;
|
||||||
|
|
||||||
|
pub use mentor_service::MentorServiceImpl;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MentorEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub industries: Vec<String>,
|
||||||
|
pub expertise: Vec<String>,
|
||||||
|
pub languages: Vec<String>,
|
||||||
|
pub current_company: String,
|
||||||
|
pub current_role: String,
|
||||||
|
pub years_of_experience: i32,
|
||||||
|
pub topics_of_interest: Vec<String>,
|
||||||
|
pub preferred_mentee_level: Vec<String>,
|
||||||
|
pub preferred_mentoring_formats: Vec<String>,
|
||||||
|
pub availability_commitment: String,
|
||||||
|
pub mentoring_rate: f64,
|
||||||
|
pub status: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod mentor;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use mentor::MentorEntity;
|
||||||
|
pub use repository::MentorRepository;
|
||||||
|
pub use service::MentorService;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::mentor::MentorEntity;
|
||||||
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait MentorRepository: Send + Sync {
|
||||||
|
async fn find_all(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<MentorEntity>, AppError>;
|
||||||
|
|
||||||
|
async fn find_by_id(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
include_deleted: bool,
|
||||||
|
) -> Result<MentorEntity, AppError>;
|
||||||
|
|
||||||
|
async fn find_by_user_id(
|
||||||
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
include_deleted: bool,
|
||||||
|
) -> Result<MentorEntity, AppError>;
|
||||||
|
|
||||||
|
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError>;
|
||||||
|
|
||||||
|
async fn update(&self, entity: MentorEntity) -> Result<(), AppError>;
|
||||||
|
|
||||||
|
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::mentor::MentorEntity;
|
||||||
|
use crate::mentors::infrastructure::http::dto::{
|
||||||
|
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||||
|
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait MentorService: Send + Sync {
|
||||||
|
async fn list(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<MentorListResponseDto>, AppError>;
|
||||||
|
|
||||||
|
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetailResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn get_by_email(&self, email: &str) -> Result<MentorDetailResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn register(
|
||||||
|
&self,
|
||||||
|
dto: MentorUserRegisterRequestDto,
|
||||||
|
) -> Result<MentorRegisterResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
dto: MentorUpdateRequestDto,
|
||||||
|
) -> Result<MentorDetailResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn update_me(
|
||||||
|
&self,
|
||||||
|
email: &str,
|
||||||
|
dto: MentorUpdateRequestDto,
|
||||||
|
) -> Result<MentorDetailResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
dto: MentorVerifyRequestDto,
|
||||||
|
) -> Result<MentorDetailResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn get_status(&self, email: &str) -> Result<String, AppError>;
|
||||||
|
|
||||||
|
async fn get_entity_by_id(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
include_deleted: bool,
|
||||||
|
) -> Result<MentorEntity, AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
use imphnen_libs::ZodValidate;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use zod_rs::prelude::*;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Response DTOs
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct MentorListResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub fullname: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct MentorDetailResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub fullname: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub legal_name: Option<String>,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub domicile: Option<String>,
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
|
pub bio: Option<String>,
|
||||||
|
pub last_education: Option<String>,
|
||||||
|
pub linkedin_url: Option<String>,
|
||||||
|
pub github_url: Option<String>,
|
||||||
|
pub cv_url: Option<String>,
|
||||||
|
pub portfolio_url: Option<String>,
|
||||||
|
pub industries: Vec<String>,
|
||||||
|
pub expertise: Vec<String>,
|
||||||
|
pub languages: Vec<String>,
|
||||||
|
pub current_company: String,
|
||||||
|
pub current_role: String,
|
||||||
|
pub years_of_experience: i32,
|
||||||
|
pub topics_of_interest: Vec<String>,
|
||||||
|
pub preferred_mentee_level: Vec<String>,
|
||||||
|
pub preferred_mentoring_formats: Vec<String>,
|
||||||
|
pub availability_commitment: String,
|
||||||
|
pub mentoring_rate: f64,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct MentorRegisterResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Request DTOs
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct MentorUserRegisterRequestDto {
|
||||||
|
#[zod(email, min_length(1))]
|
||||||
|
pub email: String,
|
||||||
|
#[zod(min_length(8), regex(pattern = "^[A-Za-z\\d@$!%*?&]{8,}$"))]
|
||||||
|
pub password: String,
|
||||||
|
#[zod(min_length(2))]
|
||||||
|
pub fullname: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phone_number: Option<String>,
|
||||||
|
pub identity_and_verification: IdentityAndVerification,
|
||||||
|
pub professional_profile: ProfessionalProfile,
|
||||||
|
pub mentoring_logistics: MentoringLogistics,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for MentorUserRegisterRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct IdentityAndVerification {
|
||||||
|
#[zod(min_length(3))]
|
||||||
|
pub legal_name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gender: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub domicile: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
pub identity_document_url: String,
|
||||||
|
#[zod(min_length(10), max_length(15))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for IdentityAndVerification {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct ProfessionalProfile {
|
||||||
|
#[zod(min_length(50))]
|
||||||
|
pub bio: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_education: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub linkedin_url: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub github_url: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cv_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub portfolio_url: Option<String>,
|
||||||
|
pub industries: Vec<String>,
|
||||||
|
pub expertise: Vec<String>,
|
||||||
|
pub languages: Vec<String>,
|
||||||
|
#[zod(min_length(1))]
|
||||||
|
pub current_company: String,
|
||||||
|
#[zod(min_length(1))]
|
||||||
|
pub current_role: String,
|
||||||
|
#[zod(min(2.0), int)]
|
||||||
|
pub years_of_experience: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for ProfessionalProfile {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct MentoringLogistics {
|
||||||
|
pub topics_of_interest: Vec<String>,
|
||||||
|
pub preferred_mentee_level: Vec<String>,
|
||||||
|
pub preferred_mentoring_formats: Vec<String>,
|
||||||
|
#[zod(min_length(5))]
|
||||||
|
pub availability_commitment: String,
|
||||||
|
#[zod(min(1.0))]
|
||||||
|
pub mentoring_rate_amount: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for MentoringLogistics {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct MentorUpdateRequestDto {
|
||||||
|
#[zod(min_length(3))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub legal_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gender: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub domicile: Option<String>,
|
||||||
|
#[zod(min_length(10), max_length(15))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phone_for_verification: Option<String>,
|
||||||
|
#[zod(min_length(50))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bio: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_education: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub linkedin_url: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub github_url: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cv_url: Option<String>,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub portfolio_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub industries: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub expertise: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub languages: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_company: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_role: Option<String>,
|
||||||
|
#[zod(min(2.0), int)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub years_of_experience: Option<i32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub topics_of_interest: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub preferred_mentee_level: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub preferred_mentoring_formats: Option<Vec<String>>,
|
||||||
|
#[zod(min_length(5))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub availability_commitment: Option<String>,
|
||||||
|
#[zod(min(1.0))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mentoring_rate_amount: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for MentorUpdateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct MentorVerifyRequestDto {
|
||||||
|
#[zod(min_length(1))]
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for MentorVerifyRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema, Default)]
|
||||||
|
pub struct MentoringRate {
|
||||||
|
#[zod(min(1.0))]
|
||||||
|
pub amount: u64,
|
||||||
|
#[zod(min_length(1))]
|
||||||
|
pub currency: String,
|
||||||
|
#[zod(min_length(1))]
|
||||||
|
pub per_duration: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for MentoringRate {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct MentorRegisterFromTokenRequestDto {
|
||||||
|
pub identity_and_verification: IdentityAndVerification,
|
||||||
|
pub professional_profile: ProfessionalProfile,
|
||||||
|
pub mentoring_logistics: MentoringLogistics,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for MentorRegisterFromTokenRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{
|
||||||
|
extract::{Extension, Path},
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use paginator_axum::PaginationQuery;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
|
use imphnen_utils::{ApiSuccess, ApiPaginated, ApiMessage, extract_email};
|
||||||
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use crate::mentors::domain::MentorService;
|
||||||
|
use super::dto::{
|
||||||
|
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||||
|
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/mentors/create",
|
||||||
|
request_body = MentorUserRegisterRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||||
|
(status = 400, description = "[PUBLIC] Bad request - validation error"),
|
||||||
|
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
|
||||||
|
(status = 500, description = "[PUBLIC] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors"
|
||||||
|
)]
|
||||||
|
pub async fn post_register_mentor(
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||||
|
) -> Response {
|
||||||
|
match service.register(dto).await {
|
||||||
|
Ok(resp) => ApiSuccess(resp).into_response(),
|
||||||
|
Err(e) => ApiMessage::new(e.status_code(), e.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors",
|
||||||
|
params(
|
||||||
|
("page" = Option<u64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<u64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search query"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||||
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn get_mentor_list(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
PaginationQuery(params): PaginationQuery,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::ReadListMentors], {
|
||||||
|
let result = service.list(params).await?;
|
||||||
|
Ok(ApiPaginated(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
|
||||||
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn get_mentor_by_id(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let mentor_uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::ReadDetailMentors], {
|
||||||
|
let dto = service.get_by_id(mentor_uuid).await?;
|
||||||
|
Ok(ApiSuccess(dto))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/mentors/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID")
|
||||||
|
),
|
||||||
|
request_body = MentorUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
|
||||||
|
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||||
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors - Admin",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn put_update_mentor(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let mentor_uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::UpdateMentors], {
|
||||||
|
let result = service.update(mentor_uuid, dto).await?;
|
||||||
|
Ok(ApiSuccess(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/mentors/delete/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
|
||||||
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors - Admin",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn delete_mentor(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let mentor_uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::DeleteMentors], {
|
||||||
|
service.delete(mentor_uuid).await?;
|
||||||
|
Ok(ApiMessage::ok("Mentor deleted successfully"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/mentors/verify/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID")
|
||||||
|
),
|
||||||
|
request_body = MentorVerifyRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
|
||||||
|
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||||
|
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||||
|
(status = 500, description = "[ADMIN] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors - Admin",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn put_verify_mentor(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let mentor_uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::VerifyMentors], {
|
||||||
|
let result = service.verify(mentor_uuid, dto).await?;
|
||||||
|
Ok(ApiSuccess(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors/me",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
|
||||||
|
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||||
|
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
|
||||||
|
(status = 500, description = "[MENTOR] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn get_mentor_me(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers.clone(), state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||||
|
let email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let dto = service.get_by_email(&email).await
|
||||||
|
.map_err(|_| AppError::ForbiddenError("Mentor profile not found for current user".to_string()))?;
|
||||||
|
Ok(ApiSuccess(dto))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/mentors/me/update",
|
||||||
|
request_body = MentorUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||||
|
(status = 400, description = "[MENTOR] Bad request - validation error"),
|
||||||
|
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||||
|
(status = 404, description = "[MENTOR] Mentor profile not found"),
|
||||||
|
(status = 500, description = "[MENTOR] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn put_update_mentor_me(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers.clone(), state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||||
|
let email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let resp = service.update_me(&email, dto).await?;
|
||||||
|
Ok(ApiSuccess(resp))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/mentors/update",
|
||||||
|
request_body = MentorUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
|
||||||
|
),
|
||||||
|
tag = "Mentors - Admin"
|
||||||
|
)]
|
||||||
|
pub async fn put_update_mentor_no_id() -> impl IntoResponse {
|
||||||
|
ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, "Mentor ID is required for update")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors/me/status",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[MENTOR] Mentor application status", body = String),
|
||||||
|
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||||
|
(status = 403, description = "[MENTOR] No mentor application found for current user"),
|
||||||
|
(status = 500, description = "[MENTOR] Internal server error")
|
||||||
|
),
|
||||||
|
tag = "Mentors",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn get_mentor_status(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers.clone(), state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||||
|
let email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let status = service.get_status(&email).await
|
||||||
|
.map_err(|_| AppError::ForbiddenError("No mentor application found for current user".to_string()))?;
|
||||||
|
Ok(ApiMessage::ok(&status))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::{mentors_protected_routes, mentors_public_routes};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
Extension, Router,
|
||||||
|
};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use imphnen_iam::users::infrastructure::persistence::PostgresUserRepository;
|
||||||
|
use imphnen_iam::roles::infrastructure::persistence::PostgresRoleRepository;
|
||||||
|
use crate::mentors::application::MentorServiceImpl;
|
||||||
|
use crate::mentors::domain::MentorService;
|
||||||
|
use crate::mentors::infrastructure::persistence::PostgresMentorRepository;
|
||||||
|
use super::handlers::{
|
||||||
|
delete_mentor, get_mentor_by_id, get_mentor_list, get_mentor_me, get_mentor_status,
|
||||||
|
post_register_mentor, put_update_mentor, put_update_mentor_me, put_update_mentor_no_id,
|
||||||
|
put_verify_mentor,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn build_service(db: DatabaseConnection, state: Arc<AppState>) -> Arc<dyn MentorService> {
|
||||||
|
let user_repo = Arc::new(PostgresUserRepository::new(db.clone()));
|
||||||
|
let role_repo = Arc::new(PostgresRoleRepository::new(db.clone()));
|
||||||
|
let repo = Arc::new(PostgresMentorRepository::new(db));
|
||||||
|
Arc::new(MentorServiceImpl::new(repo, state, user_repo, role_repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mentors_public_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||||
|
let service = build_service(db, state);
|
||||||
|
Router::new()
|
||||||
|
.route("/mentors/create", post(post_register_mentor))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mentors_protected_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||||
|
let svc = build_service(db, Arc::clone(&state));
|
||||||
|
Router::new()
|
||||||
|
.route("/mentors", get(get_mentor_list))
|
||||||
|
.route("/mentors/me", get(get_mentor_me))
|
||||||
|
.route("/mentors/me/update", put(put_update_mentor_me))
|
||||||
|
.route("/mentors/me/status", get(get_mentor_status))
|
||||||
|
.route("/mentors/detail/{id}", get(get_mentor_by_id))
|
||||||
|
.route("/mentors/update/{id}", put(put_update_mentor))
|
||||||
|
.route("/mentors/update", put(put_update_mentor_no_id))
|
||||||
|
.route("/mentors/delete/{id}", delete(delete_mentor))
|
||||||
|
.route("/mentors/verify/{id}", put(put_verify_mentor))
|
||||||
|
.layer(Extension(svc))
|
||||||
|
.layer(Extension((*state).clone()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod postgres_mentor_repository;
|
||||||
|
|
||||||
|
pub use postgres_mentor_repository::PostgresMentorRepository;
|
||||||
+283
@@ -0,0 +1,283 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, Order, QueryOrder, PaginatorTrait};
|
||||||
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_entities::seaorm::auth::mentors::{
|
||||||
|
Entity as MentorsEntity,
|
||||||
|
Column as MentorColumn,
|
||||||
|
ActiveModel as MentorActiveModel,
|
||||||
|
Model as MentorModel,
|
||||||
|
};
|
||||||
|
use crate::mentors::domain::{mentor::MentorEntity, repository::MentorRepository};
|
||||||
|
|
||||||
|
fn model_to_entity(model: MentorModel) -> MentorEntity {
|
||||||
|
MentorEntity {
|
||||||
|
id: model.id,
|
||||||
|
user_id: model.user_id,
|
||||||
|
industries: serde_json::from_value(
|
||||||
|
model.industries.unwrap_or(serde_json::Value::Array(vec![])),
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
expertise: serde_json::from_value(
|
||||||
|
model.expertise.unwrap_or(serde_json::Value::Array(vec![])),
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
languages: serde_json::from_value(
|
||||||
|
model.languages.unwrap_or(serde_json::Value::Array(vec![])),
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
current_company: model.current_company.unwrap_or_default(),
|
||||||
|
current_role: model.current_role.unwrap_or_default(),
|
||||||
|
years_of_experience: model.years_of_experience.unwrap_or(0),
|
||||||
|
topics_of_interest: serde_json::from_value(
|
||||||
|
model.topics_of_interest.unwrap_or(serde_json::Value::Array(vec![])),
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
preferred_mentee_level: serde_json::from_str(
|
||||||
|
&model.preferred_mentee_level.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
preferred_mentoring_formats: serde_json::from_value(
|
||||||
|
model
|
||||||
|
.preferred_mentoring_formats
|
||||||
|
.unwrap_or(serde_json::Value::Array(vec![])),
|
||||||
|
)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
availability_commitment: model.availability_commitment.unwrap_or_default(),
|
||||||
|
mentoring_rate: model.mentoring_rate.unwrap_or(0.0),
|
||||||
|
status: model.status.unwrap_or_default(),
|
||||||
|
is_deleted: model.is_deleted,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresMentorRepository {
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresMentorRepository {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db: Arc::new(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MentorRepository for PostgresMentorRepository {
|
||||||
|
async fn find_all(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<MentorEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
|
let mut query = MentorsEntity::find()
|
||||||
|
.filter(MentorColumn::IsDeleted.eq(false));
|
||||||
|
|
||||||
|
query = match params.sort_by.as_deref() {
|
||||||
|
Some("updated_at") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => query.order_by(MentorColumn::UpdatedAt, Order::Asc),
|
||||||
|
_ => query.order_by(MentorColumn::UpdatedAt, Order::Desc),
|
||||||
|
},
|
||||||
|
_ => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => query.order_by(MentorColumn::CreatedAt, Order::Asc),
|
||||||
|
_ => query.order_by(MentorColumn::CreatedAt, Order::Desc),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
|
let total = paginator
|
||||||
|
.num_items()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let mentors = paginator
|
||||||
|
.fetch_page((page - 1) as u64)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let data = mentors.into_iter().map(model_to_entity).collect();
|
||||||
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
|
Ok(PaginatorResponse { data, meta })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
include_deleted: bool,
|
||||||
|
) -> Result<MentorEntity, AppError> {
|
||||||
|
let mut query = MentorsEntity::find_by_id(id);
|
||||||
|
|
||||||
|
if !include_deleted {
|
||||||
|
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let model = query
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(model_to_entity(model))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_user_id(
|
||||||
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
include_deleted: bool,
|
||||||
|
) -> Result<MentorEntity, AppError> {
|
||||||
|
let mut query = MentorsEntity::find()
|
||||||
|
.filter(MentorColumn::UserId.eq(user_id));
|
||||||
|
|
||||||
|
if !include_deleted {
|
||||||
|
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let model = query
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(model_to_entity(model))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError> {
|
||||||
|
let active_model = MentorActiveModel {
|
||||||
|
user_id: ActiveValue::Set(entity.user_id),
|
||||||
|
industries: ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.industries)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
)),
|
||||||
|
expertise: ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.expertise)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
)),
|
||||||
|
languages: ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.languages)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
)),
|
||||||
|
current_company: ActiveValue::Set(Some(entity.current_company)),
|
||||||
|
current_role: ActiveValue::Set(Some(entity.current_role)),
|
||||||
|
years_of_experience: ActiveValue::Set(Some(entity.years_of_experience)),
|
||||||
|
topics_of_interest: ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.topics_of_interest)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
)),
|
||||||
|
preferred_mentee_level: ActiveValue::Set(Some(
|
||||||
|
serde_json::to_string(&entity.preferred_mentee_level)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
)),
|
||||||
|
preferred_mentoring_formats: ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
)),
|
||||||
|
availability_commitment: ActiveValue::Set(Some(entity.availability_commitment)),
|
||||||
|
mentoring_rate: ActiveValue::Set(Some(entity.mentoring_rate)),
|
||||||
|
status: ActiveValue::Set(Some(entity.status)),
|
||||||
|
is_deleted: ActiveValue::Set(false),
|
||||||
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = MentorsEntity::insert(active_model)
|
||||||
|
.exec(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(result.last_insert_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: MentorEntity) -> Result<(), AppError> {
|
||||||
|
let mut active_model: MentorActiveModel = MentorsEntity::find_by_id(entity.id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
if !entity.industries.is_empty() {
|
||||||
|
active_model.industries = ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.industries)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !entity.expertise.is_empty() {
|
||||||
|
active_model.expertise = ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.expertise)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !entity.languages.is_empty() {
|
||||||
|
active_model.languages = ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.languages)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !entity.current_company.is_empty() {
|
||||||
|
active_model.current_company = ActiveValue::Set(Some(entity.current_company));
|
||||||
|
}
|
||||||
|
if !entity.current_role.is_empty() {
|
||||||
|
active_model.current_role = ActiveValue::Set(Some(entity.current_role));
|
||||||
|
}
|
||||||
|
active_model.years_of_experience = ActiveValue::Set(Some(entity.years_of_experience));
|
||||||
|
if !entity.topics_of_interest.is_empty() {
|
||||||
|
active_model.topics_of_interest = ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.topics_of_interest)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !entity.preferred_mentee_level.is_empty() {
|
||||||
|
active_model.preferred_mentee_level = ActiveValue::Set(Some(
|
||||||
|
serde_json::to_string(&entity.preferred_mentee_level)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !entity.preferred_mentoring_formats.is_empty() {
|
||||||
|
active_model.preferred_mentoring_formats = ActiveValue::Set(Some(
|
||||||
|
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !entity.availability_commitment.is_empty() {
|
||||||
|
active_model.availability_commitment =
|
||||||
|
ActiveValue::Set(Some(entity.availability_commitment));
|
||||||
|
}
|
||||||
|
active_model.mentoring_rate = ActiveValue::Set(Some(entity.mentoring_rate));
|
||||||
|
if !entity.status.is_empty() {
|
||||||
|
active_model.status = ActiveValue::Set(Some(entity.status));
|
||||||
|
}
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let model = MentorsEntity::find_by_id(id)
|
||||||
|
.filter(MentorColumn::IsDeleted.eq(false))
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||||
|
|
||||||
|
let mut active_model: MentorActiveModel = model.into();
|
||||||
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
|
||||||
|
pub use infrastructure::http::routes::{mentors_protected_routes, mentors_public_routes};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod session_service;
|
||||||
|
|
||||||
|
pub use session_service::SessionServiceImpl;
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use crate::sessions::domain::{SessionEntity, SessionRepository, SessionService};
|
||||||
|
use crate::sessions::infrastructure::http::dto::{
|
||||||
|
AvailabilitySlotDto, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||||
|
SessionDetailDto, SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||||
|
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SessionServiceImpl {
|
||||||
|
repo: Arc<dyn SessionRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn SessionRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SessionService for SessionServiceImpl {
|
||||||
|
async fn book_session(
|
||||||
|
&self,
|
||||||
|
mentor_id: String,
|
||||||
|
user_id: String,
|
||||||
|
dto: BookSessionRequestDto,
|
||||||
|
) -> Result<BookSessionResponseDto, AppError> {
|
||||||
|
let scheduled_at = DateTime::parse_from_rfc3339(&dto.scheduled_at)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid scheduled_at format: {}", e)))?
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
|
||||||
|
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||||
|
|
||||||
|
let mentee_uuid = Uuid::parse_str(&user_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||||
|
|
||||||
|
let entity = SessionEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
mentor_id: mentor_uuid,
|
||||||
|
mentee_id: mentee_uuid,
|
||||||
|
topic: dto.topic,
|
||||||
|
description: dto.description,
|
||||||
|
scheduled_at,
|
||||||
|
duration_minutes: dto.duration_minutes.unwrap_or(60),
|
||||||
|
meeting_link: None,
|
||||||
|
session_type: dto.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||||
|
status: "pending".to_string(),
|
||||||
|
feedback: None,
|
||||||
|
rating: None,
|
||||||
|
feedback_submitted_at: None,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let created = self.repo.create(entity).await?;
|
||||||
|
|
||||||
|
Ok(BookSessionResponseDto {
|
||||||
|
id: created.id.to_string(),
|
||||||
|
mentor_id: created.mentor_id.to_string(),
|
||||||
|
mentee_id: created.mentee_id.to_string(),
|
||||||
|
topic: created.topic,
|
||||||
|
description: created.description,
|
||||||
|
scheduled_at: created.scheduled_at.to_rfc3339(),
|
||||||
|
duration_minutes: created.duration_minutes,
|
||||||
|
session_type: created.session_type,
|
||||||
|
status: created.status,
|
||||||
|
created_at: created.created_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mentor_sessions(
|
||||||
|
&self,
|
||||||
|
mentor_id: String,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<SessionListResponseDto, AppError> {
|
||||||
|
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||||
|
|
||||||
|
let count = self
|
||||||
|
.repo
|
||||||
|
.count_by_mentor(mentor_uuid, status_filter.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let sessions = self
|
||||||
|
.repo
|
||||||
|
.find_by_mentor_id(mentor_uuid, status_filter)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let items: Vec<SessionListItemDto> = sessions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| SessionListItemDto {
|
||||||
|
id: s.id.to_string(),
|
||||||
|
mentor_id: s.mentor_id.to_string(),
|
||||||
|
mentee_id: s.mentee_id.to_string(),
|
||||||
|
mentee_fullname: None,
|
||||||
|
mentee_email: None,
|
||||||
|
topic: s.topic,
|
||||||
|
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||||
|
duration_minutes: s.duration_minutes,
|
||||||
|
session_type: s.session_type,
|
||||||
|
status: s.status,
|
||||||
|
rating: s.rating,
|
||||||
|
created_at: s.created_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(SessionListResponseDto {
|
||||||
|
sessions: items,
|
||||||
|
total: count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_sessions(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<SessionListResponseDto, AppError> {
|
||||||
|
let user_uuid = Uuid::parse_str(&user_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||||
|
|
||||||
|
let count = self
|
||||||
|
.repo
|
||||||
|
.count_by_mentee(user_uuid, status_filter.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let sessions = self
|
||||||
|
.repo
|
||||||
|
.find_by_mentee_id(user_uuid, status_filter)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let items: Vec<SessionListItemDto> = sessions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| SessionListItemDto {
|
||||||
|
id: s.id.to_string(),
|
||||||
|
mentor_id: s.mentor_id.to_string(),
|
||||||
|
mentee_id: s.mentee_id.to_string(),
|
||||||
|
mentee_fullname: None,
|
||||||
|
mentee_email: None,
|
||||||
|
topic: s.topic,
|
||||||
|
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||||
|
duration_minutes: s.duration_minutes,
|
||||||
|
session_type: s.session_type,
|
||||||
|
status: s.status,
|
||||||
|
rating: s.rating,
|
||||||
|
created_at: s.created_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(SessionListResponseDto {
|
||||||
|
sessions: items,
|
||||||
|
total: count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mentor_availability(
|
||||||
|
&self,
|
||||||
|
mentor_id: String,
|
||||||
|
) -> Result<MentorAvailabilityDto, AppError> {
|
||||||
|
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||||
|
|
||||||
|
let booked_dates = self.repo.find_booked_dates(mentor_uuid).await?;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
for hour in 9..17 {
|
||||||
|
let time_str = format!("{:02}:00", hour);
|
||||||
|
let datetime_prefix = format!("{}T{}", date_str, time_str);
|
||||||
|
|
||||||
|
let is_booked = booked_dates
|
||||||
|
.iter()
|
||||||
|
.any(|d| d.starts_with(&datetime_prefix[..13]));
|
||||||
|
|
||||||
|
slots.push(AvailabilitySlotDto {
|
||||||
|
date: date_str.clone(),
|
||||||
|
time: time_str,
|
||||||
|
available: !is_booked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_session_status(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
_user_id: String,
|
||||||
|
dto: UpdateSessionStatusRequestDto,
|
||||||
|
) -> Result<UpdateSessionStatusResponseDto, AppError> {
|
||||||
|
let session_uuid = Uuid::parse_str(&session_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||||
|
|
||||||
|
let mut session = self
|
||||||
|
.repo
|
||||||
|
.find_by_id(session_uuid)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||||
|
|
||||||
|
session.status = dto.status;
|
||||||
|
if let Some(link) = dto.meeting_link {
|
||||||
|
session.meeting_link = Some(link);
|
||||||
|
}
|
||||||
|
session.updated_at = Utc::now();
|
||||||
|
|
||||||
|
let updated = self.repo.update(session_uuid, session).await?;
|
||||||
|
|
||||||
|
Ok(UpdateSessionStatusResponseDto {
|
||||||
|
id: updated.id.to_string(),
|
||||||
|
status: updated.status,
|
||||||
|
meeting_link: updated.meeting_link,
|
||||||
|
updated_at: updated.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_feedback(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
user_id: String,
|
||||||
|
dto: SessionFeedbackRequestDto,
|
||||||
|
) -> Result<SessionFeedbackResponseDto, AppError> {
|
||||||
|
let session_uuid = Uuid::parse_str(&session_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||||
|
|
||||||
|
let mut session = self
|
||||||
|
.repo
|
||||||
|
.find_by_id(session_uuid)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||||
|
|
||||||
|
if session.mentee_id.to_string() != user_id {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Only the mentee can submit feedback".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.status != "completed" {
|
||||||
|
return Err(AppError::BadRequestError(
|
||||||
|
"Feedback can only be submitted for completed sessions".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
session.feedback = Some(dto.feedback.clone());
|
||||||
|
session.rating = Some(dto.rating);
|
||||||
|
session.feedback_submitted_at = Some(Utc::now());
|
||||||
|
session.updated_at = Utc::now();
|
||||||
|
|
||||||
|
let updated = self.repo.update(session_uuid, session).await?;
|
||||||
|
let submitted_at = updated
|
||||||
|
.feedback_submitted_at
|
||||||
|
.unwrap_or_else(Utc::now)
|
||||||
|
.to_rfc3339();
|
||||||
|
|
||||||
|
Ok(SessionFeedbackResponseDto {
|
||||||
|
id: updated.id.to_string(),
|
||||||
|
feedback: dto.feedback,
|
||||||
|
rating: dto.rating,
|
||||||
|
submitted_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_session_detail(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
) -> Result<SessionDetailDto, AppError> {
|
||||||
|
let session_uuid = Uuid::parse_str(&session_id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||||
|
|
||||||
|
let session = self
|
||||||
|
.repo
|
||||||
|
.find_by_id(session_uuid)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(SessionDetailDto {
|
||||||
|
id: session.id.to_string(),
|
||||||
|
mentor_id: session.mentor_id.to_string(),
|
||||||
|
mentor_fullname: None,
|
||||||
|
mentee_id: session.mentee_id.to_string(),
|
||||||
|
mentee_fullname: None,
|
||||||
|
topic: session.topic,
|
||||||
|
description: session.description,
|
||||||
|
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||||
|
duration_minutes: session.duration_minutes,
|
||||||
|
meeting_link: session.meeting_link,
|
||||||
|
session_type: session.session_type,
|
||||||
|
status: session.status,
|
||||||
|
feedback: session.feedback,
|
||||||
|
rating: session.rating,
|
||||||
|
feedback_submitted_at: session.feedback_submitted_at.map(|dt| dt.to_rfc3339()),
|
||||||
|
created_at: session.created_at.to_rfc3339(),
|
||||||
|
updated_at: session.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
pub mod session;
|
||||||
|
|
||||||
|
pub use repository::SessionRepository;
|
||||||
|
pub use service::SessionService;
|
||||||
|
pub use session::SessionEntity;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use super::session::SessionEntity;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait SessionRepository: Send + Sync {
|
||||||
|
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError>;
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError>;
|
||||||
|
|
||||||
|
async fn find_by_mentor_id(
|
||||||
|
&self,
|
||||||
|
mentor_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<Vec<SessionEntity>, AppError>;
|
||||||
|
|
||||||
|
async fn find_by_mentee_id(
|
||||||
|
&self,
|
||||||
|
mentee_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<Vec<SessionEntity>, AppError>;
|
||||||
|
|
||||||
|
async fn find_booked_dates(&self, mentor_id: Uuid) -> Result<Vec<String>, AppError>;
|
||||||
|
|
||||||
|
async fn update(&self, id: Uuid, entity: SessionEntity) -> Result<SessionEntity, AppError>;
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
|
||||||
|
async fn count_by_mentor(
|
||||||
|
&self,
|
||||||
|
mentor_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<usize, AppError>;
|
||||||
|
|
||||||
|
async fn count_by_mentee(
|
||||||
|
&self,
|
||||||
|
mentee_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<usize, AppError>;
|
||||||
|
|
||||||
|
async fn find_all_paginated(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<SessionEntity>, AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use crate::sessions::infrastructure::http::dto::{
|
||||||
|
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||||
|
SessionDetailDto, SessionFeedbackRequestDto, SessionFeedbackResponseDto,
|
||||||
|
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait SessionService: Send + Sync {
|
||||||
|
async fn book_session(
|
||||||
|
&self,
|
||||||
|
mentor_id: String,
|
||||||
|
user_id: String,
|
||||||
|
dto: BookSessionRequestDto,
|
||||||
|
) -> Result<BookSessionResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn get_mentor_sessions(
|
||||||
|
&self,
|
||||||
|
mentor_id: String,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<SessionListResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn get_user_sessions(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<SessionListResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn get_mentor_availability(
|
||||||
|
&self,
|
||||||
|
mentor_id: String,
|
||||||
|
) -> Result<MentorAvailabilityDto, AppError>;
|
||||||
|
|
||||||
|
async fn update_session_status(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
user_id: String,
|
||||||
|
dto: UpdateSessionStatusRequestDto,
|
||||||
|
) -> Result<UpdateSessionStatusResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn submit_feedback(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
user_id: String,
|
||||||
|
dto: SessionFeedbackRequestDto,
|
||||||
|
) -> Result<SessionFeedbackResponseDto, AppError>;
|
||||||
|
|
||||||
|
async fn get_session_detail(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
) -> Result<SessionDetailDto, AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SessionEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub mentor_id: Uuid,
|
||||||
|
pub mentee_id: Uuid,
|
||||||
|
pub topic: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub scheduled_at: DateTime<Utc>,
|
||||||
|
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<DateTime<Utc>>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use imphnen_libs::ZodValidate;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use zod_rs::prelude::*;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Request DTOs
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct BookSessionRequestDto {
|
||||||
|
#[zod(min_length(3), max_length(200))]
|
||||||
|
pub topic: String,
|
||||||
|
#[zod(max_length(1000))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[zod(min_length(1))]
|
||||||
|
pub scheduled_at: String,
|
||||||
|
#[zod(min(15.0), max(240.0), int)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub duration_minutes: Option<i32>,
|
||||||
|
#[zod(max_length(50))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session_type: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for BookSessionRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct UpdateSessionStatusRequestDto {
|
||||||
|
#[zod(min_length(1), max_length(50))]
|
||||||
|
pub status: String,
|
||||||
|
#[zod(url)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub meeting_link: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for UpdateSessionStatusRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
|
pub struct SessionFeedbackRequestDto {
|
||||||
|
#[zod(min_length(10), max_length(2000))]
|
||||||
|
pub feedback: String,
|
||||||
|
#[zod(min(1.0), max(5.0), int)]
|
||||||
|
pub rating: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for SessionFeedbackRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Response DTOs
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AvailabilitySlotDto {
|
||||||
|
pub date: String,
|
||||||
|
pub time: String,
|
||||||
|
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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateSessionStatusResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub status: String,
|
||||||
|
pub meeting_link: Option<String>,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SessionFeedbackResponseDto {
|
||||||
|
pub id: String,
|
||||||
|
pub feedback: String,
|
||||||
|
pub rating: i32,
|
||||||
|
pub submitted_at: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{
|
||||||
|
extract::{Extension, Path, Query},
|
||||||
|
http::HeaderMap,
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use imphnen_libs::ValidatedJson;
|
||||||
|
use imphnen_utils::{ApiSuccess, extract_email};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use crate::sessions::domain::SessionService;
|
||||||
|
use super::dto::{
|
||||||
|
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||||
|
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
|
||||||
|
UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SessionStatusFilter {
|
||||||
|
pub status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/mentors/{id}/sessions/create",
|
||||||
|
tag = "sessions",
|
||||||
|
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(service): Extension<Arc<dyn SessionService>>,
|
||||||
|
Path(mentor_id): Path<String>,
|
||||||
|
ValidatedJson(dto): ValidatedJson<BookSessionRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let user_email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let resp = service.book_session(mentor_id, user_email, dto).await?;
|
||||||
|
Ok(ApiSuccess(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors/{id}/sessions",
|
||||||
|
tag = "sessions",
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Mentor ID"),
|
||||||
|
("status" = Option<String>, Query, description = "Filter by status"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 404, description = "Mentor not found"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_mentor_sessions(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||||
|
Path(mentor_id): Path<String>,
|
||||||
|
Query(filter): Query<SessionStatusFilter>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let _user_email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let resp = service.get_mentor_sessions(mentor_id, filter.status).await?;
|
||||||
|
Ok(ApiSuccess(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/mentors/{id}/availability",
|
||||||
|
tag = "sessions",
|
||||||
|
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(service): Extension<Arc<dyn SessionService>>,
|
||||||
|
Path(mentor_id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let resp = service.get_mentor_availability(mentor_id).await?;
|
||||||
|
Ok(ApiSuccess(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/sessions/update/{id}/status",
|
||||||
|
tag = "sessions",
|
||||||
|
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 = 404, description = "Session not found"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn put_update_session_status(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||||
|
Path(session_id): Path<String>,
|
||||||
|
ValidatedJson(dto): ValidatedJson<UpdateSessionStatusRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let user_email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let resp = service.update_session_status(session_id, user_email, dto).await?;
|
||||||
|
Ok(ApiSuccess(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/sessions/{id}/feedback/create",
|
||||||
|
tag = "sessions",
|
||||||
|
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(service): Extension<Arc<dyn SessionService>>,
|
||||||
|
Path(session_id): Path<String>,
|
||||||
|
ValidatedJson(dto): ValidatedJson<SessionFeedbackRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let user_email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let resp = service.submit_feedback(session_id, user_email, dto).await?;
|
||||||
|
Ok(ApiSuccess(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/users/me/sessions",
|
||||||
|
tag = "sessions",
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
params(
|
||||||
|
("status" = Option<String>, Query, description = "Filter by status"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_my_sessions(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||||
|
Query(filter): Query<SessionStatusFilter>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let user_email = extract_email(&headers)
|
||||||
|
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||||
|
let resp = service.get_user_sessions(user_email, filter.status).await?;
|
||||||
|
Ok(ApiSuccess(resp))
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::{sessions_protected_routes, sessions_public_routes};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{
|
||||||
|
routing::{get, post, put},
|
||||||
|
Extension, Router,
|
||||||
|
};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use imphnen_libs::AppState;
|
||||||
|
use crate::sessions::application::SessionServiceImpl;
|
||||||
|
use crate::sessions::domain::SessionService;
|
||||||
|
use crate::sessions::infrastructure::persistence::PostgresSessionRepository;
|
||||||
|
use super::handlers::{
|
||||||
|
get_mentor_availability, get_mentor_sessions, get_my_sessions, post_book_session,
|
||||||
|
post_submit_feedback, put_update_session_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn build_service(db: DatabaseConnection) -> Arc<dyn SessionService> {
|
||||||
|
let repo = Arc::new(PostgresSessionRepository::new(db));
|
||||||
|
Arc::new(SessionServiceImpl::new(repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sessions_public_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/mentors/{id}/availability", get(get_mentor_availability))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sessions_protected_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/mentors/{id}/sessions/create", post(post_book_session))
|
||||||
|
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
|
||||||
|
.route("/sessions/update/{id}/status", put(put_update_session_status))
|
||||||
|
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
|
||||||
|
.route("/users/me/sessions", get(get_my_sessions))
|
||||||
|
.layer(Extension(service))
|
||||||
|
.layer(Extension((*state).clone()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod postgres_session_repository;
|
||||||
|
|
||||||
|
pub use postgres_session_repository::PostgresSessionRepository;
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, IntoActiveModel, Order, QueryOrder, PaginatorTrait};
|
||||||
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_entities::seaorm::auth::sessions::{
|
||||||
|
Entity as SessionsEntity,
|
||||||
|
Column as SessionColumn,
|
||||||
|
ActiveModel as SessionActiveModel,
|
||||||
|
Model as SessionModel,
|
||||||
|
};
|
||||||
|
use crate::sessions::domain::{session::SessionEntity, repository::SessionRepository};
|
||||||
|
|
||||||
|
fn model_to_entity(model: SessionModel) -> SessionEntity {
|
||||||
|
SessionEntity {
|
||||||
|
id: model.id,
|
||||||
|
mentor_id: model.mentor_id,
|
||||||
|
mentee_id: model.mentee_id,
|
||||||
|
topic: model.topic,
|
||||||
|
description: model.description,
|
||||||
|
scheduled_at: model.scheduled_at,
|
||||||
|
duration_minutes: model.duration_minutes,
|
||||||
|
meeting_link: model.meeting_link,
|
||||||
|
session_type: model.session_type,
|
||||||
|
status: model.status,
|
||||||
|
feedback: model.feedback,
|
||||||
|
rating: model.rating,
|
||||||
|
feedback_submitted_at: model.feedback_submitted_at,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresSessionRepository {
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresSessionRepository {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db: Arc::new(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SessionRepository for PostgresSessionRepository {
|
||||||
|
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError> {
|
||||||
|
let active_model = SessionActiveModel {
|
||||||
|
id: ActiveValue::Set(entity.id),
|
||||||
|
mentor_id: ActiveValue::Set(entity.mentor_id),
|
||||||
|
mentee_id: ActiveValue::Set(entity.mentee_id),
|
||||||
|
topic: ActiveValue::Set(entity.topic.clone()),
|
||||||
|
description: ActiveValue::Set(entity.description.clone()),
|
||||||
|
scheduled_at: ActiveValue::Set(entity.scheduled_at),
|
||||||
|
duration_minutes: ActiveValue::Set(entity.duration_minutes),
|
||||||
|
meeting_link: ActiveValue::Set(entity.meeting_link.clone()),
|
||||||
|
session_type: ActiveValue::Set(entity.session_type.clone()),
|
||||||
|
status: ActiveValue::Set(entity.status.clone()),
|
||||||
|
feedback: ActiveValue::Set(entity.feedback.clone()),
|
||||||
|
rating: ActiveValue::Set(entity.rating),
|
||||||
|
feedback_submitted_at: ActiveValue::Set(entity.feedback_submitted_at),
|
||||||
|
created_at: ActiveValue::Set(entity.created_at),
|
||||||
|
updated_at: ActiveValue::Set(entity.updated_at),
|
||||||
|
};
|
||||||
|
|
||||||
|
let model: SessionModel = active_model
|
||||||
|
.insert(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(model_to_entity(model))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError> {
|
||||||
|
let model = SessionsEntity::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(model.map(model_to_entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_mentor_id(
|
||||||
|
&self,
|
||||||
|
mentor_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<Vec<SessionEntity>, AppError> {
|
||||||
|
let mut query = SessionsEntity::find()
|
||||||
|
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||||
|
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||||
|
|
||||||
|
if let Some(status) = status_filter {
|
||||||
|
query = query.filter(SessionColumn::Status.eq(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
let models = query
|
||||||
|
.all(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(models.into_iter().map(model_to_entity).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_mentee_id(
|
||||||
|
&self,
|
||||||
|
mentee_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<Vec<SessionEntity>, AppError> {
|
||||||
|
let mut query = SessionsEntity::find()
|
||||||
|
.filter(SessionColumn::MenteeId.eq(mentee_id))
|
||||||
|
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||||
|
|
||||||
|
if let Some(status) = status_filter {
|
||||||
|
query = query.filter(SessionColumn::Status.eq(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
let models = query
|
||||||
|
.all(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(models.into_iter().map(model_to_entity).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_booked_dates(&self, mentor_id: Uuid) -> Result<Vec<String>, AppError> {
|
||||||
|
let sessions = SessionsEntity::find()
|
||||||
|
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||||
|
.filter(SessionColumn::Status.is_in(["pending", "confirmed"]))
|
||||||
|
.order_by(SessionColumn::ScheduledAt, Order::Asc)
|
||||||
|
.all(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(sessions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.scheduled_at.to_rfc3339())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, id: Uuid, entity: SessionEntity) -> Result<SessionEntity, AppError> {
|
||||||
|
let model = SessionsEntity::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||||
|
|
||||||
|
let mut active_model = model.into_active_model();
|
||||||
|
|
||||||
|
active_model.topic = ActiveValue::Set(entity.topic);
|
||||||
|
active_model.description = ActiveValue::Set(entity.description);
|
||||||
|
active_model.scheduled_at = ActiveValue::Set(entity.scheduled_at);
|
||||||
|
active_model.duration_minutes = ActiveValue::Set(entity.duration_minutes);
|
||||||
|
active_model.meeting_link = ActiveValue::Set(entity.meeting_link);
|
||||||
|
active_model.session_type = ActiveValue::Set(entity.session_type);
|
||||||
|
active_model.status = ActiveValue::Set(entity.status);
|
||||||
|
active_model.feedback = ActiveValue::Set(entity.feedback);
|
||||||
|
active_model.rating = ActiveValue::Set(entity.rating);
|
||||||
|
active_model.feedback_submitted_at = ActiveValue::Set(entity.feedback_submitted_at);
|
||||||
|
active_model.updated_at = ActiveValue::Set(entity.updated_at);
|
||||||
|
|
||||||
|
let updated: SessionModel = active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(model_to_entity(updated))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let model = SessionsEntity::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||||
|
|
||||||
|
model
|
||||||
|
.into_active_model()
|
||||||
|
.delete(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn count_by_mentor(
|
||||||
|
&self,
|
||||||
|
mentor_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<usize, AppError> {
|
||||||
|
let mut query = SessionsEntity::find()
|
||||||
|
.filter(SessionColumn::MentorId.eq(mentor_id));
|
||||||
|
|
||||||
|
if let Some(status) = status_filter {
|
||||||
|
query = query.filter(SessionColumn::Status.eq(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = query
|
||||||
|
.count(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(count as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn count_by_mentee(
|
||||||
|
&self,
|
||||||
|
mentee_id: Uuid,
|
||||||
|
status_filter: Option<String>,
|
||||||
|
) -> Result<usize, AppError> {
|
||||||
|
let mut query = SessionsEntity::find()
|
||||||
|
.filter(SessionColumn::MenteeId.eq(mentee_id));
|
||||||
|
|
||||||
|
if let Some(status) = status_filter {
|
||||||
|
query = query.filter(SessionColumn::Status.eq(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = query
|
||||||
|
.count(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(count as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_all_paginated(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<SessionEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
|
let query = match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => SessionsEntity::find()
|
||||||
|
.order_by(SessionColumn::CreatedAt, Order::Asc),
|
||||||
|
_ => SessionsEntity::find()
|
||||||
|
.order_by(SessionColumn::CreatedAt, Order::Desc),
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
|
let total = paginator
|
||||||
|
.num_items()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let sessions = paginator
|
||||||
|
.fetch_page((page - 1) as u64)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let data = sessions.into_iter().map(model_to_entity).collect();
|
||||||
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
|
Ok(PaginatorResponse { data, meta })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
|
||||||
|
pub use infrastructure::http::routes::{sessions_protected_routes, sessions_public_routes};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-entities"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
sea-orm.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
imphnen-macros.workspace = true
|
||||||
|
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "uuid", "chrono", "json"] }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
use crate::seaorm::common::audit_log;
|
||||||
|
|
||||||
|
pub type AuditLogSchema = audit_log::Model;
|
||||||
@@ -1,37 +1,35 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::{IntoParams, ToSchema};
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct MessageResponseDto {
|
pub struct MessageResponseDto {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoParams)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct MetaRequestDto {
|
pub struct MetaResponseDto {
|
||||||
pub page: Option<u64>,
|
pub page: Option<u64>,
|
||||||
pub per_page: Option<u64>,
|
pub per_page: Option<u64>,
|
||||||
pub search: Option<String>,
|
pub total: Option<u64>,
|
||||||
pub sort_by: Option<String>,
|
}
|
||||||
pub order: Option<String>,
|
|
||||||
pub filter: Option<String>,
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub filter_by: Option<String>,
|
pub struct ResponseSuccessDto<T: Serialize> {
|
||||||
}
|
pub data: T,
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoParams)]
|
|
||||||
pub struct MetaResponseDto {
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub page: Option<u64>,
|
pub struct ResponseListSuccessDto<T: Serialize> {
|
||||||
pub per_page: Option<u64>,
|
pub data: T,
|
||||||
pub total: Option<u64>,
|
pub meta: Option<MetaResponseDto>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
|
||||||
pub struct ResponseSuccessDto<T: Serialize> {
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub data: T,
|
pub struct ErrorDto {
|
||||||
}
|
pub status: u16,
|
||||||
|
pub message: String,
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
pub details: Option<serde_json::Value>,
|
||||||
pub struct ResponseListSuccessDto<T: Serialize> {
|
}
|
||||||
pub data: T,
|
|
||||||
pub meta: Option<MetaResponseDto>,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
pub mod error {
|
||||||
|
use axum::Json;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::response::Response;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("database error: {0}")]
|
||||||
|
Db(String),
|
||||||
|
#[error("anyhow error: {0}")]
|
||||||
|
Anyhow(#[from] anyhow::Error),
|
||||||
|
#[error("HTTP status code error: {0}")]
|
||||||
|
StatusCode(StatusCode),
|
||||||
|
#[error("authentication error: {0}")]
|
||||||
|
Auth(String),
|
||||||
|
#[error("validation error: {0}")]
|
||||||
|
Validation(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, error_message) = match self {
|
||||||
|
Error::Db(detail) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Database error: {detail}"),
|
||||||
|
),
|
||||||
|
Error::Anyhow(detail) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Internal server error: {detail}"),
|
||||||
|
),
|
||||||
|
Error::StatusCode(s) => (s, format!("HTTP error: {s}")),
|
||||||
|
Error::Auth(detail) => (
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
format!("Authentication error: {detail}"),
|
||||||
|
),
|
||||||
|
Error::Validation(detail) => (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("Validation error: {detail}"),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
(status, Json(error_message)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StatusCode> for Error {
|
||||||
|
fn from(status: StatusCode) -> Self {
|
||||||
|
Self::StatusCode(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user