Compare commits

...
Author SHA1 Message Date
MythEclipse 0b51a56dee feat: Enhance CORS middleware to dynamically include development port and allow OPTIONS method 2025-08-17 16:06:21 +07:00
MythEclipse cb98440b65 feat: Refactor update_user_avatar method to be static and enhance avatar deletion logic 2025-08-17 15:22:15 +07:00
MythEclipse 3c19d262f3 feat: Increase CSRF token maximum age to 30 minutes and enhance logging for token generation 2025-08-17 15:08:58 +07:00
MythEclipse bdffe85544 Update dependencies and refactor OAuth service
- Updated various dependencies in Cargo.toml to their latest versions for improved performance and security.
- Refactored the Google OAuth service to streamline the creation of the OAuth client and improve code readability.
- Changed the way WebSocket messages are sent in clear_db_test.rs to use `into()` for better type handling.
- Enhanced error handling and logging in the Google OAuth callback method.
2025-08-16 19:10:27 +07:00
MythEclipse 9ed619e3fd feat: Optimize password verification by using blocking tasks for improved performance 2025-08-16 13:47:29 +07:00
MythEclipse 00fd7e1907 feat: Refactor password hashing and JWT encoding to use static keys for improved performance and security 2025-08-16 13:43:18 +07:00
MythEclipse b70126734e feat: Refactor password reset flow to use async processing and improve token validation 2025-08-16 13:38:35 +07:00
MythEclipse 6af9461079 a 2025-08-16 13:22:39 +07:00
MythEclipse 57b3662951 feat: Refactor auth service and middleware for improved user data handling and caching 2025-08-16 13:18:55 +07:00
MythEclipse c0c08f2821 Enhance Users Service and DTOs
- Added Default trait to UsersDetailItemDto for easier instantiation.
- Refactored UsersServiceTrait to use Pin<Box<dyn Future<...>> for async functions, improving compatibility with async/await.
- Updated all service methods to return futures instead of using async_trait.
- Improved error handling and logging in upload_file method, ensuring proper handling of multipart data.
- Masked sensitive information in Env struct's Debug implementation for better security in logs.
- Cleaned up Cargo.toml by removing unnecessary async-trait workspace dependency.
2025-08-16 12:48:24 +07:00
MythEclipse 8402415649 feat: Enhance user permissions and update event handling with improved response structures 2025-08-15 00:32:15 +07:00
MythEclipse 01e20baa45 feat: Add axum-extra dependency and refactor auth middleware for improved token handling 2025-08-14 23:16:41 +07:00
MythEclipse 10dc869eaf feat: Enhance permissions and roles management with improved indexing and state handling 2025-08-14 23:11:44 +07:00
MythEclipse 199d0c885e feat: Add axum-extra dependency and implement typed headers
feat: Define unique index on users table for email and fix minor syntax error

refactor: Update permissions_guard to use claims from JWT and improve user retrieval

refactor: Modify user-related service methods to accept user details directly

fix: Update token generation functions to include user ID and permissions

test: Update Google OAuth flow tests to reflect changes in token generation
2025-08-14 22:46:42 +07:00
MythEclipse 89ef48a57e feat: Update permissions for user and gacha claim access 2025-08-14 21:38:47 +07:00
MythEclipse da1826ebba feat: Add file upload with deduplication to MinIO service 2025-08-14 17:03:23 +07:00
MythEclipse 874986f1d6 feat: Implement file upload functionality with MinIO integration
- Added new dependencies for HMAC, hex, and urlencoding in Cargo.toml.
- Introduced FileUploadSchema for handling multipart file uploads.
- Enhanced UsersService with upload_file method to handle file uploads to MinIO.
- Implemented MinioService for managing MinIO interactions, including file uploads and presigned URL generation.
- Updated environment configuration to include MinIO region and secure settings.
- Added validation for file types and sizes during upload.
- Improved error handling and logging for file upload processes.
- Created utility functions for base64 decoding and content type extraction.
2025-08-14 16:53:14 +07:00
MythEclipse bbff555a06 feat(docs): add upload_file endpoint to OpenAPI documentation 2025-08-14 14:16:07 +07:00
Asep Haryana Saputra d3eb687a58 Merge branch 'feat/auth-google' of https://github.com/IMPHNEN/imphnen-backend-service into feat/auth-google 2025-08-14 06:55:26 +00:00
Asep Haryana Saputra c5516d4006 a 2025-08-14 06:54:56 +00:00
MythEclipse 5f51dd281d feat: update dependencies and add MinIO integration
- Updated `async-channel` to version 2.5.0 and added new dependencies in `Cargo.lock`.
- Introduced `minio` crate for file upload functionality.
- Added `career_status` field to user-related DTOs and schemas.
- Implemented file upload endpoint in `users_controller.rs` with multipart support.
- Created `MinioService` for handling file uploads to MinIO.
- Updated user seeding and test cases to accommodate new `career_status` field.
- Refactored permissions guard to return user details.
2025-08-14 13:16:39 +07:00
MythEclipse f6232832af feat(users): Enhance user schema and DTOs with additional fields for website, social links, location, skills, experience, and education 2025-08-13 20:40:02 +07:00
MythEclipse 277cf0770d feat(auth): Add GOOGLE_REDIRECT_URL to .env.example for Google OAuth callback 2025-08-13 01:29:41 +07:00
MythEclipse 650728a385 feat(auth): Update Google OAuth service to support custom redirect URIs 2025-08-12 23:21:34 +07:00
MythEclipse 3cc3e2a7d8 feat(auth): Update Google OAuth flow to support custom redirect URIs 2025-08-12 22:06:26 +07:00
MythEclipse 39fc2f86ad feat(users): Include avatar field in user creation from DTO 2025-08-12 19:50:52 +07:00
Asep Haryana Saputra 7bd0ef2274 a 2025-08-12 12:41:16 +00:00
MythEclipse b40a430c49 feat(auth): Enhance Google OAuth flow with async email extraction and caching 2025-08-12 19:25:10 +07:00
MythEclipse 44b1e09551 fix(users): Change meta parameter extraction from Json to Query in get_user_list function 2025-08-12 18:53:11 +07:00
MythEclipse 1b9251b2dd Refactor mentor and user schemas to centralize personal data management
- Updated `seed_mentor_user.rs` to include additional fields in the `app_users` creation and removed redundant fields from `app_mentors`.
- Modified `seed_users.rs` to initialize new user fields including legal name, domicile, and professional links.
- Adjusted `mentors_dto.rs` to remove personal data fields from `MentorDetailWithUserDto` and `MentorDetailResponseDto`, now sourced from `UsersSchema`.
- Updated `mentors_repository.rs` to search for user data through `user_id` instead of mentor fields.
- Refined `mentors_schema.rs` to remove personal data fields, now managed in `UsersSchema`.
- Enhanced `mentors_service.rs` to populate mentor responses with user data from `UsersSchema`.
- Removed identity document URL and related fields from various DTOs and schemas across the codebase.
- Updated tests to reflect the removal of identity document URL and ensure compatibility with the new schema structure.
2025-08-12 18:43:30 +07:00
MythEclipse 34b6cdabfb feat(users): Add optional fields to user schemas and update user avatar functionality 2025-08-12 17:31:12 +07:00
MythEclipse a25e896fa2 feat(auth): Enhance Google OAuth integration with PKCE support and CSRF validation improvements 2025-08-12 16:11:15 +07:00
Asep Haryana Saputra adf3ef8e28 a 2025-08-12 08:14:05 +00:00
MythEclipse ad46687f07 refactor(tests): Remove unused imports in CSRF token tests 2025-08-12 01:51:10 +07:00
MythEclipse 0ba04abee9 feat(auth): Enhance Google OAuth service with CSRF protection and validation logic
feat(auth): Add error handling for authentication and validation errors
feat(auth): Implement default role assignment for new users in Google OAuth flow
feat(utils): Introduce CSRF token generation and validation utilities
fix(dependencies): Update Cargo.toml to include base64 and sha2 dependencies
2025-08-12 00:32:59 +07:00
MythEclipse 1e25a5b496 feat(auth): Update Google OAuth integration to enhance response structure and improve callback handling 2025-08-11 23:47:15 +07:00
MythEclipse d7178c792d a 2025-08-11 23:06:45 +07:00
MythEclipse d2754de9ef feat(auth): Refactor Google OAuth service and controller to utilize environment variables and enhance response structure 2025-08-11 22:54:10 +07:00
MythEclipse cda950ed7e feat(auth): Implement Google OAuth 2.0 integration with user creation and JWT generation
- Added Google OAuth controller and service to handle authentication via Google.
- Introduced DTOs for Google user and token responses.
- Updated AuthService and UsersService traits to support new Google OAuth functionality.
- Implemented logic to create a new user if they do not exist in the system after Google authentication.
- Enhanced existing user retrieval and JWT generation upon successful login.
- Added tests for Google OAuth flow, including login redirection and callback handling for both new and existing users.
- Updated environment configuration to include Google OAuth credentials.
2025-08-11 22:31:51 +07:00
MythEclipse c6a95c1231 fix: update default surrealdb password to 'root' for consistency 2025-07-31 20:34:36 +07:00
MythEclipse 86116b8157 feat: add logging for SurrealDB queries across multiple repositories 2025-07-31 16:51:08 +07:00
MythEclipse ac06224ed1 feat: enhance logging and dependency management with dotenvy and tracing-subscriber 2025-07-31 15:42:15 +07:00
MythEclipse 83e765f358 feat: add logger module with initialization function using tracing and dotenvy 2025-07-31 15:15:55 +07:00
MythEclipse 558e0ce568 refactor: simplify environment variable loading with helper function and remove unused mail configuration 2025-07-31 15:12:01 +07:00
MythEclipse 148cf3a5cd refactor: streamline environment variable loading and improve logging 2025-07-31 15:07:18 +07:00
Maulana Sodiqin ee9d23e01b Merge pull request #44 from IMPHNEN/feat/mentor
Add comprehensive tests for mentor repository and authentication
2025-07-21 22:18:14 +07:00
MythEclipse 1a2e0c58b6 Add comprehensive tests for mentor repository and authentication
- Implemented tests for creating, retrieving, updating, and deleting mentors in `mentor_repository_test.rs`.
- Added tests for user authentication, including successful login, invalid email formats, and inactive users in `auth_login_tests.rs`.
- Created a mock test environment setup in `mock_test.rs` to facilitate database operations during tests.
- Updated module structure to include new test files for mentors and authentication.
- Ensured cleanup of the database after tests to maintain isolation and prevent side effects.
2025-07-21 21:29:04 +07:00
Maulana Sodiqin e66f1f1634 chore: remove deprecated deployment 2025-06-30 09:28:11 +07:00
Maulana Sodiqin 6f45cf80c8 Merge pull request #38 from IMPHNEN/feat/client-dynamic-wss-or-ws
refactor: update surrealdb client usage to support 'any' engine type
2025-06-30 09:27:16 +07:00
132 changed files with 12066 additions and 2164 deletions

No files matched your search

