Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2e0c58b6 | ||
|
|
e66f1f1634 | ||
|
|
6f45cf80c8 | ||
|
|
02be09506a | ||
|
|
b88ecd1361 | ||
|
|
fa435994f2 | ||
|
|
16c613d699 | ||
|
|
f598dedb3b | ||
|
|
cfa251337f | ||
|
|
cca9aa6318 | ||
|
|
f1e4e02697 | ||
|
|
a3c2715a85 | ||
|
|
89cc631fb0 | ||
|
|
36c12110c2 | ||
|
|
070060365a | ||
|
|
68e816f956 | ||
|
|
48db5f07ef | ||
|
|
a4b5d91906 | ||
|
|
9878279660 | ||
|
|
465cbee2c3 | ||
|
|
33a2ae86c6 | ||
|
|
d906fc4747 | ||
|
|
055ef6ac8e | ||
|
|
e63658c082 | ||
|
|
f4b0ed0a4a |
+24
-8
@@ -1,8 +1,24 @@
|
||||
PORT=
|
||||
SURREALDB_URL=
|
||||
SURREALDB_USERNAME=
|
||||
SURREALDB_PASSWORD=
|
||||
SURREALDB_NAMESPACE=
|
||||
SURREALDB_DBNAME=
|
||||
ACCESS_TOKEN_SECRET=
|
||||
REFRESH_TOKEN_SECRET=
|
||||
RUST_ENV=development
|
||||
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=your-minio-access-key
|
||||
MINIO_SECRET_KEY=your-minio-secret-key
|
||||
MAIL_USER=your-email@example.com
|
||||
MAIL_PASSWORD=your-smtp-password
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=465
|
||||
MAIL_SECURE=true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Deploy
|
||||
name: Deploy to Ancikri
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -25,10 +25,10 @@ jobs:
|
||||
- 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 }}
|
||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||
script: |
|
||||
set -e
|
||||
echo "Stopping the service before uploading the binary"
|
||||
@@ -37,10 +37,10 @@ jobs:
|
||||
- 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 }}
|
||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||
source: ./target/release/*
|
||||
target: /opt/imphnen-backend-service/imphnen-backend-service
|
||||
rm: true
|
||||
@@ -49,10 +49,10 @@ jobs:
|
||||
- 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 }}
|
||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||
script: |
|
||||
set -e
|
||||
|
||||
Generated
+807
-638
File diff suppressed because it is too large
Load Diff
+40
-11
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"tests",
|
||||
"imphnen-iam",
|
||||
"imphnen-cms",
|
||||
"imphnen-libs",
|
||||
"imphnen-utils",
|
||||
"imphnen-gacha",
|
||||
@@ -15,30 +16,58 @@ members = [
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
axum = { version = "0.8.1", features = ["multipart"] }
|
||||
axum = { version = "0.8.4", features = ["multipart"] }
|
||||
log = "0.4.25"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.138"
|
||||
tokio = { version = "1.43.0" }
|
||||
tokio = { version = "1.45.0", features = ["full"] }
|
||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||
jsonwebtoken = "9.3.1"
|
||||
chrono = "0.4.39"
|
||||
chrono = "0.4.41"
|
||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
||||
lettre = { version = "0.11.12", features = ["tokio1-native-tls"] }
|
||||
surrealdb = { version = "2.2.1", features = ["kv-mem"] }
|
||||
thiserror = "2.0.11"
|
||||
anyhow = "1.0.97"
|
||||
rand = "0.9.0"
|
||||
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||
validator = { version = "0.12", features = ["derive"] }
|
||||
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] }
|
||||
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
||||
thiserror = "2.0.12"
|
||||
anyhow = "1.0.98"
|
||||
rand = { version = "0.9.1", features = ["std", "alloc"] }
|
||||
rand_distr = "0.5.1"
|
||||
tower-http = { version = "0.6.4", features = ["cors", "trace"] }
|
||||
http-body-util = "0.1.1"
|
||||
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.11.1"
|
||||
axum-test = "17.2.0"
|
||||
fancy-regex = "0.14.0"
|
||||
fancy-regex = "0.15.0"
|
||||
futures = "0.3.31"
|
||||
tower = "0.5.2"
|
||||
env_logger = "0.11.8"
|
||||
dotenvy = "0.15.7"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
once_cell = "1.21.3"
|
||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] }
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
strum_macros = "0.27.1"
|
||||
hyper = "1.6.0"
|
||||
hyper-util = "0.1.0"
|
||||
async-trait = "0.1.75"
|
||||
tokio-tungstenite = "0.23"
|
||||
url = "2.5"
|
||||
futures-util = "0.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]
|
||||
lto = "fat"
|
||||
|
||||
+37
-12
@@ -1,22 +1,47 @@
|
||||
FROM rust:1.83-slim-bullseye AS builder
|
||||
FROM rust:1.86-alpine AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libssl-dev \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache \
|
||||
curl \
|
||||
musl-dev \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
pkgconfig
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./src ./src
|
||||
|
||||
RUN cargo build --release && strip /app/target/release/imphnen-backend-service
|
||||
RUN mkdir -p imphnen-backend/src imphnen-cms/src imphnen-dimentorin/src \
|
||||
imphnen-entities/src imphnen-gacha/src imphnen-gateway/src \
|
||||
imphnen-iam/src imphnen-libs/src imphnen-middleware/src \
|
||||
imphnen-utils/src tests/src && \
|
||||
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
||||
find . -name "src" -type d -exec sh -c 'echo "// dummy" > "$1/lib.rs"' _ {} \;
|
||||
|
||||
FROM gcr.io/distroless/cc AS runner
|
||||
RUN echo '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/target/release/imphnen-backend-service .
|
||||
RUN echo -e '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||
|
||||
CMD ["/app/imphnen-backend-service"]
|
||||
|
||||
COPY imphnen-backend ./imphnen-backend
|
||||
COPY imphnen-cms ./imphnen-cms
|
||||
COPY imphnen-dimentorin ./imphnen-dimentorin
|
||||
COPY imphnen-entities ./imphnen-entities
|
||||
COPY imphnen-gacha ./imphnen-gacha
|
||||
COPY imphnen-gateway ./imphnen-gateway
|
||||
COPY imphnen-iam ./imphnen-iam
|
||||
COPY imphnen-libs ./imphnen-libs
|
||||
COPY imphnen-middleware ./imphnen-middleware
|
||||
COPY imphnen-utils ./imphnen-utils
|
||||
COPY tests ./tests
|
||||
|
||||
RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-units=1 -C strip=symbols" \
|
||||
cargo build -p imphnen-backend --release && \
|
||||
strip target/release/api && \
|
||||
upx --best --lzma target/release/api 2>/dev/null || true
|
||||
|
||||
FROM scratch AS runner
|
||||
COPY --from=builder /app/target/release/api /api
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
ENTRYPOINT ["/api"]
|
||||
@@ -6,12 +6,13 @@
|
||||
|
||||
This repository serves as the **monorepo** for all backend services of IMPHNEN. It encompasses several main services:
|
||||
|
||||
1. **Core Service** - Provides fundamental functionalities and shared resources for other services.
|
||||
2. **IAM Service** - Handles identity and access management across IMPHNEN applications.
|
||||
3. **CMS Service** - Supports the cms services by IMPHNEN [Landing Page website](https://imphnen.dev/).
|
||||
4. **Gacha Service** - Supports the gacha services by IMPHNEN [Gacha website](https://gacha.imphnen.dev/).
|
||||
5. **Dimentorin Service** - Supports the mentoring services by IMPHNEN [Dimentorin website](https://dimentorin.imphnen.dev/).
|
||||
6. **Gateway Service** - Acts as the API gateway, routing requests to appropriate services.
|
||||
1. **IMPHNEN-Backend** - Provides fundamental functionalities and shared resources for other services.
|
||||
2. **IMPHNEN-IAM** - Handles identity and access management across IMPHNEN applications.
|
||||
3. **IMPHNEN-CMS** - Supports the cms services by IMPHNEN [Landing Page website](https://imphnen.dev/).
|
||||
4. **IMPHNEN-Gacha** - Supports the gacha services by IMPHNEN [Gacha website](https://gacha.imphnen.dev/).
|
||||
5. **IMPHNEN-Dimentorin** - Supports the mentoring services by IMPHNEN [Dimentorin website](https://dimentorin.imphnen.dev/).
|
||||
6. **IMPHNEN-Gateway** - Acts as the API gateway, routing requests to appropriate services.
|
||||
7. **IMPHNEN-Middleware** - Acts as the middleware for the API Gateway, providing authentication and authorization.
|
||||
|
||||
## How to Install
|
||||
|
||||
@@ -39,7 +40,7 @@ This repository serves as the **monorepo** for all backend services of IMPHNEN.
|
||||
if you use unix based system
|
||||
|
||||
```sh
|
||||
sh apply-env.sh
|
||||
source ./apply-env.sh
|
||||
```
|
||||
|
||||
- Modify the `.env` files with your specific configuration settings.
|
||||
@@ -49,7 +50,15 @@ This repository serves as the **monorepo** for all backend services of IMPHNEN.
|
||||
Ensure you have [Rust](https://www.rust-lang.org/) installed. Then, run:
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
cargo fetch
|
||||
```
|
||||
|
||||
4. **Run the seeders**:
|
||||
|
||||
to run the seeders, run:
|
||||
|
||||
```sh
|
||||
cargo run --bin seeder
|
||||
```
|
||||
|
||||
## How to Run
|
||||
@@ -64,10 +73,16 @@ To run the services in development mode:
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Run the desired service**. For example, to run the Core Service:
|
||||
2. **Run using cargo run**. For example, to run the Core Service:
|
||||
|
||||
```sh
|
||||
cargo run -p imphnen-core-service --bin api
|
||||
cargo run --bin api
|
||||
```
|
||||
|
||||
3. **Run using cargo watch**. For example, to run the Core Service:
|
||||
|
||||
```sh
|
||||
cargo watch -x "run --bin api"
|
||||
```
|
||||
|
||||
### Production
|
||||
@@ -77,17 +92,25 @@ For production deployment:
|
||||
1. **Build the Docker image**:
|
||||
|
||||
```sh
|
||||
docker build -t imphnen-backend-service .
|
||||
docker build -t imphnen-backend .
|
||||
```
|
||||
|
||||
2. **Run the Docker container**:
|
||||
|
||||
```sh
|
||||
docker run -d --env-file .env -p 8080:8080 imphnen-backend-service
|
||||
docker run --name imphnen-backend -d --env-file .env -p 3000:3000 imphnen-backend
|
||||
```
|
||||
|
||||
Adjust the port and environment variables as needed.
|
||||
|
||||
## How to Run the Tests
|
||||
|
||||
1. **Run the tests**:
|
||||
|
||||
```sh
|
||||
cargo test -p tests
|
||||
```
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Fork the repository** and clone it locally.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
:: Cek apakah file .env ada
|
||||
if not exist ".env" (
|
||||
echo File .env tidak ditemukan di direktori saat ini.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Memuat variabel dari .env...
|
||||
|
||||
:: Baca file .env baris per baris
|
||||
for /f "tokens=*" %%a in ('type ".env" ^| findstr /v "^$" ^| findstr /v "^#"') do (
|
||||
echo.%%a | findstr "=" >nul && (
|
||||
for /f "tokens=1,2 delims==" %%b in ("%%a") do (
|
||||
set "key=%%b"
|
||||
set "value=%%c"
|
||||
:: Trim whitespace
|
||||
call :trimValue key value
|
||||
echo Set variabel: %%b=%%c
|
||||
setx %%b %%c >nul
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Semua variabel telah dimuat.
|
||||
endlocal
|
||||
goto :eof
|
||||
|
||||
:: Fungsi trim (sederhana)
|
||||
:trimValue
|
||||
set "%1=%[%1]%"
|
||||
set "%2=%[%2]%"
|
||||
goto :eof
|
||||
@@ -4,11 +4,13 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-gateway = { version = "0.1.0", path = "../imphnen-gateway" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-gateway.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -25,3 +27,9 @@ anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
env_logger.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace=true
|
||||
tokio-tungstenite.workspace = true
|
||||
url.workspace = true
|
||||
futures-util.workspace = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use env_logger;
|
||||
use imphnen_gateway::gateway_service;
|
||||
use imphnen_libs::axum_init;
|
||||
|
||||
|
||||
@@ -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).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)).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)).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)).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)).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)).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(())
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.await?;
|
||||
|
||||
let events = vec![
|
||||
(
|
||||
"Tech Conference 2025",
|
||||
"Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.",
|
||||
"https://techconf2025.example.com",
|
||||
150.0,
|
||||
Some("Jakarta Convention Center".to_string()),
|
||||
false,
|
||||
"2025-06-15T09:00:00Z",
|
||||
"2025-06-17T18:00:00Z",
|
||||
),
|
||||
(
|
||||
"Online Web Development Workshop",
|
||||
"Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.",
|
||||
"https://webdev-workshop.example.com",
|
||||
75.0,
|
||||
None,
|
||||
true,
|
||||
"2025-07-10T14:00:00Z",
|
||||
"2025-07-10T17:00:00Z",
|
||||
),
|
||||
(
|
||||
"Startup Pitch Competition",
|
||||
"Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.",
|
||||
"https://startup-pitch.example.com",
|
||||
25.0,
|
||||
Some("Innovation Hub Surabaya".to_string()),
|
||||
false,
|
||||
"2025-08-05T10:00:00Z",
|
||||
"2025-08-05T16:00:00Z",
|
||||
),
|
||||
(
|
||||
"Digital Marketing Masterclass",
|
||||
"Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.",
|
||||
"https://digital-marketing.example.com",
|
||||
100.0,
|
||||
None,
|
||||
true,
|
||||
"2025-09-20T13:00:00Z",
|
||||
"2025-09-22T15:00:00Z",
|
||||
),
|
||||
];
|
||||
|
||||
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 {
|
||||
id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
detail_link: detail_link.into(),
|
||||
price,
|
||||
location,
|
||||
is_online,
|
||||
is_deleted: false,
|
||||
start_date: start_date.into(),
|
||||
end_date: end_date.into(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
|
||||
db.create::<Option<EventsSchema>>(("app_events", uuid.as_str())) // Use generated UUID
|
||||
.content(event)
|
||||
.await?;
|
||||
|
||||
println!(
|
||||
"✅ Inserted event: {} ({})",
|
||||
name,
|
||||
if is_online { "Online" } else { "In-person" }
|
||||
);
|
||||
}
|
||||
|
||||
println!("✅ All Events seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
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)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.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_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
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)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.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, created_at = $created_at, updated_at = $updated_at")
|
||||
.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(("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, legal_name = $legal_name, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, linkedin_url = $linkedin_url, github_url = $github_url, cv_url = $cv_url, 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, status = $status, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at, email = $email")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))))
|
||||
.bind(("legal_name", "Mentor User"))
|
||||
.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(("linkedin_url", "https://linkedin.com/in/mentor"))
|
||||
.bind(("github_url", "https://github.com/mentor"))
|
||||
.bind(("cv_url", Option::<String>::None))
|
||||
.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()))
|
||||
.bind(("email", "mentor@example.com"))
|
||||
.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(())
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal};
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
|
||||
let env = Env::new();
|
||||
let db = Surreal::new::<Ws>(env.surrealdb_url).await?;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
@@ -15,131 +20,59 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.await?;
|
||||
let permissions = vec![
|
||||
(
|
||||
"023e2dfe-93c3-4008-94a8-b5dff403f73b",
|
||||
"Create Users",
|
||||
Some("2025-01-29T06:08:23.838311+00"),
|
||||
Some("2025-01-29T06:08:23.838312+00"),
|
||||
),
|
||||
(
|
||||
"0269ed71-0ae0-4c43-ad29-e3d861d8f9a0",
|
||||
"Create Permissions",
|
||||
Some("2025-01-29T05:11:01.265+00"),
|
||||
Some("2025-01-29T05:11:01.265001+00"),
|
||||
),
|
||||
(
|
||||
"299cb4d5-6556-4cc9-b6c1-32e6d31e0f9b",
|
||||
"Update Permissions",
|
||||
Some("2025-01-29T05:11:01.265+00"),
|
||||
Some("2025-01-29T05:11:01.265001+00"),
|
||||
),
|
||||
(
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a2",
|
||||
"Create Roles",
|
||||
Some("2025-01-29T05:11:01.265+00"),
|
||||
Some("2025-01-29T05:11:01.265001+00"),
|
||||
),
|
||||
(
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6",
|
||||
"Read Detail Users",
|
||||
Some("2025-01-29T05:11:01.265+00"),
|
||||
Some("2025-01-29T05:11:01.265001+00"),
|
||||
),
|
||||
(
|
||||
"35b0d992-65c8-4b62-b030-e6e0320e4048",
|
||||
"Delete Roles",
|
||||
Some("2025-01-29T05:34:40.621554+00"),
|
||||
Some("2025-01-29T05:34:40.621555+00"),
|
||||
),
|
||||
(
|
||||
"4da8b434-89f9-4d91-85ae-eebd63cdbeda",
|
||||
"Update Activate Users",
|
||||
Some("2025-02-01T12:38:09.741726+00"),
|
||||
Some("2025-02-01T12:38:09.741727+00"),
|
||||
),
|
||||
(
|
||||
"73888d18-b3e9-4f62-95a5-ba2c0d69fccb",
|
||||
"Read Detail Roles",
|
||||
Some("2025-01-29T05:13:06.445925+00"),
|
||||
Some("2025-01-29T10:31:46.408564+00"),
|
||||
),
|
||||
(
|
||||
"7c15e31d-36e2-49f9-97db-138c03fb0cf6",
|
||||
"Read List Users",
|
||||
Some("2025-01-28T15:02:41.772931+00"),
|
||||
Some("2025-01-28T15:02:41.772933+00"),
|
||||
),
|
||||
(
|
||||
"7d4b1379-4960-416a-b045-98cd82c0cac9",
|
||||
"Read Detail Sessions",
|
||||
Some("2025-02-24T16:52:26.886664+00"),
|
||||
Some("2025-02-24T16:52:26.886673+00"),
|
||||
),
|
||||
(
|
||||
"8195eeb8-e64f-4172-aa57-596492c84a72",
|
||||
"Read List Permissions",
|
||||
Some("2025-01-28T15:05:28.6299+00"),
|
||||
Some("2025-01-28T15:05:28.629901+00"),
|
||||
),
|
||||
(
|
||||
"81eba91d-b8ab-44b9-bbfe-4e6da2f98952",
|
||||
"Read List Tests",
|
||||
Some("2025-02-24T16:52:27.179542+00"),
|
||||
Some("2025-02-24T16:52:27.179551+00"),
|
||||
),
|
||||
(
|
||||
"9164ca6e-c7e3-4238-a15f-f36ab9577e7e",
|
||||
"Read List Roles",
|
||||
Some("2025-01-29T05:34:40.621554+00"),
|
||||
Some("2025-01-29T05:34:40.621555+00"),
|
||||
),
|
||||
(
|
||||
"96df0689-2ae9-4894-bf00-837c19415e5c",
|
||||
"Delete Users",
|
||||
Some("2025-02-02T06:52:05.195565+00"),
|
||||
Some("2025-02-02T06:52:05.195565+00"),
|
||||
),
|
||||
(
|
||||
"98b3dc4c-0124-461f-afcd-166637c5e6e8",
|
||||
"Update Users",
|
||||
Some("2025-01-29T05:34:40.621554+00"),
|
||||
Some("2025-01-29T05:34:40.621555+00"),
|
||||
),
|
||||
(
|
||||
"a00d5608-4c48-4542-845c-dfe004687022",
|
||||
"Update Roles",
|
||||
Some("2025-01-29T05:34:40.621554+00"),
|
||||
Some("2025-01-29T05:34:40.621555+00"),
|
||||
),
|
||||
(
|
||||
"b2dc3928-86ba-4c59-a03d-0b57d5183ebc",
|
||||
"Delete Permissions",
|
||||
Some("2025-01-29T05:14:22.511084+00"),
|
||||
Some("2025-01-29T05:14:22.511085+00"),
|
||||
),
|
||||
(
|
||||
"dad435cf-042c-41bd-a946-cea61ed2ffbc",
|
||||
"Read Detail Permissions",
|
||||
Some("2025-01-28T15:07:10.990214+00"),
|
||||
Some("2025-01-28T15:07:10.990214+00"),
|
||||
),
|
||||
];
|
||||
for (id, name, _created_at, _updated_at) in permissions {
|
||||
|
||||
for permission in [
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::CreateUsers,
|
||||
PermissionsEnum::DeleteUsers,
|
||||
PermissionsEnum::UpdateUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::CreateRoles,
|
||||
PermissionsEnum::DeleteRoles,
|
||||
PermissionsEnum::UpdateRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::CreatePermissions,
|
||||
PermissionsEnum::DeletePermissions,
|
||||
PermissionsEnum::UpdatePermissions,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::CreateGachaItems,
|
||||
PermissionsEnum::DeleteGachaItems,
|
||||
PermissionsEnum::UpdateGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::RegisterMentors,
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::UpdateOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
] {
|
||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||
.bind(("id", id))
|
||||
.bind(("id", permission.id()))
|
||||
.bind((
|
||||
"data",
|
||||
json!({
|
||||
"name": name,
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date()
|
||||
"name": permission.to_string(),
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date()
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
println!("✅ Inserted: {}", name);
|
||||
println!("✅ Inserted: {permission}");
|
||||
}
|
||||
|
||||
println!("✅ All Permissions seeded");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, Env};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal};
|
||||
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let db = Surreal::new::<Ws>(env.surrealdb_url).await?;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
@@ -47,9 +49,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
None,
|
||||
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 {
|
||||
db.query("DELETE type::thing('app_roles', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
|
||||
.bind(("id", id))
|
||||
.bind((
|
||||
@@ -63,7 +74,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
println!("✅ Inserted role: {}", name);
|
||||
println!("✅ Inserted role: {name}");
|
||||
}
|
||||
println!("✅ All Roles seeded");
|
||||
Ok(())
|
||||
|
||||
@@ -1,57 +1,134 @@
|
||||
use imphnen_iam::{get_iso_date, make_thing, Env};
|
||||
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum};
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use std::error::Error;
|
||||
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal};
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let db = Surreal::new::<Ws>(env.surrealdb_url).await?;
|
||||
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
|
||||
db.use_ns(env.surrealdb_namespace)
|
||||
.use_db(env.surrealdb_dbname)
|
||||
.await?;
|
||||
|
||||
let admin_permissions = vec![
|
||||
"023e2dfe-93c3-4008-94a8-b5dff403f73b",
|
||||
"0269ed71-0ae0-4c43-ad29-e3d861d8f9a0",
|
||||
"299cb4d5-6556-4cc9-b6c1-32e6d31e0f9b",
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a2",
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6",
|
||||
"35b0d992-65c8-4b62-b030-e6e0320e4048",
|
||||
"4da8b434-89f9-4d91-85ae-eebd63cdbeda",
|
||||
"73888d18-b3e9-4f62-95a5-ba2c0d69fccb",
|
||||
"7c15e31d-36e2-49f9-97db-138c03fb0cf6",
|
||||
"7d4b1379-4960-416a-b045-98cd82c0cac9",
|
||||
"8195eeb8-e64f-4172-aa57-596492c84a72",
|
||||
"81eba91d-b8ab-44b9-bbfe-4e6da2f98952",
|
||||
"9164ca6e-c7e3-4238-a15f-f36ab9577e7e",
|
||||
"96df0689-2ae9-4894-bf00-837c19415e5c",
|
||||
"98b3dc4c-0124-461f-afcd-166637c5e6e8",
|
||||
"a00d5608-4c48-4542-845c-dfe004687022",
|
||||
"b2dc3928-86ba-4c59-a03d-0b57d5183ebc",
|
||||
"dad435cf-042c-41bd-a946-cea61ed2ffbc",
|
||||
let roles_permissions = vec![
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::CreateUsers,
|
||||
PermissionsEnum::DeleteUsers,
|
||||
PermissionsEnum::UpdateUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::CreateRoles,
|
||||
PermissionsEnum::DeleteRoles,
|
||||
PermissionsEnum::UpdateRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::CreatePermissions,
|
||||
PermissionsEnum::DeletePermissions,
|
||||
PermissionsEnum::UpdatePermissions,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::CreateGachaItems,
|
||||
PermissionsEnum::DeleteGachaItems,
|
||||
PermissionsEnum::UpdateGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::RegisterMentors,
|
||||
PermissionsEnum::UpdateMentors,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
],
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
vec![
|
||||
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::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![]),
|
||||
];
|
||||
|
||||
let admin_role_id = "f6b03f25-e416-4893-ac88-caaa690afb07";
|
||||
for (role_id, permissions) in roles_permissions {
|
||||
let permission_refs: Vec<_> = permissions
|
||||
.iter()
|
||||
.map(|perm| make_thing("app_permissions", perm.id()))
|
||||
.collect();
|
||||
|
||||
let permission_refs_admin: Vec<_> = admin_permissions
|
||||
.into_iter()
|
||||
.map(|perm_id| 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", admin_role_id))
|
||||
.bind(("permissions", permission_refs_admin))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
|
||||
println!("✅ All permissions successfully added to each role");
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use imphnen_iam::UsersSchema;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||
use std::error::Error;
|
||||
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, sql::Thing, Surreal};
|
||||
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let db = Surreal::new::<Ws>(env.surrealdb_url).await?;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
@@ -38,6 +41,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
];
|
||||
|
||||
for (id, email, fullname, role_id) in users {
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
|
||||
let user = UsersSchema {
|
||||
id: Thing::from(("app_users", id)),
|
||||
fullname: fullname.into(),
|
||||
@@ -47,6 +54,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
phone_number: "081234567890".into(),
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
mentor_id: None,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
role: Thing::from(("app_roles", role_id)),
|
||||
@@ -58,7 +66,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
.content(user)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted user: {} ({})", fullname, email);
|
||||
println!("✅ Inserted user: {fullname} ({email})");
|
||||
}
|
||||
|
||||
println!("✅ All Users seeded");
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
|
||||
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
||||
println!("🔧 Seeding: {bin}");
|
||||
let status = Command::new("cargo").args(["run", "--bin", bin]).status()?;
|
||||
|
||||
if !status.success() {
|
||||
Err(format!("❌ Failed to run seed: {bin}").into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
println!("🚀 Running all seeders...\n");
|
||||
|
||||
run_seed("seed_permissions")?;
|
||||
run_seed("seed_roles")?;
|
||||
run_seed("seed_roles_permissions")?;
|
||||
run_seed("seed_users")?;
|
||||
run_seed("seed_events")?;
|
||||
run_seed("seed_gacha_rolls")?;
|
||||
run_seed("seed_mentor_user")?;
|
||||
println!("\n✅ All seeding completed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,6 +3,8 @@ use imphnen_libs::axum_init;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||
})
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "imphnen-cms"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
axum-test.workspace = true
|
||||
surrealdb.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
VALID_URL_REGEX = "^https?://"
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod v1;
|
||||
pub use v1::*;
|
||||
@@ -0,0 +1,119 @@
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_service::EventsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, Json};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::get_event_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::get_event_by_id(&state, id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/create",
|
||||
request_body = EventsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Create new event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn post_create_event(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<EventsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::create_event(&state, payload).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
request_body = EventsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Update event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn patch_update_event(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<EventsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::update_event(&state, id, payload).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Soft delete event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn delete_event(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::delete_event(&state, id).await
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use lazy_static::lazy_static;
|
||||
lazy_static! {
|
||||
pub static ref VALID_URL_REGEX: regex::Regex =
|
||||
regex::Regex::new(r"^https?://").unwrap();
|
||||
}
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsCreateRequestDto {
|
||||
#[validate(length(min = 1, message = "Name is required"))]
|
||||
pub name: String,
|
||||
|
||||
#[validate(length(min = 1, message = "Description is required"))]
|
||||
pub description: String,
|
||||
|
||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
||||
pub detail_link: String,
|
||||
|
||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
||||
pub price: f64,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
pub location: Option<String>,
|
||||
pub is_online: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsUpdateRequestDto {
|
||||
#[validate(length(min = 1, message = "Name is required"))]
|
||||
pub name: String,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub description: String,
|
||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsListItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub location: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsDetailItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsQueryDto {
|
||||
pub id: Thing,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl EventsQueryDto {
|
||||
pub fn from(self) -> EventsListItemDto {
|
||||
EventsListItemDto {
|
||||
id: self.id.id.to_raw(),
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
detail_link: self.detail_link,
|
||||
price: self.price,
|
||||
location: self.location,
|
||||
is_online: self.is_online,
|
||||
start_date: self.start_date,
|
||||
end_date: self.end_date,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct EventsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> EventsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_event_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let query = ListQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
let res: Vec<EventsQueryDto> =
|
||||
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 {
|
||||
data: res,
|
||||
meta: None,
|
||||
};
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||
.with_id(&id)
|
||||
.with_select_fields(vec!["*"]);
|
||||
let sql = builder.build();
|
||||
let result: Option<EventsQueryDto> =
|
||||
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 {
|
||||
Some(event) => {
|
||||
if event.is_deleted {
|
||||
bail!("Event not found");
|
||||
}
|
||||
Ok(event)
|
||||
}
|
||||
None => bail!("Event not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<EventsSchema> = db
|
||||
.create(ResourceEnum::Events.to_string())
|
||||
.content(data)
|
||||
.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 {
|
||||
Some(_) => Ok("Success create event".into()),
|
||||
None => bail!("Failed to create event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let existing = self.query_event_by_id(data.id.id.to_raw()).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Event already deleted");
|
||||
}
|
||||
|
||||
let merged = EventsSchema {
|
||||
created_at: existing.created_at,
|
||||
updated_at: get_iso_date(),
|
||||
..data
|
||||
};
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
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 {
|
||||
Some(_) => Ok("Success update event".into()),
|
||||
None => bail!("Failed to update event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_event(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let event = self.query_event_by_id(id).await?;
|
||||
if event.is_deleted {
|
||||
bail!("Event not found");
|
||||
}
|
||||
|
||||
let record_key = get_id(&event.id)?;
|
||||
let record: Option<EventsSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.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 {
|
||||
Some(_) => Ok("Success delete event".into()),
|
||||
None => bail!("Failed to delete event"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use super::events_dto::{
|
||||
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsSchema {
|
||||
pub id: Thing,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub name: String,
|
||||
pub end_date: String,
|
||||
pub start_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for EventsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Events.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
detail_link: String::new(),
|
||||
price: 0.0,
|
||||
location: None,
|
||||
is_online: false,
|
||||
is_deleted: false,
|
||||
start_date: String::new(),
|
||||
end_date: String::new(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsSchema {
|
||||
pub fn from(dto: EventsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
detail_link: dto.detail_link,
|
||||
price: dto.price,
|
||||
location: dto.location,
|
||||
is_online: dto.is_online,
|
||||
is_deleted: false,
|
||||
start_date: dto.start_date,
|
||||
end_date: dto.end_date,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: EventsCreateRequestDto) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Events.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
is_deleted: false,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(payload: EventsUpdateRequestDto, id: String) -> Self {
|
||||
Self {
|
||||
id: make_thing(&ResourceEnum::Events.to_string(), &id),
|
||||
name: payload.name,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
updated_at: get_iso_date(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_repository::EventsRepository,
|
||||
events_schema::EventsSchema,
|
||||
};
|
||||
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;
|
||||
|
||||
impl EventsService {
|
||||
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<EventsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(EventsQueryDto::from)
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_by_id(id).await {
|
||||
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: EventsDetailItemDto {
|
||||
id: event.id.id.to_raw(),
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
start_date: event.start_date,
|
||||
end_date: event.end_date,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.updated_at,
|
||||
location: event.location,
|
||||
},
|
||||
}),
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_event(
|
||||
state: &AppState,
|
||||
payload: EventsCreateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
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 {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
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 {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_delete_event(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod events_controller;
|
||||
pub mod events_dto;
|
||||
pub mod events_repository;
|
||||
pub mod events_schema;
|
||||
pub mod events_service;
|
||||
|
||||
pub use events_controller::*;
|
||||
pub use events_dto::*;
|
||||
pub use events_repository::*;
|
||||
pub use events_schema::*;
|
||||
pub use events_service::*;
|
||||
|
||||
pub fn events_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/events",
|
||||
get(events_controller::get_event_list),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/events/detail/{id}",
|
||||
get(events_controller::get_event_by_id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn events_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/events/create",
|
||||
post(events_controller::post_create_event),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/events/update/{id}",
|
||||
patch(events_controller::patch_update_event),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/events/delete/{id}",
|
||||
delete(events_controller::delete_event),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
|
||||
pub use events::*;
|
||||
pub use testimonials::*;
|
||||
@@ -0,0 +1,44 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod testimonials_controller;
|
||||
pub mod testimonials_dto;
|
||||
pub mod testimonials_repository;
|
||||
pub mod testimonials_schema;
|
||||
pub mod testimonials_service;
|
||||
|
||||
pub use testimonials_controller::*;
|
||||
pub use testimonials_dto::*;
|
||||
pub use testimonials_repository::*;
|
||||
pub use testimonials_schema::*;
|
||||
pub use testimonials_service::*;
|
||||
|
||||
pub fn testimonials_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/testimonials",
|
||||
get(testimonials_controller::get_testimonial_list),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/testimonials/detail/{id}",
|
||||
get(testimonials_controller::get_testimonial_by_id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn testimonials_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/cms/landing/testimonials/create",
|
||||
post(testimonials_controller::post_create_testimonial),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/testimonials/update/{id}",
|
||||
patch(testimonials_controller::patch_update_testimonial),
|
||||
)
|
||||
.route(
|
||||
"/cms/landing/testimonials/delete/{id}",
|
||||
delete(testimonials_controller::delete_testimonial),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_service::TestimonialsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, Json};
|
||||
use imphnen_iam::UsersDetailQueryDto;
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::get_testimonial_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::get_testimonial_by_id(&state, id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/create",
|
||||
request_body = TestimonialsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Create new testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn post_create_testimonial(
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Json(payload): Json<TestimonialsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
request_body = TestimonialsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Update testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn patch_update_testimonial(
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Json(payload): Json<TestimonialsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user)
|
||||
.await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Soft delete testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn delete_testimonial(
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use imphnen_iam::users::UsersSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsCreateRequestDto {
|
||||
#[validate(length(min = 1, message = "Role is required"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 500,
|
||||
message = "Content must be between 1 and 500 characters"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsUpdateRequestDto {
|
||||
#[validate(length(min = 1, message = "Role is required"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 500,
|
||||
message = "Content must be between 1 and 500 characters"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsListItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsDetailItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsQueryDto {
|
||||
pub id: Thing,
|
||||
pub user: UsersSchema,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl TestimonialsQueryDto {
|
||||
pub fn from(self) -> TestimonialsListItemDto {
|
||||
TestimonialsListItemDto {
|
||||
id: self.id.id.to_raw(),
|
||||
user_id: self.user.id.id.to_raw(),
|
||||
user_fullname: self.user.fullname,
|
||||
role: self.role,
|
||||
content: self.content,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use super::{
|
||||
testimonials_dto::TestimonialsQueryDto, testimonials_schema::TestimonialsSchema,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||
use serde_json;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct TestimonialsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> TestimonialsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_testimonial_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let query = ListQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||
.with_select_fields(vec!["*", "user.* as user"])
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
let res: Vec<TestimonialsQueryDto> =
|
||||
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 {
|
||||
data: res,
|
||||
meta: None,
|
||||
};
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_testimonial_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<TestimonialsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||
.with_id(&id)
|
||||
.with_condition("is_deleted = false")
|
||||
.with_select_fields(vec!["*", "user.* as user"]);
|
||||
let sql = builder.build();
|
||||
let result: Option<TestimonialsQueryDto> =
|
||||
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 {
|
||||
Some(testimonial) => {
|
||||
if testimonial.is_deleted {
|
||||
bail!("Testimonial not found");
|
||||
}
|
||||
Ok(testimonial)
|
||||
}
|
||||
None => bail!("Testimonial not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<TestimonialsSchema> = db
|
||||
.create(ResourceEnum::Testimonials.to_string())
|
||||
.content(data)
|
||||
.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 {
|
||||
Some(_) => Ok("Success create testimonial".into()),
|
||||
None => bail!("Failed to create testimonial"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Testimonial already deleted");
|
||||
}
|
||||
|
||||
let merged = TestimonialsSchema {
|
||||
created_at: existing.created_at,
|
||||
updated_at: get_iso_date(),
|
||||
user: existing.user.id,
|
||||
..data
|
||||
};
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
let record: Option<TestimonialsSchema> =
|
||||
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 {
|
||||
Some(_) => Ok("Success update testimonial".into()),
|
||||
None => bail!("Failed to update testimonial"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let testimonial = self.query_testimonial_by_id(id).await?;
|
||||
if testimonial.is_deleted {
|
||||
bail!("Testimonial not found");
|
||||
}
|
||||
|
||||
let record_key = get_id(&testimonial.id)?;
|
||||
let record: Option<TestimonialsSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.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 {
|
||||
Some(_) => Ok("Success delete testimonial".into()),
|
||||
None => bail!("Failed to delete testimonial"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use super::testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for TestimonialsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
role: String::new(),
|
||||
content: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestimonialsSchema {
|
||||
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user: dto.user.id,
|
||||
role: dto.role,
|
||||
content: dto.content,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: user_id.clone(),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
id: String,
|
||||
user_id: &Thing,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: make_thing(&ResourceEnum::Testimonials.to_string(), &id),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
updated_at: get_iso_date(),
|
||||
user: user_id.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_repository::TestimonialsRepository,
|
||||
testimonials_schema::TestimonialsSchema,
|
||||
};
|
||||
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 TestimonialsService;
|
||||
|
||||
impl TestimonialsService {
|
||||
pub async fn get_testimonial_list(
|
||||
state: &AppState,
|
||||
meta: MetaRequestDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<TestimonialsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(|e| e.from())
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_testimonial_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_by_id(id).await {
|
||||
Ok(testimonial) if !testimonial.is_deleted => {
|
||||
success_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: testimonial.id.id.to_raw(),
|
||||
user_id: testimonial.user.id.id.to_raw(),
|
||||
user_fullname: testimonial.user.fullname,
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
created_at: testimonial.created_at,
|
||||
updated_at: testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Testimonial not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_testimonial(
|
||||
state: &AppState,
|
||||
payload: TestimonialsCreateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
||||
match repo.query_create_testimonial(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_testimonial(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::update(payload, id, &authenticated_user.id);
|
||||
match repo.query_update_testimonial(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_testimonial(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
_authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_delete_testimonial(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod landing;
|
||||
|
||||
pub use landing::*;
|
||||
@@ -4,9 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -22,3 +23,8 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
http-body-util.workspace = true
|
||||
|
||||
@@ -1,14 +1,2 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
pub mod v1;
|
||||
pub use v1::*;
|
||||
|
||||
@@ -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,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadListMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => 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,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadDetailMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => 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,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::UpdateMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => 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,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::DeleteMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => 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,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::VerifyMentors],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => 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(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadOwnMentorProfile],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return (
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "Unauthorized",
|
||||
"message": "Token tidak valid"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_me(&app_state, &email).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[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(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::UpdateOwnMentorProfile],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
#[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(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
app_state.clone(),
|
||||
vec![PermissionsEnum::ReadOwnMentorStatus],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return (
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "Unauthorized",
|
||||
"message": "Token tidak valid"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_status(&app_state, &email).await
|
||||
}
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
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,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
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,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
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(url(message = "Invalid identity document URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub identity_document_url: Option<String>,
|
||||
#[validate(length(
|
||||
min = 10,
|
||||
max = 15,
|
||||
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>,
|
||||
#[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>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
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,
|
||||
email: schema.email,
|
||||
legal_name: schema.legal_name,
|
||||
gender: schema.gender,
|
||||
domicile: schema.domicile,
|
||||
identity_document_url: schema.identity_document_url,
|
||||
phone_for_verification: schema.phone_for_verification,
|
||||
bio: schema.bio,
|
||||
last_education: schema.last_education,
|
||||
linkedin_url: schema.linkedin_url,
|
||||
github_url: schema.github_url,
|
||||
cv_url: schema.cv_url,
|
||||
portfolio_url: schema.portfolio_url,
|
||||
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,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
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: dto.fullname,
|
||||
email: dto.email,
|
||||
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),
|
||||
fullname: dto.fullname,
|
||||
email: dto.email,
|
||||
legal_name: dto.legal_name,
|
||||
identity_document_url: dto.identity_document_url,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
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: schema.email,
|
||||
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,
|
||||
fullname: dto.fullname,
|
||||
email: dto.email,
|
||||
legal_name: dto.legal_name,
|
||||
gender: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
domicile: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
identity_document_url: dto.identity_document_url,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
last_education: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
portfolio_url: None, // Frontend form implies these are optional, not present in original MentorDetailWithUserDto
|
||||
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,299 @@
|
||||
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;
|
||||
|
||||
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("legal_name")
|
||||
.select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
"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("email", Some(email.clone()))
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
"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();
|
||||
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",
|
||||
"user_id.fullname as fullname",
|
||||
"email",
|
||||
"legal_name",
|
||||
"identity_document_url",
|
||||
"phone_for_verification",
|
||||
"bio",
|
||||
"linkedin_url",
|
||||
"github_url",
|
||||
"cv_url",
|
||||
"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();
|
||||
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 record: Option<MentorSchema> = db
|
||||
.create(ResourceEnum::Mentors.to_string())
|
||||
.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()));
|
||||
|
||||
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()));
|
||||
|
||||
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,250 @@
|
||||
use super::{
|
||||
IdentityAndVerification, 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>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: String,
|
||||
pub bio: String,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
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(),
|
||||
)),
|
||||
email: None,
|
||||
legal_name: String::new(),
|
||||
gender: None,
|
||||
domicile: None,
|
||||
identity_document_url: String::new(),
|
||||
phone_for_verification: String::new(),
|
||||
bio: String::new(),
|
||||
last_education: None,
|
||||
linkedin_url: None,
|
||||
github_url: None,
|
||||
cv_url: None,
|
||||
portfolio_url: None,
|
||||
industries: Vec::new(),
|
||||
expertise: Vec::new(),
|
||||
languages: Vec::new(),
|
||||
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(
|
||||
identity_and_verification: IdentityAndVerification,
|
||||
professional_profile: ProfessionalProfile,
|
||||
mentoring_logistics: MentoringLogistics,
|
||||
user_id_raw: String,
|
||||
email_str: 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)),
|
||||
email: Some(email_str),
|
||||
legal_name: identity_and_verification.legal_name,
|
||||
gender: identity_and_verification.gender,
|
||||
domicile: identity_and_verification.domicile,
|
||||
identity_document_url: identity_and_verification.identity_document_url,
|
||||
phone_for_verification: identity_and_verification.phone_for_verification,
|
||||
bio: professional_profile.bio,
|
||||
last_education: professional_profile.last_education,
|
||||
linkedin_url: professional_profile.linkedin_url,
|
||||
github_url: professional_profile.github_url,
|
||||
cv_url: professional_profile.cv_url,
|
||||
portfolio_url: professional_profile.portfolio_url,
|
||||
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),
|
||||
email: dto.email,
|
||||
legal_name: dto.legal_name,
|
||||
gender: dto.gender,
|
||||
domicile: dto.domicile,
|
||||
identity_document_url: dto.identity_document_url,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
last_education: dto.last_education,
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
portfolio_url: dto.portfolio_url,
|
||||
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 {
|
||||
// Update fields only if they are Some(value), otherwise preserve current value
|
||||
if let Some(val) = dto.legal_name {
|
||||
self.legal_name = val;
|
||||
}
|
||||
if let Some(val) = dto.gender {
|
||||
self.gender = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.domicile {
|
||||
self.domicile = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.identity_document_url {
|
||||
self.identity_document_url = val;
|
||||
}
|
||||
if let Some(val) = dto.phone_for_verification {
|
||||
self.phone_for_verification = val;
|
||||
}
|
||||
if let Some(val) = dto.bio {
|
||||
self.bio = val;
|
||||
}
|
||||
if let Some(val) = dto.last_education {
|
||||
self.last_education = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.linkedin_url {
|
||||
self.linkedin_url = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.github_url {
|
||||
self.github_url = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.cv_url {
|
||||
self.cv_url = Some(val);
|
||||
}
|
||||
if let Some(val) = dto.portfolio_url {
|
||||
self.portfolio_url = Some(val);
|
||||
}
|
||||
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,386 @@
|
||||
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();
|
||||
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(),
|
||||
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.identity_and_verification,
|
||||
dto.professional_profile,
|
||||
dto.mentoring_logistics,
|
||||
user_id.to_raw(),
|
||||
final_user_email.clone(),
|
||||
);
|
||||
|
||||
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
||||
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);
|
||||
match repo.query_mentor_list(meta).await {
|
||||
Ok(result) => {
|
||||
let data: Vec<MentorListResponseDto> = result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(MentorDetailQueryDto::from)
|
||||
.map(MentorListResponseDto::from)
|
||||
.collect();
|
||||
success_list_response(ResponseListSuccessDto {
|
||||
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 repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => {
|
||||
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(put_verify_mentor)))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod mentors;
|
||||
|
||||
pub fn dimentorin_router() -> Router {
|
||||
Router::new().nest("/mentors", mentors::mentors_router())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{
|
||||
engine::{local::Db, remote::ws::Client},
|
||||
Surreal,
|
||||
};
|
||||
use surrealdb::{Surreal, engine::any::Any, engine::local::Db};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
@@ -54,7 +51,7 @@ pub struct ResponseListSuccessDto<T: Serialize> {
|
||||
pub meta: Option<MetaResponseDto>,
|
||||
}
|
||||
|
||||
pub type SurrealWsClient = Surreal<Client>;
|
||||
pub type SurrealWsClient = Surreal<Any>;
|
||||
pub type SurrealMemClient = Surreal<Db>;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
pub mod error {
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Response;
|
||||
use axum::Json;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("database error")]
|
||||
Db,
|
||||
#[error("database error: {0}")]
|
||||
Db(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
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}"),
|
||||
),
|
||||
};
|
||||
(status, Json(error_message)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<surrealdb::Error> for Error {
|
||||
fn from(error: surrealdb::Error) -> Self {
|
||||
eprintln!("{error}");
|
||||
Self::Db
|
||||
Self::Db(error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -22,3 +23,6 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
rand_distr.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -1,8 +1,5 @@
|
||||
use imphnen_entities::*;
|
||||
use imphnen_libs::*;
|
||||
use imphnen_utils::*;
|
||||
|
||||
pub mod v1;
|
||||
|
||||
pub use imphnen_entities::*;
|
||||
pub use imphnen_libs::*;
|
||||
pub use imphnen_utils::*;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
use super::GachaClaimSchema;
|
||||
use crate::{AppState, ResourceEnum};
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
pub struct GachaClaimRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> GachaClaimRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
pub async fn query_gacha_claim_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<GachaClaimSchema> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let result: Option<GachaClaimSchema> = db
|
||||
.select((ResourceEnum::GachaClaims.to_string(), id.clone()))
|
||||
.await?;
|
||||
match result {
|
||||
Some(claim) if !claim.is_deleted => Ok(claim),
|
||||
_ => bail!("Gacha Claim not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_create_gacha_claim(
|
||||
&self,
|
||||
data: GachaClaimSchema,
|
||||
) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaClaimSchema> = db
|
||||
.create(ResourceEnum::GachaClaims.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Claim".into()),
|
||||
None => bail!("Failed to create Gacha Claim"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use crate::{ResourceEnum, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaClaimSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub item: Thing,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GachaClaimSchema {
|
||||
fn default() -> Self {
|
||||
GachaClaimSchema {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaClaims.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
item: make_thing(
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod gacha_claim_dto;
|
||||
pub mod gacha_claim_repository;
|
||||
pub mod gacha_claim_schema;
|
||||
|
||||
pub use gacha_claim_dto::*;
|
||||
pub use gacha_claim_repository::*;
|
||||
pub use gacha_claim_schema::*;
|
||||
@@ -0,0 +1,66 @@
|
||||
use axum::Extension;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Json, extract::Path};
|
||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
use imphnen_libs::{AppState, MessageResponseDto, ResponseSuccessDto};
|
||||
|
||||
use super::{GachaClaimItemDto, GachaClaimRequestDto, GachaClaimService};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/gacha/claims/detail/{id}",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
params(("id" = String, Path, description = "Gacha Claim ID")),
|
||||
responses(
|
||||
(status = 200, description = "Get Gacha Claim by ID", body = ResponseSuccessDto<GachaClaimItemDto>)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn get_detail_gacha_claim(
|
||||
headers: axum::http::HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::CreateGachaClaims],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaClaimService::get_gacha_claim_by_id(&state, id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/gacha/claims/create",
|
||||
request_body = GachaClaimRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Create new gacha claim", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn post_create_gacha_claim(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<GachaClaimRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::CreateGachaClaims],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaClaimService::create_gacha_claim(&state, payload).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
+21
-7
@@ -1,3 +1,5 @@
|
||||
use crate::{GachaItemDto, GachaItemSchema};
|
||||
use imphnen_iam::{UsersDetailItemDto, UsersDetailQueryDto};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
@@ -7,27 +9,39 @@ use validator::Validate;
|
||||
pub struct GachaClaimRequestDto {
|
||||
#[validate(length(min = 1, message = "User ID must not be empty"))]
|
||||
pub user_id: String,
|
||||
|
||||
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
||||
pub item_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct GachaClaimDto {
|
||||
pub struct GachaClaimItemDto {
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
pub item: String,
|
||||
pub user: UsersDetailItemDto,
|
||||
pub item: GachaItemDto,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaClaimDtoRaw {
|
||||
pub struct GachaClaimQueryDto {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub item: Thing,
|
||||
pub user: UsersDetailQueryDto,
|
||||
pub item: GachaItemSchema,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl GachaClaimItemDto {
|
||||
pub fn from(dto: &GachaClaimQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
user: UsersDetailItemDto::from(&dto.user),
|
||||
item: GachaItemDto::from(dto.item.clone()),
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at.clone(),
|
||||
updated_at: dto.updated_at.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use super::{GachaClaimQueryDto, GachaClaimSchema};
|
||||
use crate::{AppState, ResourceEnum};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::DetailQueryBuilder;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaClaimRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> GachaClaimRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_claim_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<GachaClaimQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string())
|
||||
.with_id(id.clone())
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("item")
|
||||
.with_fetch("user");
|
||||
let sql = builder.build();
|
||||
let result: Option<GachaClaimQueryDto> =
|
||||
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 {
|
||||
Some(claim) if !claim.is_deleted => Ok(claim),
|
||||
_ => bail!("Gacha Claim not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_gacha_claim(
|
||||
&self,
|
||||
data: GachaClaimSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaClaimSchema> = db
|
||||
.create(ResourceEnum::GachaClaims.to_string())
|
||||
.content(data)
|
||||
.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 {
|
||||
Some(_) => Ok("Success create Gacha Claim".into()),
|
||||
None => bail!("Failed to create Gacha Claim"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use crate::{GachaRollQueryDto, ResourceEnum, make_thing};
|
||||
use imphnen_iam::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
|
||||
use super::GachaClaimRequestDto;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaClaimSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub item: Thing,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GachaClaimSchema {
|
||||
fn default() -> Self {
|
||||
GachaClaimSchema {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaClaims.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
item: make_thing(
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GachaClaimSchema {
|
||||
pub fn from(dto: GachaClaimRequestDto) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaClaims.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(&ResourceEnum::Users.to_string(), &dto.user_id),
|
||||
item: make_thing(&ResourceEnum::GachaItems.to_string(), &dto.item_id),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn roll(roll: GachaRollQueryDto, user_id: Thing) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaClaims.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: user_id,
|
||||
item: roll.item.id.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::{
|
||||
AppState, GachaClaimItemDto, GachaClaimRepository, GachaClaimRequestDto,
|
||||
GachaClaimSchema, ResponseSuccessDto, common_response, success_response,
|
||||
validate_request,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
|
||||
pub struct GachaClaimService;
|
||||
|
||||
impl GachaClaimService {
|
||||
pub async fn get_gacha_claim_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = GachaClaimRepository::new(state);
|
||||
match repo.query_gacha_claim_by_id(id).await {
|
||||
Ok(claim) => success_response(ResponseSuccessDto {
|
||||
data: GachaClaimItemDto::from(&claim),
|
||||
}),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_gacha_claim(
|
||||
state: &AppState,
|
||||
payload: GachaClaimRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = GachaClaimRepository::new(state);
|
||||
let schema = GachaClaimSchema::from(payload);
|
||||
match repo.query_create_gacha_claim(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
pub mod gacha_claims_controller;
|
||||
pub mod gacha_claims_dto;
|
||||
pub mod gacha_claims_repository;
|
||||
pub mod gacha_claims_schema;
|
||||
pub mod gacha_claims_service;
|
||||
|
||||
pub use gacha_claims_controller::*;
|
||||
pub use gacha_claims_dto::*;
|
||||
pub use gacha_claims_repository::*;
|
||||
pub use gacha_claims_schema::*;
|
||||
pub use gacha_claims_service::*;
|
||||
|
||||
pub fn gacha_claim_router() -> Router {
|
||||
Router::new()
|
||||
.route("/create", post(post_create_gacha_claim))
|
||||
.route("/detail/{id}", get(get_detail_gacha_claim))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GachaCreditRequestDto {
|
||||
pub user_id: String,
|
||||
pub amount: i32,
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use super::{GachaCreditRequestDto, GachaCreditSchema};
|
||||
use crate::{AppState, ResourceEnum};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::make_thing;
|
||||
use std::time::Instant;
|
||||
use surrealdb::Uuid;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaCreditRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> GachaCreditRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, user_id), err)]
|
||||
pub async fn query_by_user_id(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<GachaCreditSchema>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let sql = format!(
|
||||
"SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1",
|
||||
ResourceEnum::GachaCredits,
|
||||
ResourceEnum::Users
|
||||
);
|
||||
let result: Vec<GachaCreditSchema> =
|
||||
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())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, user_id), err)]
|
||||
pub async fn query_consume_credit(&self, user_id: String) -> Result<()> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let credit_opt = self.query_by_user_id(user_id).await?;
|
||||
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(());
|
||||
};
|
||||
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");
|
||||
}
|
||||
credit.available_rolls -= 1;
|
||||
let _: Option<GachaCreditSchema> = db
|
||||
.update((
|
||||
&ResourceEnum::GachaCredits.to_string(),
|
||||
credit.id.id.to_raw(),
|
||||
))
|
||||
.merge(credit)
|
||||
.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(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), err)]
|
||||
pub async fn query_add_credit(
|
||||
&self,
|
||||
payload: GachaCreditRequestDto,
|
||||
) -> Result<()> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
|
||||
credit.available_rolls += payload.amount;
|
||||
let _: Option<GachaCreditSchema> = db
|
||||
.update((
|
||||
&ResourceEnum::GachaCredits.to_string(),
|
||||
credit.id.id.to_raw(),
|
||||
))
|
||||
.merge(credit)
|
||||
.await?;
|
||||
} else {
|
||||
let data = GachaCreditSchema {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaCredits.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id),
|
||||
available_rolls: payload.amount,
|
||||
..Default::default()
|
||||
};
|
||||
let _: Option<GachaCreditSchema> = db
|
||||
.create(ResourceEnum::GachaCredits.to_string())
|
||||
.content(data)
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use imphnen_iam::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GachaCreditSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub available_rolls: i32,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GachaCreditSchema {
|
||||
fn default() -> Self {
|
||||
GachaCreditSchema {
|
||||
id: Thing::from(("app_gacha_credits", "uuid")),
|
||||
user: Thing::from(("app_users", "uuid")),
|
||||
available_rolls: 0,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GachaCreditSchema {
|
||||
pub fn from(&self) -> Self {
|
||||
Self {
|
||||
id: self.id.clone(),
|
||||
user: self.user.clone(),
|
||||
available_rolls: self.available_rolls,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod gacha_credits_dto;
|
||||
pub mod gacha_credits_repository;
|
||||
pub mod gacha_credits_schema;
|
||||
|
||||
pub use gacha_credits_dto::*;
|
||||
pub use gacha_credits_repository::*;
|
||||
pub use gacha_credits_schema::*;
|
||||
@@ -1,103 +0,0 @@
|
||||
use super::GachaItemSchema;
|
||||
use crate::{
|
||||
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id,
|
||||
make_thing, query_list_with_meta,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
pub struct GachaItemRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> GachaItemRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
pub async fn query_gacha_item_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<GachaItemSchema>>> {
|
||||
let mut conditions = vec!["is_deleted = false".into()];
|
||||
if meta.search.is_some() {
|
||||
conditions.push("string::contains(name, $search)".into());
|
||||
}
|
||||
query_list_with_meta(
|
||||
&self.state.surrealdb_ws,
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&meta,
|
||||
conditions,
|
||||
None,
|
||||
"name",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let result: Option<GachaItemSchema> = db
|
||||
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
||||
.await?;
|
||||
match result {
|
||||
Some(item) if !item.is_deleted => Ok(item),
|
||||
_ => bail!("Gacha Item not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_create_gacha_item(
|
||||
&self,
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaItemSchema> = db
|
||||
.create(ResourceEnum::GachaItems.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Item".into()),
|
||||
None => bail!("Failed to create Gacha Item"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_update_gacha_item(
|
||||
&self,
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(&data.id)?;
|
||||
let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Gacha Item already deleted");
|
||||
}
|
||||
let merged = GachaItemSchema {
|
||||
created_at: existing.created_at,
|
||||
..data.clone()
|
||||
};
|
||||
let record: Option<GachaItemSchema> =
|
||||
db.update(record_key).merge(merged).await?;
|
||||
match record {
|
||||
Some(_) => Ok("Success update Gacha Item".into()),
|
||||
None => bail!("Failed to update Gacha Item"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
|
||||
let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?;
|
||||
if item.is_deleted {
|
||||
bail!("Gacha Item already deleted");
|
||||
}
|
||||
let record_key = get_id(&item.id)?;
|
||||
let record: Option<GachaItemSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
match record {
|
||||
Some(_) => Ok("Success delete Gacha Item".into()),
|
||||
None => bail!("Failed to delete Gacha Item"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod gacha_item_dto;
|
||||
pub mod gacha_item_repository;
|
||||
pub mod gacha_item_schema;
|
||||
|
||||
pub use gacha_item_dto::*;
|
||||
pub use gacha_item_repository::*;
|
||||
pub use gacha_item_schema::*;
|
||||
@@ -0,0 +1,164 @@
|
||||
use crate::{
|
||||
AppState, GachaItemDto, GachaItemRequestDto, GachaItemService, MessageResponseDto,
|
||||
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
extract::{Path, Query},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/gacha/items",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get gacha item list", body = ResponseListSuccessDto<Vec<GachaItemDto>>)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn get_gacha_item_list(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::ReadListGachaItems],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaItemService::get_gacha_item_list(&state, meta).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/gacha/items/detail/{id}",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
params(("id" = String, Path, description = "Gacha Item ID")),
|
||||
responses(
|
||||
(status = 200, description = "Get gacha item by ID", body = ResponseSuccessDto<GachaItemDto>)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn get_gacha_item_by_id(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::ReadDetailGachaItems],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaItemService::get_gacha_item_by_id(&state, id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/gacha/items/create",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
request_body = GachaItemRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Create gacha item", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn post_create_gacha_item(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<GachaItemRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::CreateGachaItems],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaItemService::create_gacha_item(&state, payload).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/gacha/items/update/{id}",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
request_body = GachaItemRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Update gacha item", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn put_update_gacha_item(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<GachaItemRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::UpdateGachaItems],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaItemService::update_gacha_item(&state, payload, id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/gacha/items/delete/{id}",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Delete gacha item", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn delete_gacha_item(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::DeleteGachaItems],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaItemService::delete_gacha_item(&state, id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -1,5 +1,5 @@
|
||||
use super::GachaItemSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -20,11 +20,14 @@ pub struct GachaItemDto {
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaItemDtoRaw {
|
||||
pub id: Thing,
|
||||
pub name: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
impl GachaItemDto {
|
||||
pub fn from(dto: GachaItemSchema) -> Self {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
name: dto.name.clone(),
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at.clone(),
|
||||
updated_at: dto.updated_at.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use super::GachaItemSchema;
|
||||
use crate::{
|
||||
AppState, GachaItemDto, MetaRequestDto, ResourceEnum, ResponseListSuccessDto,
|
||||
get_id, make_thing,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::QueryListBuilder;
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde_json::{Map, Value};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaItemRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> GachaItemRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_gacha_item_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
|
||||
let now = Instant::now();
|
||||
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
||||
QueryListBuilder::new(
|
||||
&self.state.surrealdb_ws,
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&meta,
|
||||
)
|
||||
.with_condition("is_deleted = false")
|
||||
.search_field("name")
|
||||
.select_fields(vec!["*"])
|
||||
.build()
|
||||
.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
|
||||
.data
|
||||
.into_iter()
|
||||
.map(GachaItemDto::from)
|
||||
.collect();
|
||||
Ok(ResponseListSuccessDto {
|
||||
data,
|
||||
meta: raw_result.meta,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let result: Option<GachaItemSchema> = db
|
||||
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
||||
.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 {
|
||||
Some(item) if !item.is_deleted => Ok(item),
|
||||
_ => bail!("Gacha Item not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_gacha_item(
|
||||
&self,
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaItemSchema> = db
|
||||
.create(ResourceEnum::GachaItems.to_string())
|
||||
.content(data)
|
||||
.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 {
|
||||
Some(_) => Ok("Success create Gacha Item".into()),
|
||||
None => bail!("Failed to create Gacha Item"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_gacha_item(
|
||||
&self,
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(&data.id)?;
|
||||
let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Gacha Item already deleted");
|
||||
}
|
||||
let merged = GachaItemSchema {
|
||||
created_at: existing.created_at,
|
||||
..data.clone()
|
||||
};
|
||||
let record: Option<GachaItemSchema> =
|
||||
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 {
|
||||
Some(_) => Ok("Success update Gacha Item".into()),
|
||||
None => bail!("Failed to update Gacha Item"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
|
||||
let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?;
|
||||
if item.is_deleted {
|
||||
bail!("Gacha Item already deleted");
|
||||
}
|
||||
let record_key = get_id(&item.id)?;
|
||||
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()));
|
||||
|
||||
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 {
|
||||
Some(_) => Ok("Success soft delete Gacha Item".into()),
|
||||
None => bail!("Failed to soft delete Gacha Item"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -1,7 +1,10 @@
|
||||
use crate::{ResourceEnum, make_thing};
|
||||
use imphnen_iam::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
|
||||
use super::GachaItemRequestDto;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaItemSchema {
|
||||
pub id: Thing,
|
||||
@@ -22,8 +25,22 @@ impl Default for GachaItemSchema {
|
||||
name: String::new(),
|
||||
image_url: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GachaItemSchema {
|
||||
pub fn from(dto: GachaItemRequestDto) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
name: dto.name,
|
||||
image_url: dto.image_url,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use crate::{
|
||||
AppState, GachaItemDto, GachaItemRepository, GachaItemRequestDto, GachaItemSchema,
|
||||
MetaRequestDto, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
common_response, make_thing, success_list_response, success_response,
|
||||
validate_request,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
|
||||
pub struct GachaItemService;
|
||||
|
||||
impl GachaItemService {
|
||||
pub async fn get_gacha_item_list(
|
||||
state: &AppState,
|
||||
meta: MetaRequestDto,
|
||||
) -> Response {
|
||||
let repo = GachaItemRepository::new(state);
|
||||
match repo.query_gacha_item_list(meta).await {
|
||||
Ok(data) => {
|
||||
let response = ResponseListSuccessDto {
|
||||
data: data.data,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_gacha_item_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = GachaItemRepository::new(state);
|
||||
match repo.query_gacha_item_by_id(id).await {
|
||||
Ok(item) => success_response(ResponseSuccessDto {
|
||||
data: GachaItemDto::from(item),
|
||||
}),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_gacha_item(
|
||||
state: &AppState,
|
||||
payload: GachaItemRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = GachaItemRepository::new(state);
|
||||
let schema = GachaItemSchema {
|
||||
id: make_thing(&ResourceEnum::GachaItems.to_string(), &payload.name), // Fixed: Use payload.name or some other identifier
|
||||
name: payload.name,
|
||||
image_url: payload.image_url,
|
||||
..Default::default()
|
||||
};
|
||||
match repo.query_create_gacha_item(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_gacha_item(
|
||||
state: &AppState,
|
||||
payload: GachaItemRequestDto,
|
||||
id: String,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = GachaItemRepository::new(state);
|
||||
let schema = GachaItemSchema {
|
||||
id: make_thing(&ResourceEnum::GachaItems.to_string(), &id),
|
||||
name: payload.name,
|
||||
image_url: payload.image_url,
|
||||
..Default::default()
|
||||
};
|
||||
match repo.query_update_gacha_item(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => {
|
||||
if e.to_string().contains("not found") {
|
||||
common_response(StatusCode::NOT_FOUND, "Gacha Item not found")
|
||||
} else {
|
||||
common_response(StatusCode::BAD_REQUEST, &e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_gacha_item(state: &AppState, id: String) -> Response {
|
||||
let repo = GachaItemRepository::new(state);
|
||||
match repo.query_delete_gacha_item(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => {
|
||||
if e.to_string().contains("not found") {
|
||||
common_response(StatusCode::NOT_FOUND, "Gacha Item not found")
|
||||
} else {
|
||||
common_response(StatusCode::BAD_REQUEST, &e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
|
||||
pub mod gacha_items_controller;
|
||||
pub mod gacha_items_dto;
|
||||
pub mod gacha_items_repository;
|
||||
pub mod gacha_items_schema;
|
||||
pub mod gacha_items_service;
|
||||
|
||||
pub use gacha_items_controller::*;
|
||||
pub use gacha_items_dto::*;
|
||||
pub use gacha_items_repository::*;
|
||||
pub use gacha_items_schema::*;
|
||||
pub use gacha_items_service::*;
|
||||
|
||||
pub fn gacha_item_router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(get_gacha_item_list))
|
||||
.route("/create", post(post_create_gacha_item))
|
||||
.route("/detail/{id}", get(get_gacha_item_by_id))
|
||||
.route("/update/{id}", put(put_update_gacha_item))
|
||||
.route("/delete/{id}", delete(delete_gacha_item))
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use super::GachaRollSchema;
|
||||
use crate::{AppState, ResourceEnum};
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
pub struct GachaRollRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> GachaRollRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
pub async fn query_gacha_roll_by_id(&self, id: String) -> Result<GachaRollSchema> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let result: Option<GachaRollSchema> = db
|
||||
.select((ResourceEnum::GachaRolls.to_string(), id.clone()))
|
||||
.await?;
|
||||
match result {
|
||||
Some(roll) if !roll.is_deleted => Ok(roll),
|
||||
_ => bail!("Gacha Roll not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_create_gacha_roll(
|
||||
&self,
|
||||
data: GachaRollSchema,
|
||||
) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaRollSchema> = db
|
||||
.create(ResourceEnum::GachaRolls.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Roll".into()),
|
||||
None => bail!("Failed to create Gacha Roll"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod gacha_roll_dto;
|
||||
pub mod gacha_roll_repository;
|
||||
pub mod gacha_roll_schema;
|
||||
|
||||
pub use gacha_roll_dto::*;
|
||||
pub use gacha_roll_repository::*;
|
||||
pub use gacha_roll_schema::*;
|
||||
@@ -0,0 +1,122 @@
|
||||
use crate::{
|
||||
AppState, GachaRollItemDto, GachaRollRequestDto, GachaRollService,
|
||||
MessageResponseDto, ResponseSuccessDto,
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json, extract::Path, http::HeaderMap, response::IntoResponse,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/gacha/rolls/detail/{id}",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
params(("id" = String, Path, description = "Gacha Roll ID")),
|
||||
responses(
|
||||
(status = 200, description = "Get Gacha Roll by ID", body = ResponseSuccessDto<GachaRollItemDto>)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn get_detail_gacha_roll(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::ReadDetailGachaRolls],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaRollService::get_gacha_roll_by_id(&state, id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/gacha/rolls/create",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
request_body = GachaRollRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Create new gacha roll", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn post_create_gacha_roll(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<GachaRollRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::CreateGachaRolls],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaRollService::create_gacha_roll(&state, payload).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/gacha/rolls/execute",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Execute and get 1 gacha result", body = ResponseSuccessDto<GachaRollItemDto>)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn post_execute_gacha_roll(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::ExecuteGachaRolls],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaRollService::execute_roll_once(headers, &state).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/gacha/rolls/delete/{id}",
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
params(("id" = String, Path, description = "Gacha Roll ID")),
|
||||
responses(
|
||||
(status = 200, description = "Delete Gacha Roll (soft delete)", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Gacha"
|
||||
)]
|
||||
pub async fn delete_gacha_roll(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
&headers,
|
||||
state.clone(),
|
||||
vec![PermissionsEnum::DeleteGachaRolls],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => GachaRollService::soft_delete_gacha_roll(&state, id).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
+21
-9
@@ -1,3 +1,4 @@
|
||||
use crate::{GachaItemDto, GachaItemSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
@@ -7,30 +8,41 @@ use validator::Validate;
|
||||
pub struct GachaRollRequestDto {
|
||||
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
||||
pub item_id: String,
|
||||
|
||||
#[validate(range(min = 1, message = "Weight must be greater than zero"))]
|
||||
pub weight: f32,
|
||||
|
||||
#[validate(range(min = 1, message = "Quantity must be at least 1"))]
|
||||
pub quantity: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct GachaRollDto {
|
||||
pub struct GachaRollItemDto {
|
||||
pub id: String,
|
||||
pub item: String,
|
||||
pub weight: String,
|
||||
pub item: GachaItemDto,
|
||||
pub weight: f32,
|
||||
pub quantity: i32,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl GachaRollItemDto {
|
||||
pub fn from(dto: &GachaRollQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
item: GachaItemDto::from(dto.item.clone()),
|
||||
weight: dto.weight,
|
||||
quantity: dto.quantity,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at.clone(),
|
||||
updated_at: dto.updated_at.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaRollDtoRaw {
|
||||
pub struct GachaRollQueryDto {
|
||||
pub id: Thing,
|
||||
pub item: Thing,
|
||||
pub weight: String,
|
||||
pub item: GachaItemSchema,
|
||||
pub weight: f32,
|
||||
pub quantity: i32,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
@@ -0,0 +1,135 @@
|
||||
use super::GachaRollQueryDto;
|
||||
use super::GachaRollSchema;
|
||||
use crate::{AppState, DetailQueryBuilder, ResourceEnum, get_id, make_thing};
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use rand::prelude::*;
|
||||
|
||||
use imphnen_utils::get_iso_date;
|
||||
use rand_distr::weighted::WeightedIndex;
|
||||
use serde_json::{Map, Value};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct GachaRollRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> GachaRollRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_roll_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<GachaRollQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string())
|
||||
.with_id(id.clone())
|
||||
.with_condition("is_deleted = false")
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("item");
|
||||
let sql = builder.build();
|
||||
let result: Option<GachaRollQueryDto> =
|
||||
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_roll_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(roll) if !roll.is_deleted => Ok(roll),
|
||||
_ => bail!("Gacha Roll not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_gacha_roll(
|
||||
&self,
|
||||
data: GachaRollSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record: Option<GachaRollSchema> = db
|
||||
.create(ResourceEnum::GachaRolls.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_gacha_roll' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Roll".into()),
|
||||
None => bail!("Failed to create Gacha Roll"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn query_all_active_rolls(&self) -> Result<Vec<GachaRollQueryDto>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let table_name = ResourceEnum::GachaRolls.to_string();
|
||||
let sql =
|
||||
format!("SELECT * FROM {table_name} WHERE is_deleted = false FETCH item");
|
||||
let result: Vec<GachaRollQueryDto> = 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_all_active_rolls' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn roll_once(rolls: &[GachaRollQueryDto]) -> Option<GachaRollQueryDto> {
|
||||
let filtered: Vec<_> = rolls
|
||||
.iter()
|
||||
.filter(|r| !r.is_deleted && r.quantity > 0)
|
||||
.collect();
|
||||
let weights: Vec<f32> = filtered
|
||||
.iter()
|
||||
.map(|r| r.weight * r.quantity as f32)
|
||||
.collect();
|
||||
if weights.iter().all(|&w| w <= 0.0) {
|
||||
return None;
|
||||
}
|
||||
let dist = WeightedIndex::new(&weights).ok()?;
|
||||
let mut rng = rand::rngs::ThreadRng::default();
|
||||
let index = dist.sample(&mut rng);
|
||||
Some(filtered[index].clone())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_soft_delete_gacha_roll(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let roll_id_thing = make_thing(&ResourceEnum::GachaRolls.to_string(), &id);
|
||||
let roll = self.query_gacha_roll_by_id(id.clone()).await?;
|
||||
if roll.is_deleted {
|
||||
bail!("Gacha Roll already deleted");
|
||||
}
|
||||
let record_key = get_id(&roll_id_thing)?;
|
||||
|
||||
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()));
|
||||
|
||||
let record: Option<GachaRollSchema> = 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_soft_delete_gacha_roll' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success soft delete Gacha Roll".into()),
|
||||
None => bail!("Failed to soft delete Gacha Roll"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-4
@@ -1,7 +1,10 @@
|
||||
use crate::{ResourceEnum, make_thing};
|
||||
use imphnen_iam::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
|
||||
use super::GachaRollRequestDto;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaRollSchema {
|
||||
pub id: Thing,
|
||||
@@ -24,11 +27,26 @@ impl Default for GachaRollSchema {
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
weight: 0.2,
|
||||
quantity: 2,
|
||||
weight: 0.0,
|
||||
quantity: 0,
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GachaRollSchema {
|
||||
pub fn create(dto: GachaRollRequestDto) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaRolls.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
item: make_thing(&ResourceEnum::GachaItems.to_string(), &dto.item_id),
|
||||
weight: dto.weight,
|
||||
quantity: dto.quantity,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use crate::{
|
||||
AppState, GachaClaimRepository, GachaClaimSchema, GachaRollItemDto,
|
||||
GachaRollRepository, GachaRollRequestDto, GachaRollSchema, ResponseSuccessDto,
|
||||
common_response, success_response, validate_request,
|
||||
};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::Response;
|
||||
use imphnen_iam::{UsersRepository, extract_email};
|
||||
|
||||
pub struct GachaRollService;
|
||||
|
||||
impl GachaRollService {
|
||||
pub async fn get_gacha_roll_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = GachaRollRepository::new(state);
|
||||
match repo.query_gacha_roll_by_id(id).await {
|
||||
Ok(roll) => success_response(ResponseSuccessDto {
|
||||
data: GachaRollItemDto::from(&roll),
|
||||
}),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_gacha_roll(
|
||||
state: &AppState,
|
||||
payload: GachaRollRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let schema = GachaRollSchema::create(payload);
|
||||
let repo = GachaRollRepository::new(state);
|
||||
match repo.query_create_gacha_roll(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_roll_once(headers: HeaderMap, state: &AppState) -> Response {
|
||||
let repo = GachaRollRepository::new(state);
|
||||
let repo_claim = GachaClaimRepository::new(state);
|
||||
let repo_user = UsersRepository::new(state);
|
||||
let Some(email) = extract_email(&headers) else {
|
||||
return common_response(StatusCode::UNAUTHORIZED, "Unauthorized");
|
||||
};
|
||||
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||
};
|
||||
match repo.query_all_active_rolls().await {
|
||||
Ok(rolls) => match GachaRollRepository::roll_once(&rolls) {
|
||||
Some(roll) => {
|
||||
let claim = GachaClaimSchema::roll(roll.clone(), user.id);
|
||||
match repo_claim.query_create_gacha_claim(claim).await {
|
||||
Ok(_) => success_response(ResponseSuccessDto {
|
||||
data: GachaRollItemDto::from(&roll),
|
||||
}),
|
||||
Err(e) => {
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
None => common_response(StatusCode::NOT_FOUND, "No rollable item available"),
|
||||
},
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn soft_delete_gacha_roll(state: &AppState, id: String) -> Response {
|
||||
let repo = GachaRollRepository::new(state);
|
||||
match repo.query_soft_delete_gacha_roll(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
pub mod gacha_rolls_controller;
|
||||
pub mod gacha_rolls_dto;
|
||||
pub mod gacha_rolls_repository;
|
||||
pub mod gacha_rolls_schema;
|
||||
pub mod gacha_rolls_service;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
pub use gacha_rolls_controller::*;
|
||||
pub use gacha_rolls_dto::*;
|
||||
pub use gacha_rolls_repository::*;
|
||||
pub use gacha_rolls_schema::*;
|
||||
pub use gacha_rolls_service::*;
|
||||
|
||||
pub fn gacha_roll_router() -> Router {
|
||||
Router::new()
|
||||
.route("/create", post(post_create_gacha_roll))
|
||||
.route("/execute", post(post_execute_gacha_roll))
|
||||
.route("/detail/{id}", get(get_detail_gacha_roll))
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
pub mod gacha_claim;
|
||||
pub mod gacha_item;
|
||||
pub mod gacha_roll;
|
||||
use axum::Router;
|
||||
|
||||
pub use gacha_claim::*;
|
||||
pub use gacha_item::*;
|
||||
pub use gacha_roll::*;
|
||||
pub mod gacha_claims;
|
||||
pub mod gacha_credits;
|
||||
pub mod gacha_items;
|
||||
pub mod gacha_rolls;
|
||||
|
||||
pub use gacha_claims::*;
|
||||
pub use gacha_credits::*;
|
||||
pub use gacha_items::*;
|
||||
pub use gacha_rolls::*;
|
||||
|
||||
pub fn gacha_router() -> Router {
|
||||
Router::new()
|
||||
.nest("/gacha/claims", gacha_claim_router())
|
||||
.nest("/gacha/items", gacha_item_router())
|
||||
.nest("/gacha/rolls", gacha_roll_router())
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-middleware = { version = "0.1.0", path = "../imphnen-middleware" }
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-gacha.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-middleware.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
+101
-5
@@ -1,15 +1,44 @@
|
||||
use imphnen_cms::{
|
||||
events_controller,
|
||||
events_dto::{EventsDetailItemDto, EventsListItemDto},
|
||||
testimonials_controller,
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
};
|
||||
use imphnen_dimentorin::v1::mentors::{
|
||||
mentors_controller,
|
||||
mentors_dto::{
|
||||
IdentityAndVerification, MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorRegisterFromTokenRequestDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
},
|
||||
};
|
||||
use imphnen_gacha::{
|
||||
GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto,
|
||||
GachaRollItemDto, GachaRollRequestDto, gacha_claims, gacha_items, gacha_rolls,
|
||||
};
|
||||
use imphnen_iam::{
|
||||
auth, permissions, roles, users, AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, UsersListItemDto, UsersUpdateRequestDto
|
||||
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
|
||||
AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto,
|
||||
MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto,
|
||||
PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto,
|
||||
RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto,
|
||||
UsersListItemDto, UsersUpdateRequestDto, auth, permissions, roles, users,
|
||||
};
|
||||
use utoipa::{
|
||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||
Modify, OpenApi,
|
||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||
};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
auth::auth_controller::post_login,
|
||||
auth::auth_controller::post_login_mentor,
|
||||
auth::auth_controller::post_register,
|
||||
auth::auth_controller::post_verify_email,
|
||||
auth::auth_controller::post_resend_otp,
|
||||
@@ -33,7 +62,36 @@ use utoipa::{
|
||||
permissions::permissions_controller::get_permission_by_id,
|
||||
permissions::permissions_controller::post_create_permission,
|
||||
permissions::permissions_controller::put_update_permission,
|
||||
permissions::permissions_controller::delete_permission
|
||||
permissions::permissions_controller::delete_permission,
|
||||
gacha_claims::get_detail_gacha_claim,
|
||||
gacha_claims::post_create_gacha_claim,
|
||||
gacha_items::get_gacha_item_list,
|
||||
gacha_items::get_gacha_item_by_id,
|
||||
gacha_items::post_create_gacha_item,
|
||||
gacha_items::put_update_gacha_item,
|
||||
gacha_items::delete_gacha_item,
|
||||
gacha_rolls::get_detail_gacha_roll,
|
||||
gacha_rolls::post_create_gacha_roll,
|
||||
gacha_rolls::post_execute_gacha_roll,
|
||||
events_controller::get_event_list,
|
||||
events_controller::get_event_by_id,
|
||||
events_controller::post_create_event,
|
||||
events_controller::patch_update_event,
|
||||
events_controller::delete_event,
|
||||
testimonials_controller::get_testimonial_list,
|
||||
testimonials_controller::get_testimonial_by_id,
|
||||
testimonials_controller::post_create_testimonial,
|
||||
testimonials_controller::patch_update_testimonial,
|
||||
testimonials_controller::delete_testimonial,
|
||||
mentors_controller::get_mentor_list,
|
||||
mentors_controller::get_mentor_by_id,
|
||||
mentors_controller::post_register_mentor,
|
||||
mentors_controller::get_mentor_me,
|
||||
mentors_controller::put_update_mentor_me,
|
||||
mentors_controller::get_mentor_status,
|
||||
mentors_controller::put_update_mentor,
|
||||
mentors_controller::put_verify_mentor,
|
||||
mentors_controller::delete_mentor,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@@ -48,7 +106,7 @@ use utoipa::{
|
||||
AuthRefreshTokenRequestDto,
|
||||
ResponseSuccessDto<TokenDto>,
|
||||
RolesListItemDto,
|
||||
RolesRequestCreateDto,
|
||||
RolesRequestCreateDto,
|
||||
RolesRequestUpdateDto,
|
||||
PermissionsRequestDto,
|
||||
PermissionsItemDto,
|
||||
@@ -56,13 +114,43 @@ use utoipa::{
|
||||
UsersListItemDto,
|
||||
UsersUpdateRequestDto,
|
||||
UsersCreateRequestDto,
|
||||
GachaClaimItemDto,
|
||||
GachaClaimRequestDto,
|
||||
GachaItemDto,
|
||||
GachaItemRequestDto,
|
||||
GachaRollItemDto,
|
||||
GachaRollRequestDto,
|
||||
ResponseListSuccessDto<Vec<GachaItemDto>>,
|
||||
ResponseSuccessDto<GachaRollItemDto>,
|
||||
ResponseSuccessDto<GachaItemDto>,
|
||||
ResponseSuccessDto<GachaClaimItemDto>,
|
||||
ResponseSuccessDto<AuthLoginResponsetDto>,
|
||||
ResponseListSuccessDto<Vec<RolesListItemDto>>,
|
||||
ResponseSuccessDto<RolesDetailItemDto>,
|
||||
ResponseListSuccessDto<Vec<UsersListItemDto>>,
|
||||
ResponseSuccessDto<UsersDetailItemDto>,
|
||||
ResponseListSuccessDto<Vec<PermissionsItemDto>>,
|
||||
ResponseSuccessDto<PermissionsItemDto>
|
||||
ResponseSuccessDto<PermissionsItemDto>,
|
||||
ResponseListSuccessDto<Vec<EventsListItemDto>>,
|
||||
ResponseSuccessDto<EventsDetailItemDto>,
|
||||
ResponseListSuccessDto<Vec<TestimonialsListItemDto>>,
|
||||
ResponseSuccessDto<TestimonialsDetailItemDto>,
|
||||
TestimonialsCreateRequestDto,
|
||||
TestimonialsUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto,
|
||||
MentorRegisterFromTokenRequestDto,
|
||||
MentorRegisterResponseDto,
|
||||
MentorListResponseDto,
|
||||
MentorDetailResponseDto,
|
||||
MentorUpdateRequestDto,
|
||||
MentorVerifyRequestDto,
|
||||
IdentityAndVerification,
|
||||
ProfessionalProfile,
|
||||
MentoringLogistics,
|
||||
MentoringRate,
|
||||
ResponseListSuccessDto<Vec<MentorListResponseDto>>,
|
||||
ResponseSuccessDto<MentorDetailResponseDto>,
|
||||
ResponseSuccessDto<MentorRegisterResponseDto>,
|
||||
)
|
||||
),
|
||||
info(
|
||||
@@ -81,6 +169,14 @@ use utoipa::{
|
||||
modifiers(&SecurityAddon),
|
||||
tags(
|
||||
(name = "Authentication", description = "List of Authentication Endpoints"),
|
||||
(name = "Users", description = "User Management Endpoints"),
|
||||
(name = "Roles", description = "Role Management Endpoints"),
|
||||
(name = "Permissions", description = "Permission Management Endpoints"),
|
||||
(name = "Events", description = "Event Management Endpoints"),
|
||||
(name = "Testimonials", description = "Testimonial Management Endpoints"),
|
||||
(name = "Mentors", description = "Mentor Management Endpoints"),
|
||||
(name = "Mentors - Admin", description = "Mentor Admin Management Endpoints (Admin Access Required)"),
|
||||
(name = "Gacha", description = "Gacha System Endpoints"),
|
||||
)
|
||||
)]
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use axum::{
|
||||
Extension, Router, middleware::from_fn, response::Redirect, routing::get,
|
||||
};
|
||||
use imphnen_cms::{
|
||||
events_protected_routes, events_public_routes, testimonials_protected_routes,
|
||||
testimonials_public_routes,
|
||||
};
|
||||
use imphnen_dimentorin::dimentorin_router;
|
||||
use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient};
|
||||
use imphnen_gacha::gacha_router;
|
||||
use imphnen_iam::{iam_protected_routes, iam_public_routes};
|
||||
use imphnen_middleware::{auth_middleware, cors_middleware};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
@@ -18,13 +24,25 @@ pub async fn gateway_service(
|
||||
surrealdb_mem,
|
||||
};
|
||||
|
||||
let public_routes = iam_public_routes();
|
||||
let protected_routes = iam_protected_routes().layer(from_fn(auth_middleware));
|
||||
let public_routes = Router::new()
|
||||
.merge(iam_public_routes())
|
||||
.merge(testimonials_public_routes())
|
||||
.merge(events_public_routes());
|
||||
|
||||
let protected_routes = Router::new()
|
||||
.merge(iam_protected_routes())
|
||||
.merge(events_protected_routes())
|
||||
.merge(testimonials_protected_routes())
|
||||
.merge(dimentorin_router())
|
||||
.merge(gacha_router())
|
||||
.layer(from_fn(auth_middleware));
|
||||
|
||||
let routes = public_routes.merge(protected_routes);
|
||||
|
||||
Router::new()
|
||||
.route("/", get(Redirect::to("/docs")))
|
||||
.nest("/v1", public_routes.merge(protected_routes))
|
||||
.nest("/v1", routes)
|
||||
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
||||
.layer(cors_middleware())
|
||||
.layer(Extension(state.clone()))
|
||||
.layer(Extension(state))
|
||||
}
|
||||
|
||||
+13
-3
@@ -4,9 +4,9 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -22,3 +22,13 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
http-body-util.workspace = true
|
||||
|
||||
@@ -1,34 +1,6 @@
|
||||
use ::surrealdb::Uuid;
|
||||
use imphnen_entities::*;
|
||||
use imphnen_libs::*;
|
||||
use imphnen_utils::*;
|
||||
|
||||
pub mod v1;
|
||||
|
||||
pub use imphnen_entities::*;
|
||||
pub use imphnen_libs::*;
|
||||
pub use imphnen_utils::*;
|
||||
pub use v1::*;
|
||||
|
||||
pub fn create_test_user(
|
||||
email: &str,
|
||||
fullname: &str,
|
||||
is_active: bool,
|
||||
role_id: &str,
|
||||
) -> UsersSchema {
|
||||
UsersSchema {
|
||||
id: make_thing("app_users", &Uuid::new_v4().to_string()),
|
||||
email: email.to_string(),
|
||||
fullname: format!("Randomize {} {}", fullname, rand::random::<u32>()),
|
||||
password: hash_password("secret").unwrap(),
|
||||
is_deleted: false,
|
||||
avatar: None,
|
||||
phone_number: "081234567890".to_string(),
|
||||
is_active,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
role: make_thing("app_roles", role_id),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use super::{
|
||||
AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto,
|
||||
AuthResendOtpRequestDto, AuthService, AuthVerifyEmailRequestDto,
|
||||
};
|
||||
use crate::{v1::AuthLoginResponsetDto, AppState};
|
||||
use crate::{AppState, v1::AuthLoginResponsetDto};
|
||||
use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto};
|
||||
use axum::{response::IntoResponse, Extension, Json};
|
||||
use axum::{Extension, Json, response::IntoResponse};
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
@@ -23,6 +23,24 @@ pub async fn post_login(
|
||||
AuthService::mutation_login(payload, &state).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/auth/login-mentor",
|
||||
request_body = AuthLoginRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Mentor login successful", body = ResponseSuccessDto<AuthLoginResponsetDto>),
|
||||
(status = 401, description = "Mentor login failed", body = MessageResponseDto),
|
||||
(status = 403, description = "Forbidden - Not a mentor", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Authentication"
|
||||
)]
|
||||
pub async fn post_login_mentor(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<AuthLoginRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
AuthService::mutation_mentor_login(payload, &state).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/auth/register",
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use crate::UsersDetailItemDto;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
use crate::UsersDetailItemDto;
|
||||
|
||||
lazy_static! {
|
||||
static ref PASSWORD_REGEX: Regex = Regex::new(
|
||||
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn validate_password_complexity(password: &str) -> Result<(), ValidationError> {
|
||||
pub fn validate_password_complexity(password: &str) -> Result<(), ValidationError> {
|
||||
let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
|
||||
let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
|
||||
let has_digit = password.chars().any(|c| c.is_ascii_digit());
|
||||
let has_special = password.chars().any(|c| "@$!%*?&".contains(c));
|
||||
|
||||
if has_uppercase && has_lowercase && has_digit && has_special {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -98,10 +87,14 @@ pub struct AuthRefreshTokenRequestDto {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct AuthNewPasswordRequestDto {
|
||||
pub token: String,
|
||||
#[validate(length(min = 1, message = "Token cannot be empty"))]
|
||||
#[validate(regex(
|
||||
path = "PASSWORD_REGEX",
|
||||
pub token: String,
|
||||
#[validate(length(
|
||||
min = 8,
|
||||
message = "Password must have at least 8 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "validate_password_complexity",
|
||||
message = "Password must include uppercase, lowercase, number, and special character"
|
||||
))]
|
||||
pub password: String,
|
||||
@@ -118,9 +111,14 @@ pub struct AuthSetNewPasswordRequestDto {
|
||||
min = 8,
|
||||
message = "Password must have at least 8 characters"
|
||||
))]
|
||||
#[validate(regex(
|
||||
path = "PASSWORD_REGEX",
|
||||
#[validate(custom(
|
||||
function = "validate_password_complexity",
|
||||
message = "Password must include uppercase, lowercase, number, and special character"
|
||||
))]
|
||||
pub password: String,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UserCacheSchema {
|
||||
pub email: String,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use super::AuthOtpSchema;
|
||||
use crate::{AppState, ResourceEnum, UsersDetailQueryDto, make_thing};
|
||||
use super::UserCacheSchema;
|
||||
use crate::{
|
||||
AppState, PermissionsQueryDto, ResourceEnum, RolesDetailQueryDto,
|
||||
UsersDetailQueryDto,
|
||||
};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use chrono::{Duration, Utc};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
pub struct AuthRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
@@ -18,20 +23,26 @@ impl<'a> AuthRepository<'a> {
|
||||
}
|
||||
let table = ResourceEnum::UsersCache.to_string();
|
||||
let user_id = user.email.clone();
|
||||
let id = make_thing(&table, &user_id);
|
||||
let _ = self
|
||||
let permissions: Vec<String> =
|
||||
user.role.permissions.into_iter().map(|p| p.name).collect();
|
||||
let user_cache = UserCacheSchema {
|
||||
email: user_id.clone(),
|
||||
permissions,
|
||||
};
|
||||
|
||||
let _record: Option<UserCacheSchema> = self
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.delete::<Option<UsersDetailQueryDto>>((table.clone(), user_id.clone()))
|
||||
.delete::<Option<UserCacheSchema>>((table.clone(), user_id.clone()))
|
||||
.await?;
|
||||
let mut user_to_store = user.clone();
|
||||
user_to_store.id = id.clone();
|
||||
let record: Option<UsersDetailQueryDto> = self
|
||||
|
||||
let record: Option<UserCacheSchema> = self
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.create((table, user_id))
|
||||
.content(user_to_store)
|
||||
.content(user_cache)
|
||||
.await?;
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success store user data".to_string()),
|
||||
None => bail!("Failed store user data"),
|
||||
@@ -42,13 +53,54 @@ impl<'a> AuthRepository<'a> {
|
||||
&self,
|
||||
email: String,
|
||||
) -> Result<UsersDetailQueryDto> {
|
||||
let user: Option<UsersDetailQueryDto> = self
|
||||
let user_cache: Option<UserCacheSchema> = self
|
||||
.state
|
||||
.surrealdb_mem
|
||||
.select((ResourceEnum::UsersCache.to_string(), email))
|
||||
.select((ResourceEnum::UsersCache.to_string(), email.clone()))
|
||||
.await?;
|
||||
match user {
|
||||
Some(u) => Ok(u),
|
||||
|
||||
match user_cache {
|
||||
Some(cache) => {
|
||||
let permissions_query_dto: Vec<PermissionsQueryDto> = cache
|
||||
.permissions
|
||||
.into_iter()
|
||||
.map(|name| PermissionsQueryDto {
|
||||
id: Thing::from((
|
||||
"app_permissions".to_string(),
|
||||
surrealdb::sql::Id::rand(),
|
||||
)),
|
||||
name,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let role_detail_query_dto = RolesDetailQueryDto {
|
||||
id: Thing::from(("app_roles".to_string(), surrealdb::sql::Id::rand())),
|
||||
name: "CachedRole".to_string(),
|
||||
permissions: permissions_query_dto,
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
};
|
||||
|
||||
Ok(UsersDetailQueryDto {
|
||||
id: Thing::from(("app_users".to_string(), email.clone())),
|
||||
fullname: "Cached User".to_string(),
|
||||
email: cache.email,
|
||||
avatar: None,
|
||||
phone_number: String::new(),
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
password: String::new(),
|
||||
role: role_detail_query_dto,
|
||||
created_at: String::new(),
|
||||
updated_at: String::new(),
|
||||
mentor_id: None,
|
||||
})
|
||||
}
|
||||
None => bail!("No stored user data found"),
|
||||
}
|
||||
}
|
||||
@@ -59,7 +111,6 @@ impl<'a> AuthRepository<'a> {
|
||||
.surrealdb_mem
|
||||
.delete((ResourceEnum::UsersCache.to_string(), email))
|
||||
.await?;
|
||||
dbg!(record.clone());
|
||||
match record {
|
||||
Some(_) => Ok("Success delete stored user".to_string()),
|
||||
None => bail!("Failed delete stored user"),
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use surrealdb::Uuid;
|
||||
use tracing::error;
|
||||
|
||||
pub struct AuthService;
|
||||
|
||||
@@ -49,7 +50,11 @@ impl AuthService {
|
||||
|
||||
let access_token = match encode_access_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate access token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
@@ -59,7 +64,11 @@ impl AuthService {
|
||||
|
||||
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate refresh token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate refresh token",
|
||||
@@ -77,13 +86,116 @@ impl AuthService {
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(_err) = auth_repo.query_store_user(user).await {
|
||||
return common_response(StatusCode::BAD_REQUEST, "User already login");
|
||||
if let Err(err_store) = auth_repo.query_store_user(user.clone()).await {
|
||||
error!(
|
||||
"Failed to store user cache for {}: {}",
|
||||
user.email, err_store
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"User already login or failed to cache",
|
||||
);
|
||||
}
|
||||
|
||||
success_response(response)
|
||||
}
|
||||
Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()),
|
||||
Err(err_find) => {
|
||||
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mutation_mentor_login(
|
||||
payload: AuthLoginRequestDto,
|
||||
state: &AppState,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let auth_repo = AuthRepository::new(state);
|
||||
|
||||
match user_repo.query_user_by_email(payload.email.clone()).await {
|
||||
Ok(user) => {
|
||||
let is_password_correct =
|
||||
verify_password(&payload.password, &user.password).unwrap_or(false);
|
||||
|
||||
if !is_password_correct {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Email or password not correct",
|
||||
);
|
||||
}
|
||||
|
||||
if !user.is_active {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Account not active, please verify your email",
|
||||
);
|
||||
}
|
||||
|
||||
let user_detail = UsersDetailItemDto::from(&user);
|
||||
|
||||
if user_detail.role.name != RolesEnum::Mentor.to_string() {
|
||||
return common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"User does not have mentor privileges",
|
||||
);
|
||||
}
|
||||
|
||||
let access_token = match encode_access_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate access token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate refresh token for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate refresh token",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let response = ResponseSuccessDto {
|
||||
data: AuthLoginResponsetDto {
|
||||
user: UsersDetailItemDto::from(&user),
|
||||
token: TokenDto {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(err_store) = auth_repo.query_store_user(user.clone()).await {
|
||||
error!(
|
||||
"Failed to store user cache for {}: {}",
|
||||
user.email, err_store
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"User already login or failed to cache",
|
||||
);
|
||||
}
|
||||
success_response(response)
|
||||
}
|
||||
Err(err_find) => {
|
||||
common_response(StatusCode::UNAUTHORIZED, &err_find.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +206,6 @@ impl AuthService {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let auth_repo = AuthRepository::new(state);
|
||||
let role_repo = RolesRepository::new(state);
|
||||
@@ -103,9 +214,11 @@ impl AuthService {
|
||||
.await
|
||||
{
|
||||
Ok(role) => role,
|
||||
Err(_) => return common_response(StatusCode::BAD_REQUEST, "Role Not Found"),
|
||||
Err(_e) => {
|
||||
error!("Failed to retrieve User role during registration: {}", _e);
|
||||
return common_response(StatusCode::BAD_REQUEST, "Role Not Found");
|
||||
}
|
||||
};
|
||||
|
||||
if user_repo
|
||||
.query_user_by_email(payload.email.clone())
|
||||
.await
|
||||
@@ -113,50 +226,55 @@ impl AuthService {
|
||||
{
|
||||
return common_response(StatusCode::BAD_REQUEST, "User already exists");
|
||||
}
|
||||
|
||||
let hashed_password = match hash_password(&payload.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to hash password during registration for {}: {}",
|
||||
payload.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let new_user = AuthRegisterRequestDto {
|
||||
email: payload.email,
|
||||
email: payload.email.clone(),
|
||||
password: hashed_password,
|
||||
fullname: payload.fullname,
|
||||
phone_number: payload.phone_number,
|
||||
};
|
||||
|
||||
let otp = generate_otp::OtpManager::generate_otp();
|
||||
|
||||
match auth_repo
|
||||
.query_store_otp(new_user.email.clone(), otp.clone())
|
||||
.await
|
||||
{
|
||||
match auth_repo.query_store_otp(new_user.email.clone(), otp).await {
|
||||
Ok(_) => {
|
||||
let message = format!("your otp code is {}", otp);
|
||||
if let Err(err) = send_email(&new_user.email, "OTP Verification", &message) {
|
||||
let message = format!("your otp code is {otp}");
|
||||
if let Err(err_send) =
|
||||
send_email(&new_user.email, "OTP Verification", &message)
|
||||
{
|
||||
error!(
|
||||
"Failed to send OTP email to {}: {}",
|
||||
new_user.email, err_send
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&err.to_string(),
|
||||
&err_send.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string());
|
||||
Err(err_store) => {
|
||||
error!("Failed to store OTP for {}: {}", new_user.email, err_store);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&err_store.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &role.id);
|
||||
let user_thing = make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
);
|
||||
|
||||
match user_repo
|
||||
.query_create_user(UsersSchema {
|
||||
id: user_thing,
|
||||
@@ -167,13 +285,15 @@ impl AuthService {
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
role: role_thing,
|
||||
is_active: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(err) => {
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
|
||||
Err(err_create) => {
|
||||
error!("Failed to create user {}: {}", new_user.email, err_create);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err_create.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +305,6 @@ impl AuthService {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let user_repo = UsersRepository::new(state);
|
||||
if user_repo
|
||||
.query_user_by_email(payload.email.clone())
|
||||
@@ -194,18 +313,25 @@ impl AuthService {
|
||||
{
|
||||
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||
}
|
||||
|
||||
let auth_repo = AuthRepository::new(state);
|
||||
let _ = auth_repo.query_get_stored_otp(payload.email.clone()).await;
|
||||
|
||||
let otp = generate_otp::OtpManager::generate_otp();
|
||||
let message = format!("Your OTP code is {}", otp);
|
||||
let message = format!("Your OTP code is {otp}");
|
||||
match auth_repo.query_store_otp(payload.email.clone(), otp).await {
|
||||
Ok(_) => match send_email(&payload.email, "OTP Verification", &message) {
|
||||
Ok(_) => common_response(StatusCode::OK, "OTP resent successfully"),
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_send) => {
|
||||
error!(
|
||||
"Failed to send OTP email to {}: {}",
|
||||
payload.email, err_send
|
||||
);
|
||||
common_response(StatusCode::BAD_REQUEST, &err_send.to_string())
|
||||
}
|
||||
},
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_store) => {
|
||||
error!("Failed to store OTP for {}: {}", payload.email, err_store);
|
||||
common_response(StatusCode::BAD_REQUEST, &err_store.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,13 +343,14 @@ impl AuthService {
|
||||
}
|
||||
let email = match decode_refresh_token(&payload.refresh_token) {
|
||||
Ok(token) => token.claims.sub,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token");
|
||||
}
|
||||
};
|
||||
let access_token = match encode_access_token(email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!("Failed to generate access token for {}: {}", email, _e);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
@@ -232,7 +359,8 @@ impl AuthService {
|
||||
};
|
||||
let refresh_token = match encode_refresh_token(email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!("Failed to generate refresh token for {}: {}", email, _e);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate refresh token",
|
||||
@@ -259,16 +387,27 @@ impl AuthService {
|
||||
let user_result = user_repo.query_user_by_email(payload.email.clone()).await;
|
||||
let user = match user_result {
|
||||
Ok(user) => user,
|
||||
Err(err) if err.to_string().contains("User not found") => {
|
||||
Err(err_find) if err_find.to_string().contains("User not found") => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||
}
|
||||
Err(err) => {
|
||||
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string());
|
||||
Err(err_other) => {
|
||||
error!(
|
||||
"Error finding user for forgot password {}: {}",
|
||||
payload.email, err_other
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&err_other.to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let token = match encode_reset_password_token(user.email) {
|
||||
let token = match encode_reset_password_token(user.email.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!(
|
||||
"Failed to generate reset password token for {}: {}",
|
||||
user.email, _e
|
||||
);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to generate access token",
|
||||
@@ -278,12 +417,17 @@ impl AuthService {
|
||||
let env = Env::new();
|
||||
let fe_url = env.fe_url;
|
||||
let message = format!(
|
||||
"You have requested a password reset. Please click the link below to continue: {}/auth/reset-password?token={}",
|
||||
fe_url, token
|
||||
"You have requested a password reset. Please click the link below to continue: {fe_url}/auth/reset-password?token={token}"
|
||||
);
|
||||
match send_email(&payload.email, "Reset Password Request", &message) {
|
||||
Ok(_) => common_response(StatusCode::OK, "Reset Password request send"),
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_send) => {
|
||||
error!(
|
||||
"Failed to send reset password email to {}: {}",
|
||||
payload.email, err_send
|
||||
);
|
||||
common_response(StatusCode::BAD_REQUEST, &err_send.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,33 +443,38 @@ impl AuthService {
|
||||
let email = payload.email.clone();
|
||||
let user = match user_repo.query_user_by_email(email.clone()).await {
|
||||
Ok(user) if !user.is_deleted => user,
|
||||
_ => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||
_ => {
|
||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||
}
|
||||
};
|
||||
let patch = UsersSchema {
|
||||
id: user.id.clone(),
|
||||
is_active: true,
|
||||
..UsersSchema::from(user)
|
||||
..UsersSchema::from(user.clone())
|
||||
};
|
||||
match auth_repo.query_get_stored_otp(email.clone()).await {
|
||||
Ok(stored_otp) => match stored_otp == payload.otp {
|
||||
true => match user_repo.query_update_user(patch).await {
|
||||
Ok(_) => match auth_repo.query_delete_stored_otp(email).await {
|
||||
Ok(_) => match auth_repo.query_delete_stored_otp(email.clone()).await {
|
||||
Ok(_) => common_response(StatusCode::OK, "Email verified successfully"),
|
||||
Err(e) => {
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
Err(e_del) => {
|
||||
error!("Failed to delete OTP for {}: {}", email, e_del);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e_del.to_string())
|
||||
}
|
||||
},
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_update) => {
|
||||
common_response(StatusCode::BAD_REQUEST, &err_update.to_string())
|
||||
}
|
||||
},
|
||||
false => match auth_repo.query_delete_stored_otp(email).await {
|
||||
false => match auth_repo.query_delete_stored_otp(email.clone()).await {
|
||||
Ok(_) => common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP"),
|
||||
Err(e) => common_response(
|
||||
Err(e_del_mismatch) => common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&format!("Failed to delete OTP: {}", e),
|
||||
&format!("Failed to delete OTP: {e_del_mismatch}"),
|
||||
),
|
||||
},
|
||||
},
|
||||
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||
Err(err_get) => common_response(StatusCode::BAD_REQUEST, &err_get.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,14 +487,15 @@ impl AuthService {
|
||||
}
|
||||
let repo = UsersRepository::new(state);
|
||||
let email = match extract_email_token(payload.token.clone()) {
|
||||
Some(email) => email,
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Invalid or missing token");
|
||||
}
|
||||
};
|
||||
let password = match hash_password(&payload.password) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
Err(_e) => {
|
||||
error!("Failed to hash new password for {}: {}", email, _e);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password",
|
||||
@@ -354,7 +504,9 @@ impl AuthService {
|
||||
};
|
||||
let user = match repo.query_user_by_email(email.clone()).await {
|
||||
Ok(user) if !user.is_deleted => user,
|
||||
_ => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||
_ => {
|
||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||
}
|
||||
};
|
||||
let patch = UsersSchema {
|
||||
id: user.id.clone(),
|
||||
@@ -363,7 +515,7 @@ impl AuthService {
|
||||
};
|
||||
match repo.query_update_user(patch).await {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user