Compare commits
33
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 | ||
|
|
1e5ed26f76 | ||
|
|
d97095a38d | ||
|
|
e48507901e | ||
|
|
a43ef8ec04 | ||
|
|
874f25aa80 | ||
|
|
12a730a394 | ||
|
|
bdcd41f54d | ||
|
|
96fb8eb7d3 |
No files matched your search
+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
+952
-675
File diff suppressed because it is too large.
Load diff
+53
-12
@@ -1,32 +1,73 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"imphnen-*",
|
||||
"tests",
|
||||
"imphnen-iam",
|
||||
"imphnen-cms",
|
||||
"imphnen-libs",
|
||||
"imphnen-utils",
|
||||
"imphnen-gacha",
|
||||
"imphnen-gateway",
|
||||
"imphnen-backend",
|
||||
"imphnen-entities",
|
||||
"imphnen-dimentorin",
|
||||
"imphnen-middleware",
|
||||
]
|
||||
|
||||
|
||||
[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
|
||||
@@ -1,13 +1,16 @@
|
||||
[package]
|
||||
name = "imphnen-backend-service"
|
||||
name = "imphnen-backend"
|
||||
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-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-gateway-service = { version = "0.1.0", path = "../imphnen-gateway-service" }
|
||||
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
|
||||
@@ -23,3 +26,10 @@ chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
env_logger.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace=true
|
||||
tokio-tungstenite.workspace = true
|
||||
url.workspace = true
|
||||
futures-util.workspace = true
|
||||
@@ -1,8 +1,9 @@
|
||||
use imphnen_gateway_service::gateway_service;
|
||||
use imphnen_gateway::gateway_service;
|
||||
use imphnen_libs::axum_init;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||
})
|
||||
@@ -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_ofLine truncated
|
||||
.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(())
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[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?;
|
||||
|
||||
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", permission.id()))
|
||||
.bind((
|
||||
"data",
|
||||
json!({
|
||||
"name": permission.to_string(),
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date()
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
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,8 +74,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
println!("✅ Inserted role: {}", name);
|
||||
println!("✅ Inserted role: {name}");
|
||||
}
|
||||
println!("✅ Semua role berhasil disimpan ke SurrealDB!");
|
||||
println!("✅ All Roles seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum};
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
load_env();
|
||||
let env = Env::new();
|
||||
let 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 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![]),
|
||||
];
|
||||
|
||||
for (role_id, permissions) in roles_permissions {
|
||||
let permission_refs: Vec<_> = permissions
|
||||
.iter()
|
||||
.map(|perm| make_thing("app_permissions", perm.id()))
|
||||
.collect();
|
||||
|
||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||
.bind(("role_id", role_id))
|
||||
.bind(("permissions", permission_refs))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("✅ Permissions updated for role: {role_id}");
|
||||
}
|
||||
|
||||
println!("✅ All roles permissions updated!");
|
||||
Ok(())
|
||||
}
|
||||
+13
-34
@@ -1,35 +1,15 @@
|
||||
use imphnen_iam::UsersSchema;
|
||||
use imphnen_libs::enviroment::load_env;
|
||||
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, sql::Thing, Surreal};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UsersSchema {
|
||||
pub id: Thing,
|
||||
pub fullname: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub avatar: Option<String>,
|
||||
pub phone_number: String,
|
||||
pub referral_code: Option<String>,
|
||||
pub referred_by: Option<String>,
|
||||
pub identity_number: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub is_deleted: bool,
|
||||
pub student_type: String,
|
||||
pub religion: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
pub is_profile_completed: bool,
|
||||
pub role: Thing,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -61,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(),
|
||||
@@ -68,16 +52,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
password: hash_password("password").unwrap(),
|
||||
avatar: None,
|
||||
phone_number: "081234567890".into(),
|
||||
referral_code: None,
|
||||
referred_by: None,
|
||||
identity_number: None,
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
student_type: "TNI".into(),
|
||||
religion: None,
|
||||
mentor_id: None,
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
is_profile_completed: false,
|
||||
role: Thing::from(("app_roles", role_id)),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
@@ -87,9 +66,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
.content(user)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted user: {} ({})", fullname, email);
|
||||
println!("✅ Inserted user: {fullname} ({email})");
|
||||
}
|
||||
|
||||
println!("✅ Semua users berhasil disimpan ke SurrealDB!");
|
||||
println!("✅ All Users seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use imphnen_gateway_service::gateway_service;
|
||||
use imphnen_gateway::gateway_service;
|
||||
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
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "imphnen-gacha-service"
|
||||
name = "imphnen-cms"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
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,8 @@ 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::*;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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::*;
|
||||
File renamed without changes.
@@ -1,145 +0,0 @@
|
||||
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};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = Env::new();
|
||||
let db = Surreal::new::<Ws>(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 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",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"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",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6",
|
||||
"Read Detail Users",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"35b0d992-65c8-4b62-b030-e6e0320e4048",
|
||||
"Delete Roles",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"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",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"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",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"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 {
|
||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||
.bind(("id", id))
|
||||
.bind((
|
||||
"data",
|
||||
json!({
|
||||
"name": name,
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date()
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
println!("✅ Inserted: {}", name);
|
||||
}
|
||||
println!("✅ Semua permissions berhasil disimpan ke SurrealDB!");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use imphnen_utils::{get_iso_date, make_thing, Env};
|
||||
use std::error::Error;
|
||||
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = Env::new();
|
||||
let db = Surreal::new::<Ws>(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 role_permissions = vec![
|
||||
(
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86", // Staf
|
||||
vec![
|
||||
"7c15e31d-36e2-49f9-97db-138c03fb0cf6", // Read List Users
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6", // Read Detail Users
|
||||
],
|
||||
),
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07", // Admin
|
||||
vec![
|
||||
"023e2dfe-93c3-4008-94a8-b5dff403f73b", // Create Users
|
||||
"96df0689-2ae9-4894-bf00-837c19415e5c", // Delete Users
|
||||
"98b3dc4c-0124-461f-afcd-166637c5e6e8", // Update Users
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6", // Read Detail Users
|
||||
"7c15e31d-36e2-49f9-97db-138c03fb0cf6", // Read List Users
|
||||
"9164ca6e-c7e3-4238-a15f-f36ab9577e7e", // Read List Roles
|
||||
"319ee593-ff0a-4f29-bbaf-9feb3174a3a2", // Create Roles
|
||||
"a00d5608-4c48-4542-845c-dfe004687022", // Update Roles
|
||||
"35b0d992-65c8-4b62-b030-e6e0320e4048", // Delete Roles
|
||||
],
|
||||
),
|
||||
];
|
||||
for (role_id, permission_ids) in role_permissions {
|
||||
let permission_refs: Vec<_> = permission_ids
|
||||
.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")
|
||||
.bind(("role_id", role_id))
|
||||
.bind(("permissions", permission_refs))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("✅ Updated permissions for role ID: {}", role_id);
|
||||
}
|
||||
println!("✅ Semua roles telah diperbarui dengan permissions di SurrealDB!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "imphnen-dimentorin-service"
|
||||
name = "imphnen-dimentorin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||
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
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod v1;
|
||||
pub use v1::*;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 50 of 197 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user