+49 -8
View File
@@ -1,8 +1,49 @@
PORT= # RUST_ENV : Mode environment aplikasi (misal: development, production)
SURREALDB_URL= # RUST_LOG : Level logging aplikasi Rust (misal: debug, info, warn, error)
SURREALDB_USERNAME= # PORT : Port yang digunakan backend service
SURREALDB_PASSWORD= # SURREALDB_URL : URL koneksi ke SurrealDB (gunakan ws:// untuk WebSocket)
SURREALDB_NAMESPACE= # SURREALDB_USERNAME : Username untuk SurrealDB
SURREALDB_DBNAME= # SURREALDB_PASSWORD : Password untuk SurrealDB
ACCESS_TOKEN_SECRET= # SURREALDB_NAMESPACE : Namespace SurrealDB yang digunakan
REFRESH_TOKEN_SECRET= # SURREALDB_DBNAME : Nama database SurrealDB
# ACCESS_TOKEN_SECRET : Secret key untuk JWT access token (ganti dengan nilai rahasia)
# REFRESH_TOKEN_SECRET : Secret key untuk JWT refresh token (ganti dengan nilai rahasia)
# SMTP_EMAIL : Email yang digunakan untuk mengirim email (SMTP)
# SMTP_PASSWORD : Password email SMTP
# SMTP_NAME : Nama pengirim email (misal: nama aplikasi)
# SMTP_HOST : Host SMTP server (misal: smtp.gmail.com)
# REDISDB_URL : URL/host Redis database
# FE_URL : URL frontend aplikasi (misal: http://localhost)
# MINIO_ENDPOINT : Endpoint MinIO object storage
# MINIO_BUCKET_NAME : Nama bucket default MinIO
# MINIO_ACCESS_KEY : Access key MinIO atau MINIO_ROOT_USER
# MINIO_SECRET_KEY : Secret key MinIO atau MINIO_ROOT_PASSWORD
# MINIO_ROOT_USER : Username root MinIO (default: minioadmin)
# MINIO_ROOT_PASSWORD : Password root MinIO (default: minioadmin)
# GOOGLE_CLIENT_ID : Client ID OAuth Google (untuk login Google)
# GOOGLE_CLIENT_SECRET : Client Secret OAuth Google
RUST_ENV=development
RUST_LOG=debug
PORT=4099
SURREALDB_URL=ws://localhost:8000/rpc
SURREALDB_USERNAME=root
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"
-65
View File
@@ -1,65 +0,0 @@
name: Deploy
on:
push:
branches:
- develop
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Build the project
run: cargo build --release
- name: Stop service on VPS before upload
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
script: |
set -e
echo "Stopping the service before uploading the binary"
sudo systemctl stop imphnen-backend-service
- name: Upload artifact to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
source: ./target/release/*
target: /opt/imphnen-backend-service/imphnen-backend-service
rm: true
overwrite: true
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
script: |
set -e
echo "Restarting the service"
sudo systemctl daemon-reload
sudo systemctl restart imphnen-backend-service
echo "Deployment completed successfully"
Generated
+1112 -667
View File
File diff suppressed because it is too large. Load diff
+54 -16
View File
@@ -16,32 +16,70 @@ members = [
[workspace.dependencies] [workspace.dependencies]
oauth2 = "5.0.0"
reqwest = { version = "0.12.23", features = ["json"] }
serde_json = "1.0.142"
axum = { version = "0.8.4", features = ["multipart"] } axum = { version = "0.8.4", features = ["multipart"] }
log = "0.4.25" log = "0.4.27"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.138" tokio = { version = "1.47.1", features = ["full"] }
tokio = { version = "1.45.0" }
argon2 = { version = "0.5.3", features = ["password-hash"] } argon2 = { version = "0.5.3", features = ["password-hash"] }
jsonwebtoken = "9.3.1" jsonwebtoken = "9.3.1"
chrono = "0.4.41" chrono = "0.4.41"
utoipa = { version = "5.3.1", features = ["axum_extras"] } utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] } lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
surrealdb = { version = "2.3.2", features = ["kv-mem"] } surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
thiserror = "2.0.12" thiserror = "2.0.14"
anyhow = "1.0.98" anyhow = "1.0.99"
rand = { version = "0.9.1", features = ["std", "alloc"] } rand = { version = "0.9.2", features = ["std", "alloc"] }
rand_distr = "0.5.1" rand_distr = "0.5.1"
tower-http = { version = "0.6.4", features = ["cors"] } tower-http = { version = "0.6.6", features = ["cors", "trace"] }
validator = { version = "0.12", features = ["derive"] } http-body-util = "0.1.3"
lazy_static = "1.4.0" validator = { version = "0.20.0", features = ["derive"] }
lazy_static = "1.5.0"
regex = "1.11.1" regex = "1.11.1"
axum-test = "17.2.0" axum-test = "17.3.0"
fancy-regex = "0.14.0" axum-extra = { version = "0.10.1", features = ["typed-header"] }
fancy-regex = "0.16.1"
futures = "0.3.31" futures = "0.3.31"
tower = "0.5.2" tower = "0.5.2"
env_logger = "0.11.8" env_logger = "0.11.8"
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"
tokio-test = "0.4.4"
mockall = "0.13.1"
once_cell = "1.21.3"
dotenvy = "0.15.7" dotenvy = "0.15.7"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tokio-tungstenite = "0.27"
url = "2.5"
futures-util = "0.3"
http = "1.3"
tests = { path = "./tests" }
imphnen-iam = { path = "./imphnen-iam" }
imphnen-cms = { path = "./imphnen-cms" }
imphnen-libs = { path = "./imphnen-libs" }
imphnen-utils = { path = "./imphnen-utils" }
imphnen-gacha = { path = "./imphnen-gacha" }
imphnen-gateway = { path = "./imphnen-gateway" }
imphnen-backend = { path = "./imphnen-backend" }
imphnen-entities = { path = "./imphnen-entities" }
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
imphnen-middleware = { path = "./imphnen-middleware" }
[profile.release] [profile.release]
lto = "fat" lto = "fat"
codegen-units = 1 codegen-units = 1
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.85-alpine AS builder FROM rust:1.86-alpine AS builder
RUN apk add --no-cache \ RUN apk add --no-cache \
curl \ curl \
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Script deploy ke VPS
set -e
REMOTE_USER="asephs"
REMOTE_HOST="70.153.9.42"
REMOTE_PATH="/home/asephs/imphnen-backend-service"
# Build project
taskset -c 0,1 cargo build --release -j 2
# Rsync hasil build dan file yang diperlukan
rsync -avz --delete \
target/release/ \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
# Sync file konfigurasi dan source code (jika perlu)
rsync -avz --delete \
imphnen-backend/ \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/imphnen-backend/
rsync -avz --delete \
docker-compose.yml Dockerfile \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/
# Tambahkan file lain jika diperlukan
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
cd /home/asephs/imphnen-backend-service
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
export PATH=$PATH:/root/.local/share/pnpm
export PATH=$PATH:/home/asephs/.nvm/versions/node/v22.17.1/bin
export PATH=$PATH:/home/asephs/.bun/bin/bun
if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi
pm2 restart 4 --update-env
EOF
echo "Deploy selesai ke $REMOTE_HOST:$REMOTE_PATH"
+16
View File
@@ -14,3 +14,19 @@ services:
command: start --log trace --user root --pass root command: start --log trace --user root --pass root
ports: ports:
- "8000:8000" - "8000:8000"
minio:
image: minio/minio:latest
container_name: minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
volumes:
minio_data:
+53 -6
View File
@@ -3,13 +3,54 @@ name = "imphnen-backend"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[[bin]]
name = "api"
path = "src/main.rs"
[[bin]]
name = "clear_db_test"
path = "src/bin/clear_db_test.rs"
[[bin]]
name = "seeder"
path = "src/bin/seeder.rs"
[[bin]]
name = "seed_events"
path = "src/bin/seed_events.rs"
[[bin]]
name = "seed_gacha_rolls"
path = "src/bin/seed_gacha_rolls.rs"
[[bin]]
name = "seed_mentor_user"
path = "src/bin/seed_mentor_user.rs"
[[bin]]
name = "seed_permissions"
path = "src/bin/seed_permissions.rs"
[[bin]]
name = "seed_roles"
path = "src/bin/seed_roles.rs"
[[bin]]
name = "seed_roles_permissions"
path = "src/bin/seed_roles_permissions.rs"
[[bin]]
name = "seed_users"
path = "src/bin/seed_users.rs"
[dependencies] [dependencies]
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-gateway = { version = "0.1.0", path = "../imphnen-gateway" } imphnen-gateway.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } imphnen-iam.workspace = true
imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" } imphnen-cms.workspace = true
imphnen-dimentorin.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -26,3 +67,9 @@ anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
env_logger.workspace = true env_logger.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
uuid.workspace=true
tokio-tungstenite.workspace = true
url.workspace = true
futures-util.workspace = true
+5 -2
View File
@@ -1,4 +1,4 @@
use env_logger; use axum::Router;
use imphnen_gateway::gateway_service; use imphnen_gateway::gateway_service;
use imphnen_libs::axum_init; use imphnen_libs::axum_init;
@@ -6,7 +6,10 @@ use imphnen_libs::axum_init;
async fn main() { async fn main() {
env_logger::init(); env_logger::init();
axum_init(|surrealdb_ws, surrealdb_mem| async { axum_init(|surrealdb_ws, surrealdb_mem| async {
gateway_service(surrealdb_ws, surrealdb_mem).await let app = gateway_service(surrealdb_ws, surrealdb_mem).await;
let mut router = Router::new();
router = router.nest("/api/v1/auth", imphnen_iam::v1::auth::auth_router());
app.merge(router)
}) })
.await; .await;
} }
+169
View File
@@ -0,0 +1,169 @@
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use url::Url;
use futures_util::{StreamExt, SinkExt};
use serde_json::json;
// Menentukan kredensial dan detail koneksi secara langsung sebagai string statis
static SURREALDB_URL_WS: &str = "ws://localhost:8000/rpc";
static SURREALDB_USERNAME: &str = "root";
static SURREALDB_PASSWORD: &str = "root";
static SURREALDB_NAMESPACE: &str = "test";
static SURREALDB_DBNAME: &str = "test";
// Daftar tabel sebagai variabel static yang tidak dapat diubah
static TABLES_TO_CLEAR: &[&str] = &[
"app_events", "users", "roles", "permissions", "gacha_rolls",
"mentor_users", "gacha_claims", "gacha_credits", "gacha_items",
"mentor_profiles", "roles_permissions", "testimonials",
];
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Tidak perlu memuat env lagi, karena kita menggunakan nilai hardcoded
// imphnen_libs::enviroment::load_env(); // Baris ini tidak lagi dibutuhkan
// let env = Env::new(); // Baris ini tidak lagi dibutuhkan
println!("DEBUG: URL WS: {}", SURREALDB_URL_WS);
println!("DEBUG: Username: {}", SURREALDB_USERNAME);
println!("DEBUG: Namespace: {}", SURREALDB_NAMESPACE);
println!("DEBUG: Database: {}", SURREALDB_DBNAME);
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
let (ws_stream, _) = connect_async(url.as_str()).await?;
let (mut write, mut read) = ws_stream.split();
// Authenticate (signin)
let signin_query = json!({
"method": "signin",
"params": [{
"user": SURREALDB_USERNAME, // Menggunakan SURREALDB_USERNAME statis
"pass": SURREALDB_PASSWORD, // Menggunakan SURREALDB_PASSWORD statis
}],
"id": 1
}).to_string();
println!("DEBUG: Sending signin query: {}", signin_query);
write.send(Message::Text(signin_query.into())).await?;
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
let signin_response_msg = signin_response?;
let signin_response_str = signin_response_msg.to_text()?;
println!("DEBUG: Signin response: {}", signin_response_str);
if signin_response_str.contains("\"error\":") {
return Err(format!("Signin failed: {}", signin_response_str).into());
}
// Use namespace and database
let use_query = json!({
"method": "use",
"params": [SURREALDB_NAMESPACE, SURREALDB_DBNAME], // Menggunakan NS & DB statis
"id": 2
}).to_string();
println!("DEBUG: Sending use query: {}", use_query);
write.send(Message::Text(use_query.into())).await?;
let use_response = read.next().await.ok_or("Failed to read use response")?;
let use_response_msg = use_response?;
let use_response_str = use_response_msg.to_text()?;
println!("DEBUG: Use response: {}", use_response_str);
if use_response_str.contains("\"error\":") {
return Err(format!("USE command failed: {}", use_response_str).into());
}
println!("INFO: Attempting to clear database tables via WebSocket...");
let mut all_clear = true;
for (i, table) in TABLES_TO_CLEAR.iter().enumerate() {
let remove_query = format!("REMOVE TABLE {};", table);
let query_json = json!({
"method": "query",
"params": [remove_query],
"id": i + 3
}).to_string();
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
write.send(Message::Text(query_json.into())).await?;
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
match response_result {
Ok(msg) => {
let response_str = msg.to_text()?;
if response_str.contains("\"error\":") {
println!("WARN: Failed to REMOVE TABLE {}: {}. Attempting DELETE type::{}.", table, response_str, table);
let delete_all_query = format!("DELETE FROM {};", table);
let delete_all_json = json!({
"method": "query",
"params": [delete_all_query],
"id": i + 300
}).to_string();
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
write.send(Message::Text(delete_all_json.into())).await?;
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
match delete_response_result {
Ok(delete_msg) => {
let delete_response_str = delete_msg.to_text()?;
if delete_response_str.contains("\"error\":") {
println!("ERROR: Failed to DELETE type::{} : {}", table, delete_response_str);
all_clear = false;
} else {
println!("INFO: Successfully DELETED type:: table: {}", table);
}
},
Err(delete_e) => {
println!("ERROR: Error receiving response for DELETE type:: table {}: {}", table, delete_e);
all_clear = false;
}
}
} else {
println!("INFO: Successfully REMOVED TABLE: {}", table);
}
},
Err(e) => {
println!("ERROR: Error receiving response for REMOVE TABLE {}: {}", table, e);
all_clear = false;
}
}
// Check if table is empty after deletion attempt
let select_query = format!("SELECT * FROM {} LIMIT 1;", table);
let select_json = json!({
"method": "query",
"params": [select_query],
"id": i + 1000
}).to_string();
write.send(Message::Text(select_json.into())).await?;
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
match select_response_result {
Ok(select_msg) => {
let select_response_str = select_msg.to_text()?;
if select_response_str.contains("does not exist") {
println!("CHECK: Table '{}' does not exist after clear attempt (success).", table);
} else if select_response_str.contains("\"result\":[]") || select_response_str.contains("\"result\":[[]]") {
println!("CHECK: Table '{}' is empty after clear attempt.", table);
} else {
println!("WARNING: Table '{}' is NOT empty after clear attempt! Response: {}", table, select_response_str);
all_clear = false;
}
},
Err(e) => {
println!("ERROR: Error receiving response for SELECT check on table {}: {}", table, e);
all_clear = false;
}
}
}
println!("INFO: Database clearing complete.");
if !all_clear {
eprintln!("ERROR: One or more tables could not be cleared. Check logs for details.");
return Err("Database clearing failed for one or more tables.".into());
}
Ok(())
}
+25 -15
View File
@@ -1,26 +1,24 @@
use imphnen_cms::v1::landing::events::events_schema::EventsSchema; use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
use imphnen_utils::{get_iso_date, Env}; use imphnen_utils::{get_iso_date};
use std::error::Error; use std::error::Error;
use surrealdb::engine::any; use surrealdb::engine::any;
use surrealdb::{opt::auth::Root, sql::Thing}; use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
use imphnen_libs::enviroment::load_env;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); let env = &imphnen_libs::enviroment::ENV;
let env = Env::new();
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
password: &env.surrealdb_password, password: &env.surrealdb_password,
}) })
.await?; .await?;
db.use_ns(env.surrealdb_namespace) db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname) .use_db(env.surrealdb_dbname.clone())
.await?; .await?;
let events = vec![ let events = vec![
( (
"e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o",
"Tech Conference 2025", "Tech Conference 2025",
"Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.", "Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.",
"https://techconf2025.example.com", "https://techconf2025.example.com",
@@ -31,7 +29,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
"2025-06-17T18:00:00Z", "2025-06-17T18:00:00Z",
), ),
( (
"f2b3c4d5-6e7f-8g9h-0i1j-2k3l4m5n6o7p",
"Online Web Development Workshop", "Online Web Development Workshop",
"Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.", "Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.",
"https://webdev-workshop.example.com", "https://webdev-workshop.example.com",
@@ -42,7 +39,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
"2025-07-10T17:00:00Z", "2025-07-10T17:00:00Z",
), ),
( (
"g3c4d5e6-7f8g-9h0i-1j2k-3l4m5n6o7p8q",
"Startup Pitch Competition", "Startup Pitch Competition",
"Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.", "Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.",
"https://startup-pitch.example.com", "https://startup-pitch.example.com",
@@ -53,7 +49,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
"2025-08-05T16:00:00Z", "2025-08-05T16:00:00Z",
), ),
( (
"h4d5e6f7-8g9h-0i1j-2k3l-4m5n6o7p8q9r",
"Digital Marketing Masterclass", "Digital Marketing Masterclass",
"Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.", "Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.",
"https://digital-marketing.example.com", "https://digital-marketing.example.com",
@@ -65,9 +60,20 @@ async fn main() -> Result<(), Box<dyn Error>> {
), ),
]; ];
for (id, name, description, detail_link, price, location, is_online, start_date, end_date) in events { for (
name,
description,
detail_link,
price,
location,
is_online,
start_date,
end_date,
) in events
{
let uuid = Uuid::new_v4().to_string(); // Generate new UUID
let event = EventsSchema { let event = EventsSchema {
id: Thing::from(("app_events", id)), id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID
name: name.into(), name: name.into(),
description: description.into(), description: description.into(),
detail_link: detail_link.into(), detail_link: detail_link.into(),
@@ -81,11 +87,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
updated_at: get_iso_date(), updated_at: get_iso_date(),
}; };
db.create::<Option<EventsSchema>>(("app_events", id)) db.create::<Option<EventsSchema>>(("app_events", uuid.as_str())) // Use generated UUID
.content(event) .content(event)
.await?; .await?;
println!("✅ Inserted event: {} ({})", name, if is_online { "Online" } else { "In-person" }); println!(
"✅ Inserted event: {} ({})",
name,
if is_online { "Online" } else { "In-person" }
);
} }
println!("✅ All Events seeded"); println!("✅ All Events seeded");
@@ -0,0 +1,52 @@
use imphnen_utils::{get_iso_date};
use std::error::Error;
use surrealdb::opt::auth::Root;
use surrealdb::sql::Thing;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
db.query("DELETE type::thing('app_gacha_items', $id)")
.bind(("id", "gacha_item_test_id"))
.await?;
db.query("DELETE type::thing('app_gacha_rolls', $id)")
.bind(("id", "gacha_roll_test_id"))
.await?;
let gacha_item_id = "gacha_item_test_id";
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
.bind(("id", gacha_item_id))
.bind(("name", "Test Gacha Item"))
.bind(("image_url", "https://example.com/gacha_item.png"))
.bind(("is_deleted", false))
.bind(("created_at", get_iso_date()))
.bind(("updated_at", get_iso_date()))
.await?;
println!("Gacha Item seeded successfully!");
let gacha_roll_id = "gacha_roll_test_id";
db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
.bind(("id", gacha_roll_id))
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
.bind(("quantity", 10))
.bind(("weight", 1.0))
.bind(("is_deleted", false))
.bind(("created_at", get_iso_date()))
.bind(("updated_at", get_iso_date()))
.await?;
println!("Gacha Roll seeded successfully!");
println!("✅ Gacha items and rolls seeded.");
Ok(())
}
@@ -0,0 +1,94 @@
use imphnen_utils::{get_iso_date, hash_password};
use serde_json::json;
use std::error::Error;
use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
password: &env.surrealdb_password,
})
.await?;
db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname.clone())
.await?;
db.query("DELETE type::thing('app_mentors', $id)")
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
.await?;
db.query("DELETE type::thing('app_users', $id)")
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
.await?;
use surrealdb::sql::Thing;
db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, legal_name = $legal_name, domicile = $domicile, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, last_education = $last_education, linkedin_url = $lLine truncated
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
.bind(("fullname", "Mentor User"))
.bind(("email", "mentor@example.com"))
.bind(("password", hash_password("password").unwrap()))
.bind(("avatar", Option::<String>::None))
.bind(("phone_number", "081234567890"))
.bind(("is_active", true))
.bind(("is_deleted", false))
.bind(("mentor_id", Option::<Thing>::None))
.bind(("gender", "male"))
.bind(("birthdate", "1990-05-15"))
.bind(("role", Thing::from(("app_roles", "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a"))))
.bind(("legal_name", "Mentor User"))
.bind(("domicile", "Jakarta, Indonesia"))
// .bind(("identity_document_url", "https://example.com/ktp.jpg"))
.bind(("phone_for_verification", "081234567890"))
.bind(("bio", "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant."))
.bind(("last_education", "S1 Teknik Informatika"))
.bind(("linkedin_url", "https://linkedin.com/in/mentor"))
.bind(("github_url", "https://github.com/mentor"))
.bind(("cv_url", Option::<String>::None))
.bind(("portfolio_url", Option::<String>::None))
.bind(("created_at", get_iso_date()))
.bind(("updated_at", get_iso_date()))
.await?;
db.query("CREATE type::thing('app_mentors', $id) SET user_id = $user_id, industries = $industries, expertise = $expertise, languages = $languages, current_company = $current_company, current_role = $current_role, years_of_experience = $years_of_experience, topics_of_interest = $topics_of_interest, preferred_mentee_level = $preferred_mentee_level, preferred_mentoring_formats = $preferred_mentoring_formats, availability_commitment = $availability_commitment, mentoring_rate = $mentoring_rate, staLine truncated
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
.bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))))
.bind(("industries", vec!["Software", "Education"]))
.bind(("expertise", vec!["Rust", "Microservices"]))
.bind(("languages", vec!["Indonesian", "English"]))
.bind(("current_company", "PT Contoh"))
.bind(("current_role", "Senior Backend Engineer"))
.bind(("years_of_experience", 5))
.bind(("topics_of_interest", vec!["Rust Programming", "Backend Development"]))
.bind(("preferred_mentee_level", vec!["beginner", "intermediate"]))
.bind(("preferred_mentoring_formats", vec!["online", "offline"]))
.bind(("availability_commitment", "2 jam per minggu untuk mentoring online dan offline"))
.bind(("mentoring_rate", json!({
"amount": 100000,
"currency": "IDR",
"per_duration": "hour"
})))
.bind(("status", "verified"))
.bind(("is_deleted", false))
.bind(("created_at", get_iso_date()))
.bind(("updated_at", get_iso_date()))
.await?;
println!("Mentor created successfully!");
println!("Updating user with mentor_id...");
db.query("UPDATE type::thing('app_users', $id) SET mentor_id = $mentor_id")
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
.bind((
"mentor_id",
Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
))
.await?;
println!("User updated with mentor_id successfully!");
println!("✅ Inserted mentor user: mentor@example.com");
println!("✅ Mentor user seeded");
Ok(())
}
+15 -8
View File
@@ -1,6 +1,5 @@
use imphnen_iam::PermissionsEnum; use imphnen_iam::PermissionsEnum;
use imphnen_libs::enviroment::load_env; use imphnen_utils::{get_iso_date};
use imphnen_utils::{get_iso_date, Env};
use serde_json::json; use serde_json::json;
use std::error::Error; use std::error::Error;
use surrealdb::engine::any; use surrealdb::engine::any;
@@ -8,17 +7,15 @@ use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); let env = &imphnen_libs::enviroment::ENV;
let env = Env::new();
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
password: &env.surrealdb_password, password: &env.surrealdb_password,
}) })
.await?; .await?;
db.use_ns(env.surrealdb_namespace) db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname) .use_db(env.surrealdb_dbname.clone())
.await?; .await?;
for permission in [ for permission in [
@@ -48,6 +45,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
PermissionsEnum::ReadDetailGachaRolls, PermissionsEnum::ReadDetailGachaRolls,
PermissionsEnum::CreateGachaRolls, PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls, PermissionsEnum::ExecuteGachaRolls,
PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailMentors,
PermissionsEnum::RegisterMentors,
PermissionsEnum::ReadOwnMentorProfile,
PermissionsEnum::UpdateOwnMentorProfile,
PermissionsEnum::ReadOwnMentorStatus,
PermissionsEnum::UpdateMentors,
PermissionsEnum::VerifyMentors,
PermissionsEnum::DeleteMentors,
] { ] {
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data") db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
.bind(("id", permission.id())) .bind(("id", permission.id()))
@@ -61,9 +67,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
}), }),
)) ))
.await?; .await?;
println!("✅ Inserted: {}", permission.to_string()); println!("✅ Inserted: {permission}");
} }
println!("✅ All Permissions seeded"); println!("✅ All Permissions seeded");
Ok(()) Ok(())
} }
+15 -8
View File
@@ -1,21 +1,19 @@
use imphnen_utils::{get_iso_date, Env}; use imphnen_utils::{get_iso_date};
use serde_json::json; use serde_json::json;
use std::error::Error; use std::error::Error;
use surrealdb::opt::auth::Root;
use imphnen_libs::enviroment::load_env;
use surrealdb::engine::any; use surrealdb::engine::any;
use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); let env = &imphnen_libs::enviroment::ENV;
let env = Env::new();
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
password: &env.surrealdb_password, password: &env.surrealdb_password,
}) })
.await?; .await?;
db.use_ns(env.surrealdb_namespace) db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname) .use_db(env.surrealdb_dbname.clone())
.await?; .await?;
let roles = vec![ let roles = vec![
@@ -49,9 +47,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
None, None,
Some("2025-02-22T15:38:39.868306+00"), Some("2025-02-22T15:38:39.868306+00"),
), ),
(
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
"Mentor",
None,
Some("2025-07-06T10:00:00.000000+00"),
),
]; ];
for (id, name, _created_at, _updated_at) in roles { for (id, name, _created_at, _updated_at) in roles {
db.query("DELETE type::thing('app_roles', $id)")
.bind(("id", id))
.await?;
db.query("CREATE type::thing('app_roles', $id) CONTENT $data") db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
.bind(("id", id)) .bind(("id", id))
.bind(( .bind((
@@ -65,7 +72,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
}), }),
)) ))
.await?; .await?;
println!("✅ Inserted role: {}", name); println!("✅ Inserted role: {name}");
} }
println!("✅ All Roles seeded"); println!("✅ All Roles seeded");
Ok(()) Ok(())
+130 -45
View File
@@ -1,58 +1,143 @@
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum}; use imphnen_iam::{get_iso_date, make_thing, PermissionsEnum};
use std::error::Error; use std::error::Error;
use surrealdb::opt::auth::Root;
use surrealdb::engine::any; use surrealdb::engine::any;
use imphnen_libs::enviroment::load_env; use surrealdb::opt::auth::Root;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); let env = &imphnen_libs::enviroment::ENV;
let env = Env::new();
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
username: &env.surrealdb_username, username: &env.surrealdb_username,
password: &env.surrealdb_password, password: &env.surrealdb_password,
}) })
.await?; .await?;
db.use_ns(env.surrealdb_namespace) db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname) .use_db(env.surrealdb_dbname.clone())
.await?; .await?;
let permission_refs_admin: Vec<_> = [ db.query("DEFINE INDEX user_email_index ON TABLE users COLUMNS email UNIQUE;")
PermissionsEnum::ReadListUsers,
PermissionsEnum::ReadDetailUsers, .await?;
PermissionsEnum::CreateUsers, db.query("DEFINE INDEX role_name_idx ON TABLE roles COLUMNS name UNIQUE;")
PermissionsEnum::DeleteUsers,
PermissionsEnum::UpdateUsers, .await?;
PermissionsEnum::ActivateUsers,
PermissionsEnum::ReadListRoles, println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
PermissionsEnum::ReadDetailRoles,
PermissionsEnum::CreateRoles, let roles_permissions = vec![
PermissionsEnum::DeleteRoles, (
PermissionsEnum::UpdateRoles, "f6b03f25-e416-4893-ac88-caaa690afb07",
PermissionsEnum::ReadListPermissions, vec![
PermissionsEnum::ReadDetailPermissions, PermissionsEnum::ReadListUsers,
PermissionsEnum::CreatePermissions, PermissionsEnum::ReadDetailUsers,
PermissionsEnum::DeletePermissions, PermissionsEnum::CreateUsers,
PermissionsEnum::UpdatePermissions, PermissionsEnum::DeleteUsers,
PermissionsEnum::CreateGachaClaims, PermissionsEnum::UpdateUsers,
PermissionsEnum::ReadDetailGachaClaims, PermissionsEnum::ActivateUsers,
PermissionsEnum::ReadListGachaItems, PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadDetailGachaItems, PermissionsEnum::ReadDetailRoles,
PermissionsEnum::CreateGachaItems, PermissionsEnum::CreateRoles,
PermissionsEnum::DeleteGachaItems, PermissionsEnum::DeleteRoles,
PermissionsEnum::UpdateGachaItems, PermissionsEnum::UpdateRoles,
PermissionsEnum::ReadDetailGachaRolls, PermissionsEnum::ReadListPermissions,
PermissionsEnum::CreateGachaRolls, PermissionsEnum::ReadDetailPermissions,
PermissionsEnum::ExecuteGachaRolls, PermissionsEnum::CreatePermissions,
] PermissionsEnum::DeletePermissions,
.iter() PermissionsEnum::UpdatePermissions,
.map(|perm| make_thing("app_permissions", perm.id())) PermissionsEnum::CreateGachaClaims,
.collect(); PermissionsEnum::ReadDetailGachaClaims,
let admin_role_id = "f6b03f25-e416-4893-ac88-caaa690afb07"; PermissionsEnum::ReadListGachaItems,
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false") PermissionsEnum::ReadDetailGachaItems,
.bind(("role_id", admin_role_id)) PermissionsEnum::CreateGachaItems,
.bind(("permissions", permission_refs_admin)) PermissionsEnum::DeleteGachaItems,
.bind(("updated_at", get_iso_date())) PermissionsEnum::UpdateGachaItems,
.await?; PermissionsEnum::ReadDetailGachaRolls,
println!("✅ All permissions successfully added to Admin role"); PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls,
PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailMentors,
PermissionsEnum::RegisterMentors,
PermissionsEnum::UpdateMentors,
PermissionsEnum::VerifyMentors,
PermissionsEnum::DeleteMentors,
],
),
(
"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![
PermissionsEnum::ReadListUsers,
PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailUsers,
PermissionsEnum::ActivateUsers,
PermissionsEnum::ReadListRoles,
PermissionsEnum::ReadDetailRoles,
PermissionsEnum::ReadListPermissions,
PermissionsEnum::ReadDetailPermissions,
PermissionsEnum::ReadListGachaItems,
PermissionsEnum::ReadDetailGachaItems,
PermissionsEnum::ReadListMentors,
PermissionsEnum::ReadDetailMentors,
PermissionsEnum::ReadDetailGachaRolls,
PermissionsEnum::CreateGachaRolls,
PermissionsEnum::ExecuteGachaRolls,
],
),
(
"60f1aeb7-dad2-4e06-bcb5-be1ba510c906",
vec![PermissionsEnum::ActivateUsers],
),
("6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0", vec![]),
];
for (role_id, permissions) in roles_permissions {
let permission_refs: Vec<_> = permissions
.iter()
.map(|perm| make_thing("app_permissions", perm.id()))
.collect();
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
.bind(("role_id", role_id))
.bind(("permissions", permission_refs))
.bind(("updated_at", get_iso_date()))
.await?;
println!("✅ Permissions updated for role: {role_id}");
}
println!("✅ All roles permissions updated!");
Ok(()) Ok(())
} }
+31 -10
View File
@@ -1,12 +1,11 @@
use imphnen_iam::UsersSchema; use imphnen_iam::UsersSchema;
use imphnen_libs::enviroment::load_env; use imphnen_utils::{get_iso_date, hash_password};
use imphnen_utils::{get_iso_date, hash_password, Env};
use std::error::Error; use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing}; use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
load_env(); let env = &imphnen_libs::enviroment::ENV;
let env = Env::new();
use surrealdb::engine::any; use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?; let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root { db.signin(Root {
@@ -14,8 +13,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
password: &env.surrealdb_password, password: &env.surrealdb_password,
}) })
.await?; .await?;
db.use_ns(env.surrealdb_namespace) db.use_ns(env.surrealdb_namespace.clone())
.use_db(env.surrealdb_dbname) .use_db(env.surrealdb_dbname.clone())
.await?; .await?;
let users = vec![ let users = vec![
@@ -40,17 +39,39 @@ async fn main() -> Result<(), Box<dyn Error>> {
]; ];
for (id, email, fullname, role_id) in users { for (id, email, fullname, role_id) in users {
db.query("DELETE type::thing('app_users', $id)")
.bind(("id", id))
.await?;
let user = UsersSchema { let user = UsersSchema {
id: Thing::from(("app_users", id)), id: Thing::from(("app_users", id)),
fullname: fullname.into(), fullname: fullname.into(),
legal_name: Some(format!("{} Legal Name", fullname)),
email: email.into(), email: email.into(),
password: hash_password("password").unwrap(), password: hash_password("password").unwrap(),
avatar: None, avatar: Some("https://example.com/avatar.jpg".into()),
phone_number: "081234567890".into(), phone_number: "081234567890".into(),
phone_for_verification: Some("081234567890".into()),
is_active: true, is_active: true,
is_deleted: false, is_deleted: false,
gender: None, mentor_id: None,
birthdate: None, gender: Some("male".into()),
birthdate: Some("1990-05-15".into()),
domicile: Some("Jakarta, Indonesia".into()),
// identity_document_url: None, // Sudah tidak dipakai, bisa dihapus dari schema jika tidak diperlukan
bio: Some(format!("{} adalah user dengan data pribadi lengkap untuk testing.", fullname)),
last_education: Some("S1 Teknik Informatika".into()),
linkedin_url: Some("https://linkedin.com/in/user".into()),
github_url: Some("https://github.com/user".into()),
cv_url: Some("https://example.com/cv.pdf".into()),
portfolio_url: Some("https://example.com/portfolio".into()),
website_url: Some("https://example.com/website".into()),
twitter_url: Some("https://twitter.com/user".into()),
location: Some("Jakarta, Indonesia".into()),
skills: Some(vec!["JavaScript".into(), "React".into(), "Node.js".into()]),
experience: None,
education: None,
career_status: Some("Senior Developer".into()),
role: Thing::from(("app_roles", role_id)), role: Thing::from(("app_roles", role_id)),
created_at: get_iso_date(), created_at: get_iso_date(),
updated_at: get_iso_date(), updated_at: get_iso_date(),
@@ -60,7 +81,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.content(user) .content(user)
.await?; .await?;
println!("✅ Inserted user: {} ({})", fullname, email); println!("✅ Inserted user: {fullname} ({email})");
} }
println!("✅ All Users seeded"); println!("✅ All Users seeded");
+2 -1
View File
@@ -20,7 +20,8 @@ fn main() -> Result<(), Box<dyn Error>> {
run_seed("seed_roles_permissions")?; run_seed("seed_roles_permissions")?;
run_seed("seed_users")?; run_seed("seed_users")?;
run_seed("seed_events")?; run_seed("seed_events")?;
run_seed("seed_gacha_rolls")?;
run_seed("seed_mentor_user")?;
println!("\n✅ All seeding completed successfully."); println!("\n✅ All seeding completed successfully.");
Ok(()) Ok(())
} }
+2
View File
@@ -3,6 +3,8 @@ use imphnen_libs::axum_init;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt::init();
axum_init(|surrealdb_ws, surrealdb_mem| async { axum_init(|surrealdb_ws, surrealdb_mem| async {
gateway_service(surrealdb_ws, surrealdb_mem).await gateway_service(surrealdb_ws, surrealdb_mem).await
}) })
+9 -4
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } imphnen-iam.workspace = true
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -23,3 +23,8 @@ chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
log.workspace = true
tracing.workspace = true
[package.metadata.validator.regex]
VALID_URL_REGEX = "^https?://"
@@ -1,16 +1,14 @@
use chrono::{DateTime, Utc};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; lazy_static! {
pub static ref VALID_URL_REGEX: regex::Regex =
regex::Regex::new(r"^https?://").unwrap();
}
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing; use surrealdb::sql::Thing;
use utoipa::ToSchema; use utoipa::ToSchema;
use validator::Validate; use validator::Validate;
// Lazy static regex for URL validation
lazy_static! {
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://").unwrap();
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct EventsCreateRequestDto { pub struct EventsCreateRequestDto {
#[validate(length(min = 1, message = "Name is required"))] #[validate(length(min = 1, message = "Name is required"))]
@@ -19,13 +17,10 @@ pub struct EventsCreateRequestDto {
#[validate(length(min = 1, message = "Description is required"))] #[validate(length(min = 1, message = "Description is required"))]
pub description: String, pub description: String,
#[validate(regex( #[validate(url(message = "Detail link must be a valid URL"))]
path = "VALID_URL_REGEX",
message = "Detail link must be a valid URL"
))]
pub detail_link: String, pub detail_link: String,
#[validate(range(min = 0, message = "Price cannot be negative"))] #[validate(range(min = 0.0, message = "Price cannot be negative"))]
pub price: f64, pub price: f64,
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)] #[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
@@ -49,9 +44,11 @@ pub struct EventsUpdateRequestDto {
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)] #[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
pub start_date: DateTime<Utc>, pub start_date: DateTime<Utc>,
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
pub price: f64, pub price: f64,
pub is_online: bool, pub is_online: bool,
pub description: String, pub description: String,
#[validate(url(message = "Detail link must be a valid URL"))]
pub detail_link: String, pub detail_link: String,
pub location: Option<String>, pub location: Option<String>,
} }
@@ -1,7 +1,10 @@
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema}; use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date}; use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date, make_thing};
use std::time::Instant;
use tracing::instrument;
use tracing::info;
pub struct EventsRepository<'a> { pub struct EventsRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -12,17 +15,26 @@ impl<'a> EventsRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, meta), err)]
pub async fn query_event_list( pub async fn query_event_list(
&self, &self,
meta: MetaRequestDto, meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> { ) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> {
let query = ListQueryBuilder::new(&ResourceEnum::Events.to_string()) let now = Instant::now();
let query = ListQueryBuilder::new(ResourceEnum::Events.to_string())
.with_select_fields(vec!["*"]) .with_select_fields(vec!["*"])
.with_pagination(meta.page, Some(10)) .with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref()) .with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build(); .build();
info!(query = %query, "Executing SurrealDB query");
let res: Vec<EventsQueryDto> = let res: Vec<EventsQueryDto> =
self.state.surrealdb_ws.query(query).await?.take(0)?; self.state.surrealdb_ws.query(query).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_event_list' took: {elapsed:.2?}");
}
let data = ResponseListSuccessDto { let data = ResponseListSuccessDto {
data: res, data: res,
meta: None, meta: None,
@@ -30,15 +42,32 @@ impl<'a> EventsRepository<'a> {
Ok(data) Ok(data)
} }
// Get event by ID #[instrument(skip(self, id), err)]
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> { pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
// Attempt to parse the ID. If it's a full Thing (e.g., "events:some_id"), extract the ID part.
// Otherwise, assume it's already the raw ID.
let parsed_id = if id.contains(":") {
let thing = make_thing(ResourceEnum::Events.to_string().as_str(), &id);
get_id(&thing)?.1.to_string()
} else {
id.clone()
};
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
.with_id(&id) .with_id(&parsed_id)
.with_select_fields(vec!["*"]); .with_select_fields(vec!["*"]);
let sql = builder.build(); let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query");
let result: Option<EventsQueryDto> = let result: Option<EventsQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_event_by_id' took: {elapsed:.2?}");
}
match result { match result {
Some(event) => { Some(event) => {
@@ -51,13 +80,22 @@ impl<'a> EventsRepository<'a> {
} }
} }
// Create new event #[instrument(skip(self, data), err)]
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> { pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events.to_string());
info!(query = %query_str, "Executing SurrealDB query");
let record: Option<EventsSchema> = db let record: Option<EventsSchema> = db
.create(ResourceEnum::Events.to_string()) .create(ResourceEnum::Events.to_string())
.content(data) .content(data)
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_event' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success create event".into()), Some(_) => Ok("Success create event".into()),
@@ -65,17 +103,16 @@ impl<'a> EventsRepository<'a> {
} }
} }
// Update existing event #[instrument(skip(self, data), err)]
pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> { pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
// Cek apakah event ada
let existing = self.query_event_by_id(data.id.id.to_raw()).await?; let existing = self.query_event_by_id(data.id.id.to_raw()).await?;
if existing.is_deleted { if existing.is_deleted {
bail!("Event already deleted"); bail!("Event already deleted");
} }
// Merge field tertentu jika diperlukan
let merged = EventsSchema { let merged = EventsSchema {
created_at: existing.created_at, created_at: existing.created_at,
updated_at: get_iso_date(), updated_at: get_iso_date(),
@@ -83,7 +120,15 @@ impl<'a> EventsRepository<'a> {
}; };
let record_key = get_id(&merged.id)?; let record_key = get_id(&merged.id)?;
let query_str = format!("UPDATE {:?} MERGE ...", record_key);
info!(query = %query_str, "Executing SurrealDB query");
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?; let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_event' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success update event".into()), Some(_) => Ok("Success update event".into()),
@@ -91,8 +136,9 @@ impl<'a> EventsRepository<'a> {
} }
} }
// Soft delete event (mark is_deleted = true) #[instrument(skip(self, id), err)]
pub async fn query_delete_event(&self, id: String) -> Result<String> { pub async fn query_delete_event(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let event = self.query_event_by_id(id).await?; let event = self.query_event_by_id(id).await?;
if event.is_deleted { if event.is_deleted {
@@ -100,10 +146,18 @@ impl<'a> EventsRepository<'a> {
} }
let record_key = get_id(&event.id)?; let record_key = get_id(&event.id)?;
let query_str = format!("UPDATE {:?} MERGE {{ is_deleted: true }}", record_key);
info!(query = %query_str, "Executing SurrealDB query");
let record: Option<EventsSchema> = db let record: Option<EventsSchema> = db
.update(record_key) .update(record_key)
.merge(serde_json::json!({ "is_deleted": true })) .merge(serde_json::json!({ "is_deleted": true }))
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_delete_event' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success delete event".into()), Some(_) => Ok("Success delete event".into()),
@@ -1,10 +1,12 @@
use imphnen_libs::ResourceEnum; use imphnen_libs::ResourceEnum;
use imphnen_utils::{get_iso_date, make_thing};
use serde::{Deserialize, Serialize};
use surrealdb::Uuid; use surrealdb::Uuid;
use surrealdb::sql::Thing; use surrealdb::sql::Thing;
use serde::{Deserialize, Serialize};
use imphnen_utils::{get_iso_date, make_thing};
use super::events_dto::{EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto}; use super::events_dto::{
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
};
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EventsSchema { pub struct EventsSchema {
@@ -1,86 +1,101 @@
use super::{ use super::{
events_dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto, EventsUpdateRequestDto}, events_dto::{
events_repository::EventsRepository, EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
events_schema::EventsSchema, EventsUpdateRequestDto,
},
events_repository::EventsRepository,
events_schema::EventsSchema,
}; };
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
use imphnen_utils::{common_response, success_list_response, success_response, validate_request};
use axum::{http::StatusCode, response::Response}; use axum::{http::StatusCode, response::Response};
use imphnen_libs::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
};
use imphnen_utils::{
common_response, success_list_response, success_response, validate_request,
};
pub struct EventsService; pub struct EventsService;
impl EventsService { impl EventsService {
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response { pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
let repo = EventsRepository::new(state); let repo = EventsRepository::new(state);
match repo.query_event_list(meta).await { match repo.query_event_list(meta).await {
Ok(data) => { Ok(data) => {
let items: Vec<EventsListItemDto> = data.data let items: Vec<EventsListItemDto> = data
.into_iter() .data
.filter(|e| !e.is_deleted) .into_iter()
.map(EventsQueryDto::from) .filter(|e| !e.is_deleted)
.collect(); .map(EventsQueryDto::from)
let response = ResponseListSuccessDto { .collect();
data: items, let response = ResponseListSuccessDto {
meta: data.meta, data: items,
}; meta: data.meta,
success_list_response(response) };
} success_list_response(response)
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), }
} Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
} }
}
pub async fn get_event_by_id(state: &AppState, id: String) -> Response { pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state); let repo = EventsRepository::new(state);
match repo.query_event_by_id(id).await { match repo.query_event_by_id(id).await {
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto { Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
data: EventsDetailItemDto { data: EventsDetailItemDto {
id: event.id.id.to_raw(), id: event.id.id.to_raw(),
name: event.name, name: event.name,
description: event.description, description: event.description,
detail_link: event.detail_link, detail_link: event.detail_link,
price: event.price, price: event.price,
is_online: event.is_online, is_online: event.is_online,
start_date: event.start_date, start_date: event.start_date,
end_date: event.end_date, end_date: event.end_date,
created_at: event.created_at, created_at: event.created_at,
updated_at: event.updated_at, updated_at: event.updated_at,
location: event.location, location: event.location,
}, },
}), }),
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"), Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
} }
} }
pub async fn create_event(state: &AppState, payload: EventsCreateRequestDto) -> Response { pub async fn create_event(
if let Err((status, message)) = validate_request(&payload) { state: &AppState,
return common_response(status, &message); payload: EventsCreateRequestDto,
} ) -> Response {
let repo = EventsRepository::new(state); if let Err((status, message)) = validate_request(&payload) {
let schema = EventsSchema::create(payload); return common_response(status, &message);
match repo.query_create_event(schema).await { }
Ok(msg) => common_response(StatusCode::CREATED, &msg), let repo = EventsRepository::new(state);
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), let schema = EventsSchema::create(payload);
} match repo.query_create_event(schema).await {
} Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
pub async fn update_event(state: &AppState, id: String, payload: EventsUpdateRequestDto) -> Response { pub async fn update_event(
if let Err((status, message)) = validate_request(&payload) { state: &AppState,
return common_response(status, &message); id: String,
} payload: EventsUpdateRequestDto,
let repo = EventsRepository::new(state); ) -> Response {
let schema = EventsSchema::update(payload, id); if let Err((status, message)) = validate_request(&payload) {
match repo.query_update_event(schema).await { return common_response(status, &message);
Ok(msg) => common_response(StatusCode::OK, &msg), }
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), let repo = EventsRepository::new(state);
} let schema = EventsSchema::update(payload, id);
} match repo.query_update_event(schema).await {
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
pub async fn delete_event(state: &AppState, id: String) -> Response { pub async fn delete_event(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state); let repo = EventsRepository::new(state);
match repo.query_delete_event(id).await { match repo.query_delete_event(id).await {
Ok(msg) => common_response(StatusCode::OK, &msg), Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
} }
} }
} }
@@ -73,7 +73,6 @@ pub async fn post_create_testimonial(
Extension(authenticated_user): Extension<UsersDetailQueryDto>, Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsCreateRequestDto>, Json(payload): Json<TestimonialsCreateRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
println!("Authenticated User Now: {:?}", authenticated_user);
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
} }
@@ -34,7 +34,7 @@ pub struct TestimonialsUpdateRequestDto {
pub struct TestimonialsListItemDto { pub struct TestimonialsListItemDto {
pub id: String, pub id: String,
pub user_id: String, pub user_id: String,
pub user_fullname: String, // Assuming we'll fetch user's full name pub user_fullname: String,
pub role: String, pub role: String,
pub content: String, pub content: String,
pub created_at: String, pub created_at: String,
@@ -45,7 +45,7 @@ pub struct TestimonialsListItemDto {
pub struct TestimonialsDetailItemDto { pub struct TestimonialsDetailItemDto {
pub id: String, pub id: String,
pub user_id: String, pub user_id: String,
pub user_fullname: String, // Assuming we'll fetch user's full name pub user_fullname: String,
pub role: String, pub role: String,
pub content: String, pub content: String,
pub created_at: String, pub created_at: String,
@@ -55,7 +55,7 @@ pub struct TestimonialsDetailItemDto {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestimonialsQueryDto { pub struct TestimonialsQueryDto {
pub id: Thing, pub id: Thing,
pub user: UsersSchema, // Change from Thing to UsersSchema pub user: UsersSchema,
pub role: String, pub role: String,
pub content: String, pub content: String,
pub is_deleted: bool, pub is_deleted: bool,
@@ -68,7 +68,7 @@ impl TestimonialsQueryDto {
TestimonialsListItemDto { TestimonialsListItemDto {
id: self.id.id.to_raw(), id: self.id.id.to_raw(),
user_id: self.user.id.id.to_raw(), user_id: self.user.id.id.to_raw(),
user_fullname: self.user.fullname, // Extract fullname from UsersSchema user_fullname: self.user.fullname,
role: self.role, role: self.role,
content: self.content, content: self.content,
created_at: self.created_at, created_at: self.created_at,
@@ -4,6 +4,10 @@ use super::{
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date}; use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use serde_json;
use std::time::Instant;
use tracing::instrument;
use tracing::info;
pub struct TestimonialsRepository<'a> { pub struct TestimonialsRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -14,17 +18,26 @@ impl<'a> TestimonialsRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, meta), err)]
pub async fn query_testimonial_list( pub async fn query_testimonial_list(
&self, &self,
meta: MetaRequestDto, meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> { ) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
let query = ListQueryBuilder::new(&ResourceEnum::Testimonials.to_string()) let now = Instant::now();
.with_select_fields(vec!["*", "user.* as user"]) // Select user details let query = ListQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_select_fields(vec!["*", "user.* as user"])
.with_pagination(meta.page, Some(10)) .with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref()) .with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build(); .build();
info!(query = %query, "Executing SurrealDB query");
let res: Vec<TestimonialsQueryDto> = let res: Vec<TestimonialsQueryDto> =
self.state.surrealdb_ws.query(query).await?.take(0)?; self.state.surrealdb_ws.query(query).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_testimonial_list' took: {elapsed:.2?}");
}
let data = ResponseListSuccessDto { let data = ResponseListSuccessDto {
data: res, data: res,
meta: None, meta: None,
@@ -32,17 +45,27 @@ impl<'a> TestimonialsRepository<'a> {
Ok(data) Ok(data)
} }
#[instrument(skip(self, id), err)]
pub async fn query_testimonial_by_id( pub async fn query_testimonial_by_id(
&self, &self,
id: String, id: String,
) -> Result<TestimonialsQueryDto> { ) -> Result<TestimonialsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_id(&id) .with_id(&id)
.with_select_fields(vec!["*", "user.* as user"]); // Select user details .with_condition("is_deleted = false")
.with_select_fields(vec!["*", "user.* as user"]);
let sql = builder.build(); let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query");
let result: Option<TestimonialsQueryDto> = let result: Option<TestimonialsQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_testimonial_by_id' took: {elapsed:.2?}");
}
match result { match result {
Some(testimonial) => { Some(testimonial) => {
@@ -55,26 +78,41 @@ impl<'a> TestimonialsRepository<'a> {
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_create_testimonial( pub async fn query_create_testimonial(
&self, &self,
data: TestimonialsSchema, data: TestimonialsSchema,
) -> Result<String> { ) -> Result<TestimonialsSchema> { // Change return type from String to TestimonialsSchema
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
info!(
resource = %ResourceEnum::Testimonials.to_string(),
payload = ?data,
"Executing SurrealDB create"
);
let record: Option<TestimonialsSchema> = db let record: Option<TestimonialsSchema> = db
.create(ResourceEnum::Testimonials.to_string()) .create(ResourceEnum::Testimonials.to_string())
.content(data) .content(data)
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_testimonial' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success create testimonial".into()), Some(created_testimonial) => Ok(created_testimonial), // Return the created testimonial
None => bail!("Failed to create testimonial"), None => bail!("Failed to create testimonial"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_update_testimonial( pub async fn query_update_testimonial(
&self, &self,
data: TestimonialsSchema, data: TestimonialsSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?; let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
@@ -85,13 +123,24 @@ impl<'a> TestimonialsRepository<'a> {
let merged = TestimonialsSchema { let merged = TestimonialsSchema {
created_at: existing.created_at, created_at: existing.created_at,
updated_at: get_iso_date(), updated_at: get_iso_date(),
user: existing.user.id, // Preserve user ID user: existing.user.id,
..data ..data
}; };
let record_key = get_id(&merged.id)?; let record_key = get_id(&merged.id)?;
info!(
record_key = ?record_key,
payload = ?merged,
"Executing SurrealDB update"
);
let record: Option<TestimonialsSchema> = let record: Option<TestimonialsSchema> =
db.update(record_key).merge(merged).await?; db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_testimonial' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success update testimonial".into()), Some(_) => Ok("Success update testimonial".into()),
@@ -99,7 +148,9 @@ impl<'a> TestimonialsRepository<'a> {
} }
} }
#[instrument(skip(self, id), err)]
pub async fn query_delete_testimonial(&self, id: String) -> Result<String> { pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let testimonial = self.query_testimonial_by_id(id).await?; let testimonial = self.query_testimonial_by_id(id).await?;
if testimonial.is_deleted { if testimonial.is_deleted {
@@ -107,10 +158,20 @@ impl<'a> TestimonialsRepository<'a> {
} }
let record_key = get_id(&testimonial.id)?; let record_key = get_id(&testimonial.id)?;
info!(
record_key = ?record_key,
"Executing SurrealDB soft delete"
);
let record: Option<TestimonialsSchema> = db let record: Option<TestimonialsSchema> = db
.update(record_key) .update(record_key)
.merge(serde_json::json!({ "is_deleted": true })) .merge(serde_json::json!({ "is_deleted": true }))
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_delete_testimonial' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success delete testimonial".into()), Some(_) => Ok("Success delete testimonial".into()),
@@ -11,7 +11,7 @@ use super::testimonials_dto::{
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestimonialsSchema { pub struct TestimonialsSchema {
pub id: Thing, pub id: Thing,
pub user: Thing, // Link to app_users table pub user: Thing,
pub role: String, pub role: String,
pub content: String, pub content: String,
pub is_deleted: bool, pub is_deleted: bool,
@@ -28,7 +28,7 @@ impl Default for TestimonialsSchema {
), ),
user: make_thing( user: make_thing(
&ResourceEnum::Users.to_string(), &ResourceEnum::Users.to_string(),
&Uuid::new_v4().to_string(), // Placeholder, will be replaced by actual user ID &Uuid::new_v4().to_string(),
), ),
role: String::new(), role: String::new(),
content: String::new(), content: String::new(),
@@ -11,7 +11,7 @@ use imphnen_libs::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
}; };
use imphnen_utils::{ use imphnen_utils::{
common_response, success_list_response, success_response, validate_request, common_response, success_list_response, success_response, success_created_response, validate_request,
}; };
pub struct TestimonialsService; pub struct TestimonialsService;
@@ -46,8 +46,8 @@ impl TestimonialsService {
Ok(testimonial) if !testimonial.is_deleted => { Ok(testimonial) if !testimonial.is_deleted => {
success_response(ResponseSuccessDto { success_response(ResponseSuccessDto {
data: TestimonialsDetailItemDto { data: TestimonialsDetailItemDto {
id: testimonial.id.id.to_raw(), id: testimonial.id.to_raw(),
user_id: testimonial.user.id.id.to_raw(), user_id: testimonial.user.id.to_raw(),
user_fullname: testimonial.user.fullname, user_fullname: testimonial.user.fullname,
role: testimonial.role, role: testimonial.role,
content: testimonial.content, content: testimonial.content,
@@ -72,7 +72,19 @@ impl TestimonialsService {
let repo = TestimonialsRepository::new(state); let repo = TestimonialsRepository::new(state);
let schema = TestimonialsSchema::create(payload, &authenticated_user.id); let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
match repo.query_create_testimonial(schema).await { match repo.query_create_testimonial(schema).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg), Ok(created_testimonial) => {
success_created_response(ResponseSuccessDto {
data: TestimonialsDetailItemDto {
id: created_testimonial.id.to_raw(),
user_id: created_testimonial.user.id.to_raw(),
user_fullname: authenticated_user.fullname.clone(),
role: created_testimonial.role,
content: created_testimonial.content,
created_at: created_testimonial.created_at,
updated_at: created_testimonial.updated_at,
},
})
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
} }
} }
+9 -3
View File
@@ -4,9 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
imphnen-iam.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -22,3 +23,8 @@ chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
tracing.workspace = true
[dev-dependencies]
dotenvy.workspace = true
http-body-util.workspace = true
+2 -14
View File
@@ -1,14 +1,2 @@
pub fn add(left: u64, right: u64) -> u64 { pub mod v1;
left + right pub use v1::*;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
@@ -0,0 +1,356 @@
use super::{
MentorDetailResponseDto, MentorListResponseDto, MentorUpdateRequestDto,
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsService,
};
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
use ::axum::{
extract::{Extension, Json, Path, Query},
http::HeaderMap,
response::{IntoResponse, Response},
};
use imphnen_entities::*;
use imphnen_iam::{PermissionsEnum, permissions_guard};
use imphnen_utils::extract_email;
use serde_json::json;
#[utoipa::path(
post,
path = "/v1/mentors/register",
request_body = MentorUserRegisterRequestDto,
responses(
(status = 200, description = "Mentor registered successfully", body = MentorRegisterResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 409, description = "Conflict - user already has mentor profile"),
(status = 500, description = "Internal server error")
),
tag = "Mentors"
)]
pub async fn post_register_mentor(
Extension(app_state): Extension<AppState>,
Json(dto): Json<MentorUserRegisterRequestDto>,
) -> Response {
MentorsService::register_mentor(&app_state, dto).await
}
#[utoipa::path(
get,
path = "/v1/mentors",
params(
("page" = Option<u64>, Query, description = "Page number"),
("per_page" = Option<u64>, Query, description = "Items per page"),
("search" = Option<String>, Query, description = "Search query"),
("sort_by" = Option<String>, Query, description = "Sort by field"),
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
),
responses(
(status = 200, description = "Get list of mentors", body = Vec<MentorListResponseDto>),
(status = 500, description = "Internal server error")
),
tag = "Mentors",
security(
("Bearer" = [])
)
)]
pub async fn get_mentor_list(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Query(meta): Query<MetaRequestDto>,
) -> Response {
match permissions_guard(
headers,
Extension(app_state),
vec![PermissionsEnum::ReadListMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::get_mentor_list(&app_state, meta).await,
Err(response) => response,
}
}
#[utoipa::path(
get,
path = "/v1/mentors/detail/{id}",
params(
("id" = String, Path, description = "Mentor ID")
),
responses(
(status = 200, description = "Get mentor by ID", body = MentorDetailResponseDto),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
),
tag = "Mentors",
security(
("Bearer" = [])
)
)]
pub async fn get_mentor_by_id(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
) -> Response {
match permissions_guard(
headers,
Extension(app_state),
vec![PermissionsEnum::ReadDetailMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::get_mentor_by_id(&app_state, &id).await,
Err(response) => response,
}
}
#[utoipa::path(
put,
path = "/v1/mentors/update/{id}",
params(
("id" = String, Path, description = "Mentor ID")
),
request_body = MentorUpdateRequestDto,
responses(
(status = 200, description = "Mentor updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
),
tag = "Mentors - Admin",
security(
("Bearer" = [])
)
)]
pub async fn put_update_mentor(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
Json(dto): Json<MentorUpdateRequestDto>,
) -> Response {
match permissions_guard(
headers,
Extension(app_state),
vec![PermissionsEnum::UpdateMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::update_mentor(&app_state, &id, dto).await,
Err(response) => response,
}
}
#[utoipa::path(
delete,
path = "/v1/mentors/delete/{id}",
params(
("id" = String, Path, description = "Mentor ID")
),
responses(
(status = 200, description = "Mentor deleted successfully"),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
),
tag = "Mentors - Admin",
security(
("Bearer" = [])
)
)]
pub async fn delete_mentor(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
) -> Response {
match permissions_guard(
headers,
Extension(app_state),
vec![PermissionsEnum::DeleteMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::delete_mentor(&app_state, &id).await,
Err(response) => response,
}
}
#[utoipa::path(
put,
path = "/v1/mentors/verify/{id}",
params(
("id" = String, Path, description = "Mentor ID")
),
request_body = MentorVerifyRequestDto,
responses(
(status = 200, description = "Mentor verified successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 404, description = "Mentor not found"),
(status = 500, description = "Internal server error")
),
tag = "Mentors - Admin",
security(
("Bearer" = [])
)
)]
pub async fn put_verify_mentor(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Path(id): Path<String>,
Json(dto): Json<MentorVerifyRequestDto>,
) -> Response {
match permissions_guard(
headers,
Extension(app_state),
vec![PermissionsEnum::VerifyMentors],
)
.await
{
Ok((_user, app_state)) => MentorsService::verify_mentor(&app_state, &id, dto).await,
Err(response) => response,
}
}
#[utoipa::path(
get,
path = "/v1/mentors/me",
responses(
(status = 200, description = "Current user's mentor profile", body = MentorDetailResponseDto),
(status = 401, description = "Unauthorized - invalid token"),
(status = 403, description = "Mentor profile not found for current user"),
(status = 500, description = "Internal server error")
),
tag = "Mentors",
security(
("Bearer" = [])
)
)]
pub async fn get_mentor_me(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
) -> Response {
match permissions_guard(
headers.clone(),
Extension(app_state),
vec![PermissionsEnum::ReadOwnMentorProfile],
)
.await
{
Ok((_user, app_state)) => {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return (
axum::http::StatusCode::UNAUTHORIZED,
Json(json!({
"error": "Unauthorized",
"message": "Token tidak valid"
})),
)
.into_response();
}
};
MentorsService::get_mentor_me(&app_state, &email).await
}
Err(response) => response,
}
}
#[utoipa::path(
put,
path = "/v1/mentors/update/me",
request_body = MentorUpdateRequestDto,
responses(
(status = 200, description = "Mentor profile updated successfully", body = MentorDetailResponseDto),
(status = 400, description = "Bad request - validation error"),
(status = 401, description = "Unauthorized - invalid token"),
(status = 404, description = "Mentor profile not found"),
(status = 500, description = "Internal server error")
),
tag = "Mentors",
security(
("Bearer" = [])
)
)]
pub async fn put_update_mentor_me(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
Json(dto): Json<MentorUpdateRequestDto>,
) -> Response {
match permissions_guard(
headers.clone(),
Extension(app_state),
vec![PermissionsEnum::UpdateOwnMentorProfile],
)
.await
{
Ok((_user, app_state)) => {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return imphnen_utils::common_response(
axum::http::StatusCode::UNAUTHORIZED,
"Token tidak valid",
);
}
};
MentorsService::update_mentor_me(&app_state, &email, dto).await
}
Err(response) => response,
}
}
#[utoipa::path(
put,
path = "/v1/mentors/update",
request_body = MentorUpdateRequestDto,
responses(
(status = 400, description = "Bad request - Mentor ID is required for update"),
),
tag = "Mentors - Admin"
)]
pub async fn put_update_mentor_no_id() -> Response {
imphnen_utils::common_response(
axum::http::StatusCode::BAD_REQUEST,
"Mentor ID is required for update",
)
}
#[utoipa::path(
get,
path = "/v1/mentors/status",
responses(
(status = 200, description = "Mentor application status", body = String),
(status = 401, description = "Unauthorized - invalid token"),
(status = 403, description = "No mentor application found for current user"),
(status = 500, description = "Internal server error")
),
tag = "Mentors",
security(
("Bearer" = [])
)
)]
pub async fn get_mentor_status(
headers: HeaderMap,
Extension(app_state): Extension<AppState>,
) -> Response {
match permissions_guard(
headers.clone(),
Extension(app_state),
vec![PermissionsEnum::ReadOwnMentorStatus],
)
.await
{
Ok((_user, app_state)) => {
let email = match extract_email(&headers) {
Some(email) => email,
None => {
return (
axum::http::StatusCode::UNAUTHORIZED,
Json(json!({
"error": "Unauthorized",
"message": "Token tidak valid"
})),
)
.into_response();
}
};
MentorsService::get_mentor_status(&app_state, &email).await
}
Err(response) => response,
}
}
@@ -0,0 +1,438 @@
use crate::v1::mentors::MentorSchema;
use imphnen_utils::extract_id;
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use validator::Validate;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct MentorListResponseDto {
pub id: String,
pub fullname: Option<String>,
pub email: Option<String>,
pub status: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MentorDetailWithUserDto {
pub id: Thing,
pub user_id: Thing,
// Personal data is now in UsersSchema, access via user_id
// Removed: fullname, email, legal_name, identity_document_url,
// phone_for_verification, bio, linkedin_url, github_url, cv_url
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
pub current_company: String,
pub current_role: String,
pub years_of_experience: i32,
pub topics_of_interest: Vec<String>,
pub preferred_mentee_level: Vec<String>,
pub preferred_mentoring_formats: Vec<String>,
pub availability_commitment: String,
pub mentoring_rate: MentoringRate,
pub status: String,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct MentorDetailResponseDto {
pub id: String,
pub user_id: String,
// Personal data fields from UsersSchema
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>,
// Professional data from MentorSchema
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
pub current_company: String,
pub current_role: String,
pub years_of_experience: i32,
pub topics_of_interest: Vec<String>,
pub preferred_mentee_level: Vec<String>,
pub preferred_mentoring_formats: Vec<String>,
pub availability_commitment: String,
pub mentoring_rate: MentoringRate,
pub status: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct MentorRegisterResponseDto {
pub id: String,
pub user_id: String,
pub email: Option<String>,
pub status: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct MentorUpdateRequestDto {
#[validate(length(
min = 3,
message = "Legal name must be at least 3 characters"
))]
#[serde(skip_serializing_if = "Option::is_none")]
pub legal_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gender: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domicile: Option<String>,
#[validate(length(
min = 10,
max = 15,
message = "Phone must be 10-15 characters"
))]
#[serde(skip_serializing_if = "Option::is_none")]
pub phone_for_verification: Option<String>,
#[validate(length(min = 50, message = "Bio must be at least 50 characters"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_education: Option<String>,
#[validate(url(message = "Invalid LinkedIn URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub linkedin_url: Option<String>,
#[validate(url(message = "Invalid GitHub URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub github_url: Option<String>,
#[validate(url(message = "Invalid CV URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub cv_url: Option<String>,
#[validate(url(message = "Invalid portfolio URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub portfolio_url: Option<String>,
#[validate(length(min = 1, message = "At least 1 industry required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub industries: Option<Vec<String>>,
#[validate(length(min = 1, message = "At least 1 expertise required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub expertise: Option<Vec<String>>,
#[validate(length(min = 1, message = "At least 1 language required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub languages: Option<Vec<String>>,
#[validate(length(min = 1, message = "Current company required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub current_company: Option<String>,
#[validate(length(min = 1, message = "Current role required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub current_role: Option<String>,
#[validate(range(min = 2, message = "At least 2 years of experience required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub years_of_experience: Option<i32>,
#[validate(length(min = 1, message = "At least 1 topic required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub topics_of_interest: Option<Vec<String>>,
#[validate(length(min = 1, message = "At least 1 mentee level required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_mentee_level: Option<Vec<String>>,
#[validate(length(min = 1, message = "At least 1 mentoring format required"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_mentoring_formats: Option<Vec<String>>,
#[validate(length(
min = 5,
message = "Availability commitment must be at least 5 characters"
))]
#[serde(skip_serializing_if = "Option::is_none")]
pub availability_commitment: Option<String>,
#[validate(range(min = 1, message = "Amount must be at least 1"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub mentoring_rate_amount: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct MentorUserRegisterRequestDto {
#[validate(
length(min = 1, message = "Email cannot be empty"),
email(message = "Email not valid")
)]
pub email: String,
#[validate(length(
min = 8,
message = "Password must have at least 8 characters"
))]
#[validate(custom(
function = "imphnen_iam::auth_dto::validate_password_complexity",
message = "Password must include uppercase, lowercase, number, and special character"
))]
pub password: String,
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
pub fullname: String,
#[validate(length(min = 1, message = "Phone number is required"))]
pub phone_number: String,
#[validate(nested)]
pub identity_and_verification: IdentityAndVerification,
#[validate(nested)]
pub professional_profile: ProfessionalProfile,
#[validate(nested)]
pub mentoring_logistics: MentoringLogistics,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct MentorRegisterFromTokenRequestDto {
#[validate(nested)]
pub identity_and_verification: IdentityAndVerification,
#[validate(nested)]
pub professional_profile: ProfessionalProfile,
#[validate(nested)]
pub mentoring_logistics: MentoringLogistics,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct IdentityAndVerification {
#[validate(length(
min = 3,
message = "Legal name must be at least 3 characters"
))]
pub legal_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub gender: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domicile: Option<String>,
#[validate(url(message = "Invalid identity document URL"))]
pub identity_document_url: String,
#[validate(length(
min = 10,
max = 15,
message = "Phone must be 10-15 characters"
))]
pub phone_for_verification: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct ProfessionalProfile {
#[validate(length(min = 50, message = "Bio must be at least 50 characters"))]
pub bio: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_education: Option<String>,
#[validate(url(message = "Invalid LinkedIn URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub linkedin_url: Option<String>,
#[validate(url(message = "Invalid GitHub URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub github_url: Option<String>,
#[validate(url(message = "Invalid CV URL"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub cv_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub portfolio_url: Option<String>,
#[validate(length(min = 1, message = "At least 1 industry required"))]
pub industries: Vec<String>,
#[validate(length(min = 1, message = "At least 1 expertise required"))]
pub expertise: Vec<String>,
#[validate(length(min = 1, message = "At least 1 language required"))]
pub languages: Vec<String>,
#[validate(length(min = 1, message = "Current company required"))]
pub current_company: String,
#[validate(length(min = 1, message = "Current role required"))]
pub current_role: String,
#[validate(range(min = 2, message = "At least 2 years of experience required"))]
pub years_of_experience: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct MentoringLogistics {
#[validate(length(min = 1, message = "At least 1 topic required"))]
pub topics_of_interest: Vec<String>,
#[validate(length(min = 1, message = "At least 1 mentee level required"))]
pub preferred_mentee_level: Vec<String>,
#[validate(length(min = 1, message = "At least 1 mentoring format required"))]
pub preferred_mentoring_formats: Vec<String>,
#[validate(length(
min = 5,
message = "Availability commitment must be at least 5 characters"
))]
pub availability_commitment: String,
#[validate(range(min = 1, message = "Amount must be at least 1"))]
pub mentoring_rate_amount: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate, Default)]
pub struct MentoringRate {
#[validate(range(min = 1, message = "Amount must be at least 1"))]
pub amount: u64,
#[validate(length(min = 1, message = "Currency is required"))]
pub currency: String,
#[validate(length(min = 1, message = "Per duration is required"))]
pub per_duration: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MentorInsertDto {
pub id: Thing,
pub user_id: Option<Thing>,
// Personal data removed - now stored in UsersSchema
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
pub current_company: String,
pub current_role: String,
pub years_of_experience: i32,
pub topics_of_interest: Vec<String>,
pub preferred_mentee_level: Vec<String>,
pub preferred_mentoring_formats: Vec<String>,
pub availability_commitment: String,
pub mentoring_rate: MentoringRate,
pub status: String,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<MentorSchema> for MentorInsertDto {
fn from(schema: MentorSchema) -> Self {
MentorInsertDto {
id: schema.id,
user_id: schema.user_id,
// Personal data removed from MentorSchema
industries: schema.industries,
expertise: schema.expertise,
languages: schema.languages,
current_company: schema.current_company,
current_role: schema.current_role,
years_of_experience: schema.years_of_experience,
topics_of_interest: schema.topics_of_interest,
preferred_mentee_level: schema.preferred_mentee_level,
preferred_mentoring_formats: schema.preferred_mentoring_formats,
availability_commitment: schema.availability_commitment,
mentoring_rate: schema.mentoring_rate,
status: schema.status,
is_deleted: schema.is_deleted,
created_at: schema.created_at,
updated_at: schema.updated_at,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct MentorVerifyRequestDto {
#[validate(length(min = 1, message = "Status is required"))]
pub status: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MentorDetailQueryDto {
pub id: Thing,
pub user_id: Thing,
// Personal data has been moved to UsersSchema
// Use user_id to get: fullname, email, legal_name, gender, domicile,
// identity_document_url, phone_for_verification, bio, last_education,
// linkedin_url, github_url, cv_url, portfolio_url
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
pub current_company: String,
pub current_role: String,
pub years_of_experience: i32,
pub topics_of_interest: Vec<String>,
pub preferred_mentee_level: Vec<String>,
pub preferred_mentoring_formats: Vec<String>,
pub availability_commitment: String,
pub mentoring_rate: MentoringRate,
pub status: String,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<MentorDetailQueryDto> for MentorListResponseDto {
fn from(dto: MentorDetailQueryDto) -> Self {
Self {
id: extract_id(&dto.id),
fullname: None, // now in user table, must be populated from service layer
email: None, // now in user table, must be populated from service layer
status: dto.status,
created_at: dto.created_at,
updated_at: dto.updated_at,
}
}
}
impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
fn from(dto: MentorDetailQueryDto) -> Self {
Self {
id: extract_id(&dto.id),
user_id: extract_id(&dto.user_id),
// Personal data fields are populated in service layer from UsersSchema
fullname: None, // populated from user table in service layer
email: None, // populated from user table in service layer
legal_name: None, // populated from user table in service layer
gender: None, // populated from user table in service layer
domicile: None, // populated from user table in service layer
phone_for_verification: None, // populated from user table in service layer
bio: None, // populated from user table in service layer
last_education: None, // populated from user table in service layer
linkedin_url: None, // populated from user table in service layer
github_url: None, // populated from user table in service layer
cv_url: None, // populated from user table in service layer
portfolio_url: None, // populated from user table in service layer
// Professional data from mentor
industries: dto.industries,
expertise: dto.expertise,
languages: dto.languages,
current_company: dto.current_company,
current_role: dto.current_role,
years_of_experience: dto.years_of_experience,
topics_of_interest: dto.topics_of_interest,
preferred_mentee_level: dto.preferred_mentee_level,
preferred_mentoring_formats: dto.preferred_mentoring_formats,
availability_commitment: dto.availability_commitment,
mentoring_rate: dto.mentoring_rate,
status: dto.status,
created_at: dto.created_at,
updated_at: dto.updated_at,
}
}
}
impl From<MentorSchema> for MentorRegisterResponseDto {
fn from(schema: MentorSchema) -> Self {
Self {
id: schema.id.to_string(),
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
email: None, // schema.email - now in user table
status: schema.status,
created_at: schema.created_at,
updated_at: schema.updated_at,
}
}
}
impl From<MentorDetailWithUserDto> for MentorDetailQueryDto {
fn from(dto: MentorDetailWithUserDto) -> Self {
MentorDetailQueryDto {
id: dto.id,
user_id: dto.user_id,
// Personal data removed from MentorDetailWithUserDto - now in UsersSchema
industries: dto.industries,
expertise: dto.expertise,
languages: dto.languages,
current_company: dto.current_company,
current_role: dto.current_role,
years_of_experience: dto.years_of_experience,
topics_of_interest: dto.topics_of_interest,
preferred_mentee_level: dto.preferred_mentee_level,
preferred_mentoring_formats: dto.preferred_mentoring_formats,
availability_commitment: dto.availability_commitment,
mentoring_rate: dto.mentoring_rate,
status: dto.status,
is_deleted: dto.is_deleted,
created_at: dto.created_at,
updated_at: dto.updated_at,
}
}
}
@@ -0,0 +1,282 @@
use anyhow::{Result, bail};
use imphnen_iam::{get_id, make_thing};
use surrealdb::sql::Thing;
use crate::v1::mentors::{MentorDetailWithUserDto, MentorInsertDto, MentorSchema};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
use serde_json::{Map, Value};
use std::time::Instant;
use tracing::instrument;
use tracing::info;
pub struct MentorsRepository<'a> {
pub state: &'a AppState,
}
impl<'a> MentorsRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
#[instrument(skip(self, meta), err)]
pub async fn query_mentor_list(
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<MentorDetailWithUserDto>>> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let mentors_table = ResourceEnum::Mentors.to_string();
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
.search_field("user_id.legal_name") // Search in user data instead
.select_fields(vec![
"id",
"user_id",
// Personal data comes from user relation, not mentor table
"industries",
"expertise",
"languages",
"current_company",
"current_role",
"years_of_experience",
"topics_of_interest",
"preferred_mentee_level",
"preferred_mentoring_formats",
"availability_commitment",
"mentoring_rate",
"status",
"is_deleted",
"created_at",
"updated_at",
]);
let result = builder.build().await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_mentor_list' took: {elapsed:.2?}");
}
let data = result.data.into_iter().collect();
Ok(ResponseListSuccessDto {
data,
meta: result.meta,
})
}
#[instrument(skip(self, email, include_deleted), err)]
pub async fn query_mentor_by_email(
&self,
email: String,
include_deleted: bool,
) -> Result<MentorDetailWithUserDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
.with_where("user_id.email", Some(email.clone())) // Search in user table
.with_select_fields(vec![
"id",
"user_id",
// Personal data comes from user relation, not mentor table
"industries",
"expertise",
"languages",
"current_company",
"current_role",
"years_of_experience",
"topics_of_interest",
"preferred_mentee_level",
"preferred_mentoring_formats",
"availability_commitment",
"mentoring_rate",
"status",
"is_deleted",
"created_at",
"updated_at",
]);
if !include_deleted {
builder = builder.with_condition("is_deleted = false");
}
let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_email");
let mentor_opt: Option<MentorDetailWithUserDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_mentor_by_email' took: {elapsed:.2?}");
}
let Some(mentor) = mentor_opt else {
bail!("Mentor not found");
};
Ok(mentor)
}
#[instrument(skip(self, id, include_deleted), err)]
pub async fn query_mentor_by_id(
&self,
id: &Thing,
include_deleted: bool,
) -> Result<MentorDetailWithUserDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
.with_id(get_id(id)?.1)
.with_select_fields(vec![
"id",
"user_id",
// Personal data comes from user relation, not mentor table
"industries",
"expertise",
"languages",
"current_company",
"current_role",
"years_of_experience",
"topics_of_interest",
"preferred_mentee_level",
"preferred_mentoring_formats",
"availability_commitment",
"mentoring_rate",
"status",
"is_deleted",
"created_at",
"updated_at",
]);
if !include_deleted {
builder = builder.with_condition("is_deleted = false");
}
let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
let mentor_opt: Option<MentorDetailWithUserDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
}
let Some(mentor) = mentor_opt else {
bail!("Mentor not found in database");
};
if mentor.is_deleted && !include_deleted {
bail!("Mentor has been deleted");
}
Ok(mentor)
}
#[instrument(skip(self, data), err)]
pub async fn query_create_mentor(&self, data: MentorSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let dto: MentorInsertDto = data.into();
let resource = ResourceEnum::Mentors.to_string();
info!(query = %resource, "Executing SurrealDB create in query_create_mentor");
let record: Option<MentorSchema> = db
.create(resource)
.content(dto.clone())
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_mentor' took: {elapsed:.2?}");
}
match record {
Some(mentor) => {
let id_str = mentor.id.id.to_raw();
let _user = format!("{:?}", mentor.user_id);
Ok(id_str)
}
None => {
bail!("Failed to create mentor")
}
}
}
#[instrument(skip(self, data), err)]
pub async fn query_update_mentor(&self, data: MentorSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let id_ref = &data.id;
let record_key = get_id(id_ref)?;
let _existing = self.query_mentor_by_id(id_ref, false).await?;
let mut merged_data_json: Map<String, Value> =
serde_json::to_value(data.clone())
.map_err(|e| anyhow::anyhow!("Failed to serialize MentorSchema: {}", e))?
.as_object()
.cloned()
.unwrap_or_default();
merged_data_json.remove("id");
merged_data_json.remove("user_id");
merged_data_json.remove("created_at");
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
info!(query = ?record_key, "Executing SurrealDB update in query_update_mentor");
let record: Option<MentorSchema> =
db.update(record_key).merge(merged_data_json).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_mentor' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success update mentor".into()),
None => {
bail!("Failed to update mentor")
}
}
}
#[instrument(skip(self, id), err)]
pub async fn query_delete_mentor(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let thing = make_thing(ResourceEnum::Mentors.to_string().as_str(), &id);
let record_key = get_id(&thing)?;
let mentor_to_delete_res = self.query_mentor_by_id(&thing, true).await;
let _mentor_to_delete = match mentor_to_delete_res {
Ok(mentor) => {
if mentor.is_deleted {
bail!("Mentor is already soft deleted");
}
mentor
}
Err(e) => {
if e.to_string().contains("Mentor has been deleted") {
bail!("Mentor is already soft deleted");
} else {
return Err(e);
}
}
};
let mut patch = Map::new();
patch.insert("is_deleted".to_string(), Value::Bool(true));
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
info!(query = ?record_key, "Executing SurrealDB soft delete in query_delete_mentor");
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_delete_mentor' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success soft delete mentor".into()),
None => {
bail!("Failed to soft delete mentor")
}
}
}
}
@@ -0,0 +1,175 @@
use super::{
MentorDetailQueryDto, MentorUpdateRequestDto,
MentoringLogistics, MentoringRate, ProfessionalProfile,
};
use imphnen_libs::ResourceEnum;
use imphnen_utils::{get_iso_date, make_thing};
use serde::{Deserialize, Serialize};
use surrealdb::{Uuid, sql::Thing};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MentorSchema {
pub id: Thing,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<Thing>,
// Personal data has been moved to UsersSchema - use user_id to reference
// phone_for_verification, bio, last_education, linkedin_url, github_url,
// cv_url, portfolio_url
pub industries: Vec<String>,
pub expertise: Vec<String>,
pub languages: Vec<String>,
pub current_company: String,
pub current_role: String,
pub years_of_experience: i32,
pub topics_of_interest: Vec<String>,
pub preferred_mentee_level: Vec<String>,
pub preferred_mentoring_formats: Vec<String>,
pub availability_commitment: String,
pub mentoring_rate: MentoringRate,
pub status: String,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
}
impl Default for MentorSchema {
fn default() -> Self {
Self {
id: make_thing(
ResourceEnum::Mentors.to_string().as_str(),
&Uuid::new_v4().to_string(),
),
user_id: Some(make_thing(
ResourceEnum::Users.to_string().as_str(),
&Uuid::new_v4().to_string(),
)),
industries: Vec::new(),
expertise: Vec::new(),
languages: Vec::new(),
current_company: String::new(),
current_role: String::new(),
years_of_experience: 0,
topics_of_interest: Vec::new(),
preferred_mentee_level: Vec::new(),
preferred_mentoring_formats: Vec::new(),
availability_commitment: String::new(),
mentoring_rate: MentoringRate {
amount: 0,
currency: "IDR".to_string(),
per_duration: "hour".to_string(),
},
status: "pending".to_string(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
}
impl MentorSchema {
pub fn create(
professional_profile: ProfessionalProfile,
mentoring_logistics: MentoringLogistics,
user_id_raw: String,
) -> Self {
Self {
id: make_thing(
&ResourceEnum::Mentors.to_string(),
&Uuid::new_v4().to_string(),
),
user_id: Some(make_thing(&ResourceEnum::Users.to_string(), &user_id_raw)),
// Personal data now stored in UsersSchema, not here
industries: professional_profile.industries,
expertise: professional_profile.expertise,
languages: professional_profile.languages,
current_company: professional_profile.current_company,
current_role: professional_profile.current_role,
years_of_experience: professional_profile.years_of_experience,
topics_of_interest: mentoring_logistics.topics_of_interest,
preferred_mentee_level: mentoring_logistics.preferred_mentee_level,
preferred_mentoring_formats: mentoring_logistics.preferred_mentoring_formats,
availability_commitment: mentoring_logistics.availability_commitment,
mentoring_rate: MentoringRate {
amount: mentoring_logistics.mentoring_rate_amount,
currency: "IDR".to_string(),
per_duration: "hour".to_string(),
},
status: "pending".to_string(),
is_deleted: false,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
pub fn from(dto: MentorDetailQueryDto) -> Self {
Self {
id: dto.id,
user_id: Some(dto.user_id),
// Personal data now comes from UsersSchema via user_id
industries: dto.industries,
expertise: dto.expertise,
languages: dto.languages,
current_company: dto.current_company,
current_role: dto.current_role,
years_of_experience: dto.years_of_experience,
topics_of_interest: dto.topics_of_interest,
preferred_mentee_level: dto.preferred_mentee_level,
preferred_mentoring_formats: dto.preferred_mentoring_formats,
availability_commitment: dto.availability_commitment,
mentoring_rate: dto.mentoring_rate,
status: dto.status,
is_deleted: dto.is_deleted,
created_at: dto.created_at,
updated_at: dto.updated_at,
}
}
pub fn update(mut self, dto: MentorUpdateRequestDto) -> Self {
// phone_for_verification, bio, last_education, linkedin_url, github_url,
// cv_url, portfolio_url) are now updated in UsersSchema, not here
// Only update professional fields that are still in MentorSchema
if let Some(val) = dto.industries {
self.industries = val;
}
if let Some(val) = dto.expertise {
self.expertise = val;
}
if let Some(val) = dto.languages {
self.languages = val;
}
if let Some(val) = dto.current_company {
self.current_company = val;
}
if let Some(val) = dto.current_role {
self.current_role = val;
}
if let Some(val) = dto.years_of_experience {
self.years_of_experience = val;
}
if let Some(val) = dto.topics_of_interest {
self.topics_of_interest = val;
}
if let Some(val) = dto.preferred_mentee_level {
self.preferred_mentee_level = val;
}
if let Some(val) = dto.preferred_mentoring_formats {
self.preferred_mentoring_formats = val;
}
if let Some(val) = dto.availability_commitment {
self.availability_commitment = val;
}
if let Some(val) = dto.mentoring_rate_amount {
self.mentoring_rate.amount = val;
}
self.updated_at = get_iso_date();
self
}
pub fn update_status(mut self, status: String) -> Self {
self.status = status;
self.updated_at = get_iso_date();
self
}
}
@@ -0,0 +1,463 @@
use crate::v1::mentors::{
MentorDetailQueryDto, MentorDetailResponseDto, MentorListResponseDto,
MentorRegisterResponseDto, MentorSchema, MentorUpdateRequestDto,
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsRepository,
};
use axum::http::StatusCode;
use axum::response::Response;
use imphnen_entities::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
};
use imphnen_iam::{
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
};
use imphnen_libs::ResourceEnum;
use imphnen_utils::{
common_response, success_list_response, success_response, validate_request,
};
use surrealdb::Uuid;
use surrealdb::sql::Thing;
use tracing::error;
pub struct MentorsService;
impl MentorsService {
pub async fn register_mentor(
state: &AppState,
dto: MentorUserRegisterRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&dto) {
return common_response(status, &message);
}
let user_repo = UsersRepository::new(state);
let mentor_repo = MentorsRepository::new(state);
let role_repo = RolesRepository::new(state);
let auth_repo = AuthRepository::new(state);
let user_email = dto.email.clone();
let mut _user_to_update: Option<UsersSchema> = None;
let existing_user_result =
user_repo.query_user_by_email(user_email.clone()).await;
let user_id;
let final_user_email = user_email.clone();
if let Ok(user_detail_query_dto) = existing_user_result {
if mentor_repo
.query_mentor_by_email(user_email.clone(), false)
.await
.is_ok()
{
return common_response(
StatusCode::CONFLICT,
"Mentor profile already exists for this user",
);
}
let mut user_schema = UsersSchema::from(user_detail_query_dto.clone());
user_schema.fullname = dto.fullname.clone();
user_schema.phone_number = dto.phone_number.clone();
// Update personal data from identity_and_verification
user_schema.legal_name = Some(dto.identity_and_verification.legal_name.clone());
user_schema.gender = dto.identity_and_verification.gender.clone();
user_schema.domicile = dto.identity_and_verification.domicile.clone();
user_schema.phone_for_verification = Some(dto.identity_and_verification.phone_for_verification.clone());
// Update personal data from professional_profile
user_schema.bio = Some(dto.professional_profile.bio.clone());
user_schema.last_education = dto.professional_profile.last_education.clone();
user_schema.linkedin_url = dto.professional_profile.linkedin_url.clone();
user_schema.github_url = dto.professional_profile.github_url.clone();
user_schema.cv_url = dto.professional_profile.cv_url.clone();
user_schema.portfolio_url = dto.professional_profile.portfolio_url.clone();
user_schema.updated_at = imphnen_utils::get_iso_date();
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
Ok(hash) => hash,
Err(_e) => {
error!(
"Failed to hash password during update for {}: {}",
final_user_email, _e
);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to hash password",
);
}
};
user_schema.password = hashed_password;
let mentor_role = match role_repo
.query_role_by_name(RolesEnum::Mentor.to_string())
.await
{
Ok(role) => role,
Err(_e) => {
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
}
};
user_schema.role =
imphnen_utils::make_thing(&ResourceEnum::Roles.to_string(), &mentor_role.id);
user_schema.is_active = false;
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
error!(
"Failed to update existing user {} to mentor role: {}",
final_user_email, _err
);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&_err.to_string(),
);
}
user_id = user_schema.id.clone();
} else {
let mentor_role = match role_repo
.query_role_by_name(RolesEnum::Mentor.to_string())
.await
{
Ok(role) => role,
Err(_e) => {
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
}
};
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
Ok(hash) => hash,
Err(_e) => {
error!(
"Failed to hash password for new user {}: {}",
final_user_email, _e
);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to hash password",
);
}
};
let new_user_schema = UsersSchema {
id: imphnen_utils::make_thing(
&ResourceEnum::Users.to_string(),
&Uuid::new_v4().to_string(),
),
email: dto.email.clone(),
fullname: dto.fullname.clone(),
password: hashed_password,
phone_number: dto.phone_number.clone(),
// Store personal data from identity_and_verification in user
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
gender: dto.identity_and_verification.gender.clone(),
domicile: dto.identity_and_verification.domicile.clone(),
phone_for_verification: Some(dto.identity_and_verification.phone_for_verification.clone()),
// Store personal data from professional_profile in user
bio: Some(dto.professional_profile.bio.clone()),
last_education: dto.professional_profile.last_education.clone(),
linkedin_url: dto.professional_profile.linkedin_url.clone(),
github_url: dto.professional_profile.github_url.clone(),
cv_url: dto.professional_profile.cv_url.clone(),
portfolio_url: dto.professional_profile.portfolio_url.clone(),
created_at: imphnen_utils::get_iso_date(),
updated_at: imphnen_utils::get_iso_date(),
role: imphnen_utils::make_thing(
&ResourceEnum::Roles.to_string(),
&mentor_role.id,
),
is_active: false,
..Default::default()
};
user_id = new_user_schema.id.clone();
match user_repo.query_create_user(new_user_schema).await {
Ok(_) => {}
Err(_err) => {
error!("Failed to create new user {}: {}", final_user_email, _err);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&_err.to_string(),
);
}
}
}
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
match auth_repo
.query_store_otp(final_user_email.clone(), otp)
.await
{
Ok(_) => {
let message = format!("your otp code is {otp}");
if let Err(_err) =
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
{
error!("Failed to send OTP email to {}: {}", final_user_email, _err);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&_err.to_string(),
);
}
}
Err(_err) => {
error!("Failed to store OTP for {}: {}", final_user_email, _err);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&_err.to_string(),
);
}
}
let mentor_schema = MentorSchema::create(
dto.professional_profile,
dto.mentoring_logistics,
user_id.to_raw(),
);
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
Ok(mentor_profile_id) => {
let user_after_mentor_creation_dto = user_repo
.query_user_by_email(final_user_email.clone())
.await
.unwrap();
let mut user_after_mentor_creation_schema =
UsersSchema::from(user_after_mentor_creation_dto);
user_after_mentor_creation_schema = user_after_mentor_creation_schema
.update_mentor_id(Some(mentor_profile_id));
if let Err(_e) = user_repo
.query_update_user(user_after_mentor_creation_schema)
.await
{
error!(
"Failed to update user's mentor_id for {}: {}",
final_user_email, _e
);
return common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&_e.to_string(),
);
}
let response_dto = MentorRegisterResponseDto::from(mentor_schema);
success_response(ResponseSuccessDto { data: response_dto })
}
Err(_e) => {
error!(
"Failed to create mentor profile for {}: {}",
final_user_email, _e
);
common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string())
}
}
}
pub async fn get_mentor_list(state: &AppState, meta: MetaRequestDto) -> Response {
let repo = MentorsRepository::new(state);
let user_repo = UsersRepository::new(state);
match repo.query_mentor_list(meta).await {
Ok(result) => {
let mut mentor_list_data: Vec<MentorListResponseDto> = Vec::new();
for mentor_with_user in result.data {
let mentor_dto = MentorDetailQueryDto::from(mentor_with_user);
let mut list_item = MentorListResponseDto::from(mentor_dto.clone());
// Get user data to populate personal fields
if let Ok(user) = user_repo.query_user_by_id(&mentor_dto.user_id).await {
list_item.fullname = Some(user.fullname);
list_item.email = Some(user.email);
}
mentor_list_data.push(list_item);
}
success_list_response(ResponseListSuccessDto {
data: mentor_list_data,
meta: result.meta,
})
}
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
}
}
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
let mentor_repo = MentorsRepository::new(state);
let user_repo = UsersRepository::new(state);
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
match mentor_repo.query_mentor_by_id(&thing_id, false).await {
Ok(mentor) => {
// Get user data separately
let user_result = user_repo.query_user_by_id(&mentor.user_id).await;
match user_result {
Ok(user) => {
// Combine mentor and user data
let dto = MentorDetailResponseDto {
id: mentor.id.to_raw(),
user_id: mentor.user_id.to_raw(),
// Personal data from user
fullname: Some(user.fullname),
email: Some(user.email),
legal_name: user.legal_name,
gender: user.gender,
domicile: user.domicile,
phone_for_verification: user.phone_for_verification,
bio: user.bio,
last_education: user.last_education,
linkedin_url: user.linkedin_url,
github_url: user.github_url,
cv_url: user.cv_url,
portfolio_url: user.portfolio_url,
// Professional data from mentor
industries: mentor.industries,
expertise: mentor.expertise,
languages: mentor.languages,
current_company: mentor.current_company,
current_role: mentor.current_role,
years_of_experience: mentor.years_of_experience,
topics_of_interest: mentor.topics_of_interest,
preferred_mentee_level: mentor.preferred_mentee_level,
preferred_mentoring_formats: mentor.preferred_mentoring_formats,
availability_commitment: mentor.availability_commitment,
mentoring_rate: mentor.mentoring_rate,
status: mentor.status,
created_at: mentor.created_at,
updated_at: mentor.updated_at,
};
success_response(ResponseSuccessDto { data: dto })
}
Err(_e) => {
error!("Failed to get user data for mentor {}: {}", id, _e);
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get mentor user data")
}
}
}
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
}
}
pub async fn update_mentor(
state: &AppState,
id: &str,
dto: MentorUpdateRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&dto) {
return common_response(status, &message);
}
let repo = MentorsRepository::new(state);
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
Ok(mentor) => mentor,
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
};
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
schema = schema.update(dto);
match repo.query_update_mentor(schema).await {
Ok(_) => {
let updated_mentor =
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
let response_dto =
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
success_response(ResponseSuccessDto { data: response_dto })
}
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
}
}
pub async fn delete_mentor(state: &AppState, id: &str) -> Response {
let repo = MentorsRepository::new(state);
match repo.query_delete_mentor(id.to_string()).await {
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
}
}
pub async fn get_mentor_me(state: &AppState, email: &str) -> Response {
let repo = MentorsRepository::new(state);
match repo.query_mentor_by_email(email.to_string(), false).await {
Ok(mentor) => {
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
success_response(ResponseSuccessDto { data: dto })
}
Err(_e) => common_response(
StatusCode::FORBIDDEN,
"Mentor profile not found for current user",
),
}
}
pub async fn update_mentor_me(
state: &AppState,
email: &str,
dto: MentorUpdateRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&dto) {
return common_response(status, &message);
}
let repo = MentorsRepository::new(state);
let existing_mentor =
match repo.query_mentor_by_email(email.to_string(), false).await {
Ok(mentor) => mentor,
Err(_e) => return common_response(StatusCode::FORBIDDEN, &_e.to_string()),
};
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
schema = schema.update(dto);
match repo.query_update_mentor(schema).await {
Ok(_) => {
let updated_mentor = repo
.query_mentor_by_email(email.to_string(), false)
.await
.unwrap();
let response_dto =
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
success_response(ResponseSuccessDto { data: response_dto })
}
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
}
}
pub async fn get_mentor_status(state: &AppState, email: &str) -> Response {
let repo = MentorsRepository::new(state);
match repo.query_mentor_by_email(email.to_string(), false).await {
Ok(mentor) => common_response(StatusCode::OK, &mentor.status),
Err(_e) => common_response(
StatusCode::FORBIDDEN,
"No mentor application found for current user",
),
}
}
pub async fn verify_mentor(
state: &AppState,
id: &str,
dto: MentorVerifyRequestDto,
) -> Response {
let repo = MentorsRepository::new(state);
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
Ok(mentor) => mentor,
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
};
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
schema = schema.update_status(dto.status);
match repo.query_update_mentor(schema).await {
Ok(_) => {
let updated_mentor =
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
let response_dto =
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
success_response(ResponseSuccessDto { data: response_dto })
}
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
}
}
}
+30
View File
@@ -0,0 +1,30 @@
use axum::{
Router,
routing::{delete, get, post, put},
};
pub mod mentors_controller;
pub mod mentors_dto;
pub mod mentors_repository;
pub mod mentors_schema;
pub mod mentors_service;
pub use mentors_controller::*;
pub use mentors_dto::*;
pub use mentors_repository::*;
pub use mentors_schema::*;
pub use mentors_service::*;
pub fn mentors_router() -> Router {
Router::new()
.route("/", get(get_mentor_list))
.route("/register", post(post_register_mentor))
.route("/me", get(get_mentor_me))
.route("/update/me", put(put_update_mentor_me))
.route("/status", get(get_mentor_status))
.route("/detail/{id}", get(get_mentor_by_id))
.route("/update/{id}", put(put_update_mentor))
.route("/update", put(put_update_mentor_no_id))
.route("/delete/{id}", delete(delete_mentor))
.route("/verify/{id}", put(put_verify_mentor))
}
+7
View File
@@ -0,0 +1,7 @@
use axum::Router;
pub mod mentors;
pub fn dimentorin_router() -> Router {
Router::new().nest("/mentors", mentors::mentors_router())
}
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -8,4 +8,5 @@ axum.workspace = true
serde.workspace = true serde.workspace = true
utoipa.workspace = true utoipa.workspace = true
surrealdb.workspace = true surrealdb.workspace = true
anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
+1 -5
View File
@@ -1,9 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use surrealdb::{ use surrealdb::{Surreal, engine::any::Any, engine::local::Db};
engine::local::Db,
engine::any::Any,
Surreal,
};
use utoipa::{IntoParams, ToSchema}; use utoipa::{IntoParams, ToSchema};
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+38 -6
View File
@@ -1,26 +1,58 @@
pub mod error { pub mod error {
use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::response::Response; use axum::response::Response;
use axum::Json;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("database error")] #[error("database error: {0}")]
Db, Db(String),
#[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 { impl IntoResponse for Error {
fn into_response(self) -> Response { fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, Json(self.to_string())).into_response() let (status, error_message) = match self {
Error::Db(detail) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Database error: {detail}"),
),
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<surrealdb::Error> for Error { impl From<surrealdb::Error> for Error {
fn from(error: surrealdb::Error) -> Self { fn from(error: surrealdb::Error) -> Self {
eprintln!("{error}"); Self::Db(error.to_string())
Self::Db
} }
} }
impl From<StatusCode> for Error {
fn from(status: StatusCode) -> Self {
Self::StatusCode(status)
}
}
} }
+6 -4
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-iam ={ version = "0.1.0", path = "../imphnen-iam" } imphnen-iam.workspace = true
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs.workspace = true
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils.workspace = true
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -24,3 +24,5 @@ anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
rand_distr.workspace = true rand_distr.workspace = true
log.workspace = true
tracing.workspace = true
-4
View File
@@ -1,7 +1,3 @@
use imphnen_entities::*;
use imphnen_libs::*;
use imphnen_utils::*;
pub mod v1; pub mod v1;
pub use imphnen_entities::*; pub use imphnen_entities::*;
@@ -25,13 +25,13 @@ pub async fn get_detail_gacha_claim(
Path(id): Path<String>, Path(id): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match permissions_guard( match permissions_guard(
&headers, headers,
state.clone(), Extension(state),
vec![PermissionsEnum::CreateGachaClaims], vec![PermissionsEnum::ReadDetailGachaClaims],
) )
.await .await
{ {
Ok(_) => GachaClaimService::get_gacha_claim_by_id(&state, id).await, Ok((_user, state)) => GachaClaimService::get_gacha_claim_by_id(&state, id).await,
Err(response) => response, Err(response) => response,
} }
} }
@@ -54,13 +54,13 @@ pub async fn post_create_gacha_claim(
Json(payload): Json<GachaClaimRequestDto>, Json(payload): Json<GachaClaimRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match permissions_guard( match permissions_guard(
&headers, headers,
state.clone(), Extension(state),
vec![PermissionsEnum::CreateGachaClaims], vec![PermissionsEnum::CreateGachaClaims],
) )
.await .await
{ {
Ok(_) => GachaClaimService::create_gacha_claim(&state, payload).await, Ok((_user, state)) => GachaClaimService::create_gacha_claim(&state, payload).await,
Err(response) => response, Err(response) => response,
} }
} }
@@ -2,6 +2,8 @@ use super::{GachaClaimQueryDto, GachaClaimSchema};
use crate::{AppState, ResourceEnum}; use crate::{AppState, ResourceEnum};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_iam::DetailQueryBuilder; use imphnen_iam::DetailQueryBuilder;
use std::time::Instant;
use tracing::{instrument, info};
pub struct GachaClaimRepository<'a> { pub struct GachaClaimRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -12,10 +14,12 @@ impl<'a> GachaClaimRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, id), err)]
pub async fn query_gacha_claim_by_id( pub async fn query_gacha_claim_by_id(
&self, &self,
id: String, id: String,
) -> Result<GachaClaimQueryDto> { ) -> Result<GachaClaimQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string()) let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string())
.with_id(id.clone()) .with_id(id.clone())
@@ -23,23 +27,43 @@ impl<'a> GachaClaimRepository<'a> {
.with_fetch("item") .with_fetch("item")
.with_fetch("user"); .with_fetch("user");
let sql = builder.build(); let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query");
let result: Option<GachaClaimQueryDto> = let result: Option<GachaClaimQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?; builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_gacha_claim_by_id' took: {elapsed:.2?}");
}
match result { match result {
Some(claim) if !claim.is_deleted => Ok(claim), Some(claim) if !claim.is_deleted => Ok(claim),
_ => bail!("Gacha Claim not found"), _ => bail!("Gacha Claim not found"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_create_gacha_claim( pub async fn query_create_gacha_claim(
&self, &self,
data: GachaClaimSchema, data: GachaClaimSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
info!(
resource = %ResourceEnum::GachaClaims.to_string(),
content = ?data,
"Executing SurrealDB create query"
);
let record: Option<GachaClaimSchema> = db let record: Option<GachaClaimSchema> = db
.create(ResourceEnum::GachaClaims.to_string()) .create(ResourceEnum::GachaClaims.to_string())
.content(data) .content(data)
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_gacha_claim' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success create Gacha Claim".into()), Some(_) => Ok("Success create Gacha Claim".into()),
None => bail!("Failed to create Gacha Claim"), None => bail!("Failed to create Gacha Claim"),
@@ -2,7 +2,9 @@ use super::{GachaCreditRequestDto, GachaCreditSchema};
use crate::{AppState, ResourceEnum}; use crate::{AppState, ResourceEnum};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_iam::make_thing; use imphnen_iam::make_thing;
use std::time::Instant;
use surrealdb::Uuid; use surrealdb::Uuid;
use tracing::{instrument, info};
pub struct GachaCreditRepository<'a> { pub struct GachaCreditRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -13,31 +15,59 @@ impl<'a> GachaCreditRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, user_id), err)]
pub async fn query_by_user_id( pub async fn query_by_user_id(
&self, &self,
user_id: String, user_id: String,
) -> Result<Option<GachaCreditSchema>> { ) -> Result<Option<GachaCreditSchema>> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let sql = format!( let sql = format!(
"SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1", "SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1",
ResourceEnum::GachaCredits.to_string(), ResourceEnum::GachaCredits,
ResourceEnum::Users.to_string() ResourceEnum::Users
); );
info!(query = %sql, "Executing SurrealDB query");
let result: Vec<GachaCreditSchema> = let result: Vec<GachaCreditSchema> =
db.query(sql).bind(("user_id", user_id)).await?.take(0)?; db.query(sql).bind(("user_id", user_id)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_by_user_id' took: {elapsed:.2?}");
}
Ok(result.into_iter().next()) Ok(result.into_iter().next())
} }
#[instrument(skip(self, user_id), err)]
pub async fn query_consume_credit(&self, user_id: String) -> Result<()> { pub async fn query_consume_credit(&self, user_id: String) -> Result<()> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let credit_opt = self.query_by_user_id(user_id).await?; let credit_opt = self.query_by_user_id(user_id).await?;
let Some(mut credit) = credit_opt else { let Some(mut credit) = credit_opt else {
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!(
"Query 'query_consume_credit' took: {elapsed:.2?} (no credit to consume)"
);
}
return Ok(()); return Ok(());
}; };
if credit.available_rolls <= 0 { if credit.available_rolls <= 0 {
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!(
"Query 'query_consume_credit' took: {elapsed:.2?} (no rolls remaining)"
);
}
bail!("No extra roll credits remaining"); bail!("No extra roll credits remaining");
} }
credit.available_rolls -= 1; credit.available_rolls -= 1;
info!(operation = "update", table = %ResourceEnum::GachaCredits.to_string(), id = %credit.id.id.to_raw(), "Executing SurrealDB update for consume_credit");
let _: Option<GachaCreditSchema> = db let _: Option<GachaCreditSchema> = db
.update(( .update((
&ResourceEnum::GachaCredits.to_string(), &ResourceEnum::GachaCredits.to_string(),
@@ -45,16 +75,25 @@ impl<'a> GachaCreditRepository<'a> {
)) ))
.merge(credit) .merge(credit)
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_consume_credit' took: {elapsed:.2?}");
}
Ok(()) Ok(())
} }
#[instrument(skip(self, payload), err)]
pub async fn query_add_credit( pub async fn query_add_credit(
&self, &self,
payload: GachaCreditRequestDto, payload: GachaCreditRequestDto,
) -> Result<()> { ) -> Result<()> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? { if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
credit.available_rolls += payload.amount; credit.available_rolls += payload.amount;
info!(operation = "update", table = %ResourceEnum::GachaCredits.to_string(), id = %credit.id.id.to_raw(), "Executing SurrealDB update for add_credit");
let _: Option<GachaCreditSchema> = db let _: Option<GachaCreditSchema> = db
.update(( .update((
&ResourceEnum::GachaCredits.to_string(), &ResourceEnum::GachaCredits.to_string(),
@@ -63,7 +102,7 @@ impl<'a> GachaCreditRepository<'a> {
.merge(credit) .merge(credit)
.await?; .await?;
} else { } else {
let data = GachaCreditSchema::from(&GachaCreditSchema { let data = GachaCreditSchema {
id: make_thing( id: make_thing(
&ResourceEnum::GachaCredits.to_string(), &ResourceEnum::GachaCredits.to_string(),
&Uuid::new_v4().to_string(), &Uuid::new_v4().to_string(),
@@ -71,12 +110,19 @@ impl<'a> GachaCreditRepository<'a> {
user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id), user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id),
available_rolls: payload.amount, available_rolls: payload.amount,
..Default::default() ..Default::default()
}); };
info!(operation = "create", table = %ResourceEnum::GachaCredits.to_string(), "Executing SurrealDB create for add_credit");
let _: Option<GachaCreditSchema> = db let _: Option<GachaCreditSchema> = db
.create(&ResourceEnum::GachaCredits.to_string()) .create(ResourceEnum::GachaCredits.to_string())
.content(data) .content(data)
.await?; .await?;
} }
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_add_credit' took: {elapsed:.2?}");
}
Ok(()) Ok(())
} }
} }
@@ -1,5 +1,5 @@
use crate::{ use crate::{
AppState, GachaItemDto, GachaItemRequestDto, GachaItemService, MessageResponseDto, AppState, GachaItemDto, GachaItemRequestDto, GachaItemUpdateRequestDto, GachaItemService, MessageResponseDto,
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
}; };
use axum::{ use axum::{
@@ -36,13 +36,13 @@ pub async fn get_gacha_item_list(
Query(meta): Query<MetaRequestDto>, Query(meta): Query<MetaRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match permissions_guard( match permissions_guard(
&headers, headers,
state.clone(), Extension(state),
vec![PermissionsEnum::ReadListGachaItems], vec![PermissionsEnum::ReadListGachaItems],
) )
.await .await
{ {
Ok(_) => GachaItemService::get_gacha_item_list(&state, meta).await, Ok((_user, state)) => GachaItemService::get_gacha_item_list(&state, meta).await,
Err(response) => response, Err(response) => response,
} }
} }
@@ -65,13 +65,13 @@ pub async fn get_gacha_item_by_id(
Path(id): Path<String>, Path(id): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match permissions_guard( match permissions_guard(
&headers, headers,
state.clone(), Extension(state),
vec![PermissionsEnum::ReadDetailGachaItems], vec![PermissionsEnum::ReadDetailGachaItems],
) )
.await .await
{ {
Ok(_) => GachaItemService::get_gacha_item_by_id(&state, id).await, Ok((_user, state)) => GachaItemService::get_gacha_item_by_id(&state, id).await,
Err(response) => response, Err(response) => response,
} }
} }
@@ -94,13 +94,13 @@ pub async fn post_create_gacha_item(
Json(payload): Json<GachaItemRequestDto>, Json(payload): Json<GachaItemRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match permissions_guard( match permissions_guard(
&headers, headers,
state.clone(), Extension(state),
vec![PermissionsEnum::CreateGachaItems], vec![PermissionsEnum::CreateGachaItems],
) )
.await .await
{ {
Ok(_) => GachaItemService::create_gacha_item(&state, payload).await, Ok((_user, state)) => GachaItemService::create_gacha_item(&state, payload).await,
Err(response) => response, Err(response) => response,
} }
} }
@@ -111,7 +111,7 @@ pub async fn post_create_gacha_item(
security( security(
("Bearer" = []) ("Bearer" = [])
), ),
request_body = GachaItemRequestDto, request_body = GachaItemUpdateRequestDto,
responses( responses(
(status = 200, description = "Update gacha item", body = MessageResponseDto) (status = 200, description = "Update gacha item", body = MessageResponseDto)
), ),
@@ -121,16 +121,16 @@ pub async fn put_update_gacha_item(
headers: HeaderMap, headers: HeaderMap,
Extension(state): Extension<AppState>, Extension(state): Extension<AppState>,
Path(id): Path<String>, Path(id): Path<String>,
Json(payload): Json<GachaItemRequestDto>, Json(payload): Json<GachaItemUpdateRequestDto>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match permissions_guard( match permissions_guard(
&headers, headers,
state.clone(), Extension(state),
vec![PermissionsEnum::UpdateGachaItems], vec![PermissionsEnum::UpdateGachaItems],
) )
.await .await
{ {
Ok(_) => GachaItemService::update_gacha_item(&state, payload, id).await, Ok((_user, state)) => GachaItemService::update_gacha_item(&state, payload, id).await,
Err(response) => response, Err(response) => response,
} }
} }
@@ -152,13 +152,13 @@ pub async fn delete_gacha_item(
Path(id): Path<String>, Path(id): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match permissions_guard( match permissions_guard(
&headers, headers,
state.clone(), Extension(state),
vec![PermissionsEnum::DeleteGachaItems], vec![PermissionsEnum::DeleteGachaItems],
) )
.await .await
{ {
Ok(_) => GachaItemService::delete_gacha_item(&state, id).await, Ok((_user, state)) => GachaItemService::delete_gacha_item(&state, id).await,
Err(response) => response, Err(response) => response,
} }
} }
@@ -11,6 +11,16 @@ pub struct GachaItemRequestDto {
pub image_url: String, pub image_url: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct GachaItemUpdateRequestDto {
#[validate(length(min = 1, message = "Item name must not be empty"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[validate(length(min = 1, message = "Image URL must not be empty"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub image_url: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaItemDto { pub struct GachaItemDto {
pub id: String, pub id: String,
@@ -5,6 +5,11 @@ use crate::{
}; };
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use imphnen_iam::QueryListBuilder; use imphnen_iam::QueryListBuilder;
use imphnen_utils::get_iso_date;
use serde_json::{Map, Value};
use std::time::Instant;
use tracing::instrument;
use tracing::info;
pub struct GachaItemRepository<'a> { pub struct GachaItemRepository<'a> {
state: &'a AppState, state: &'a AppState,
@@ -15,10 +20,17 @@ impl<'a> GachaItemRepository<'a> {
Self { state } Self { state }
} }
#[instrument(skip(self, meta), err)]
pub async fn query_gacha_item_list( pub async fn query_gacha_item_list(
&self, &self,
meta: MetaRequestDto, meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> { ) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
let now = Instant::now();
let surreal_query = format!(
"SELECT * FROM {} WHERE is_deleted = false AND name LIKE ?",
ResourceEnum::GachaItems.to_string()
);
info!(query = %surreal_query, "Executing SurrealDB query");
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> = let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
QueryListBuilder::new( QueryListBuilder::new(
&self.state.surrealdb_ws, &self.state.surrealdb_ws,
@@ -30,6 +42,12 @@ impl<'a> GachaItemRepository<'a> {
.select_fields(vec!["*"]) .select_fields(vec!["*"])
.build() .build()
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_gacha_item_list' took: {elapsed:.2?}");
}
let data = raw_result let data = raw_result
.data .data
.into_iter() .into_iter()
@@ -41,36 +59,58 @@ impl<'a> GachaItemRepository<'a> {
}) })
} }
#[instrument(skip(self, id), err)]
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> { pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let surreal_query = format!("SELECT * FROM {} WHERE id = '{}'", ResourceEnum::GachaItems.to_string(), id);
info!(query = %surreal_query, "Executing SurrealDB query");
let result: Option<GachaItemSchema> = db let result: Option<GachaItemSchema> = db
.select((ResourceEnum::GachaItems.to_string(), id.clone())) .select((ResourceEnum::GachaItems.to_string(), id.clone()))
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_gacha_item_by_id' took: {elapsed:.2?}");
}
match result { match result {
Some(item) if !item.is_deleted => Ok(item), Some(item) if !item.is_deleted => Ok(item),
_ => bail!("Gacha Item not found"), _ => bail!("Gacha Item not found"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_create_gacha_item( pub async fn query_create_gacha_item(
&self, &self,
data: GachaItemSchema, data: GachaItemSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let surreal_query = format!("CREATE {} CONTENT ...", ResourceEnum::GachaItems.to_string());
info!(query = %surreal_query, "Executing SurrealDB query");
let record: Option<GachaItemSchema> = db let record: Option<GachaItemSchema> = db
.create(ResourceEnum::GachaItems.to_string()) .create(ResourceEnum::GachaItems.to_string())
.content(data) .content(data)
.await?; .await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_gacha_item' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success create Gacha Item".into()), Some(_) => Ok("Success create Gacha Item".into()),
None => bail!("Failed to create Gacha Item"), None => bail!("Failed to create Gacha Item"),
} }
} }
#[instrument(skip(self, data), err)]
pub async fn query_update_gacha_item( pub async fn query_update_gacha_item(
&self, &self,
data: GachaItemSchema, data: GachaItemSchema,
) -> Result<String> { ) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let record_key = get_id(&data.id)?; let record_key = get_id(&data.id)?;
let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?; let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?;
@@ -81,15 +121,25 @@ impl<'a> GachaItemRepository<'a> {
created_at: existing.created_at, created_at: existing.created_at,
..data.clone() ..data.clone()
}; };
let surreal_query = format!("UPDATE {:?} MERGE ...", record_key);
info!(query = %surreal_query, "Executing SurrealDB query");
let record: Option<GachaItemSchema> = let record: Option<GachaItemSchema> =
db.update(record_key).merge(merged).await?; db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_gacha_item' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success update Gacha Item".into()), Some(_) => Ok("Success update Gacha Item".into()),
None => bail!("Failed to update Gacha Item"), None => bail!("Failed to update Gacha Item"),
} }
} }
#[instrument(skip(self, id), err)]
pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> { pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws; let db = &self.state.surrealdb_ws;
let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id); let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?; let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?;
@@ -97,13 +147,22 @@ impl<'a> GachaItemRepository<'a> {
bail!("Gacha Item already deleted"); bail!("Gacha Item already deleted");
} }
let record_key = get_id(&item.id)?; let record_key = get_id(&item.id)?;
let record: Option<GachaItemSchema> = db let mut patch = Map::new();
.update(record_key) patch.insert("is_deleted".to_string(), Value::Bool(true));
.merge(serde_json::json!({ "is_deleted": true })) patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
.await?;
let surreal_query = format!("UPDATE {:?} MERGE ...", record_key);
info!(query = %surreal_query, "Executing SurrealDB query");
let record: Option<GachaItemSchema> = db.update(record_key).merge(patch).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_delete_gacha_item' took: {elapsed:.2?}");
}
match record { match record {
Some(_) => Ok("Success delete Gacha Item".into()), Some(_) => Ok("Success soft delete Gacha Item".into()),
None => bail!("Failed to delete Gacha Item"), None => bail!("Failed to soft delete Gacha Item"),
} }
} }
} }
Loaded 50 of 132 files, more files were not shown because too many files have changed in this diff. Show more