Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a5c560347 | ||
|
|
164c1860da | ||
|
|
ac8177d87e | ||
|
|
b4a9972ad7 | ||
|
|
9af1c2d163 | ||
|
|
495e043088 | ||
|
|
390f46b0e7 | ||
|
|
aa0b659b48 | ||
|
|
9214b32139 | ||
|
|
2ea6cd3d17 | ||
|
|
6d5af29de8 | ||
|
|
444c98074f | ||
|
|
c6ed5c5c19 | ||
|
|
3692b81324 | ||
|
|
9b5efeff87 | ||
|
|
1d34d29b0b | ||
|
|
7d1078f52a | ||
|
|
67d3f2fced | ||
|
|
b68e362a02 | ||
|
|
db44c5a51f | ||
|
|
6570bbf752 | ||
|
|
5667a0d608 | ||
|
|
a4bbc73c7e | ||
|
|
729335014f | ||
|
|
852e9652ee | ||
|
|
f225ee8969 | ||
|
|
4ee00f1fe5 | ||
|
|
d014a94ea4 | ||
|
|
e1bc336baa | ||
|
|
8144ab40e9 | ||
|
|
76201d8d3e | ||
|
|
c38b718eb4 | ||
|
|
222495472a | ||
|
|
12db8cc6b4 | ||
|
|
331a4a4e88 | ||
|
|
2ae43b3bcc | ||
|
|
4bba182ea3 | ||
|
|
5715e75593 | ||
|
|
11442c6285 | ||
|
|
05a5b39195 | ||
|
|
052fee4b78 | ||
|
|
c825a78c6f | ||
|
|
c4280e9953 | ||
|
|
30128a8fe2 | ||
|
|
e432a1a743 | ||
|
|
1b3366d735 | ||
|
|
b429b3a9c7 | ||
|
|
6fe495eed1 |
@@ -0,0 +1,2 @@
|
|||||||
|
[target.x86_64-pc-windows-msvc]
|
||||||
|
linker = "rust-lld.exe"
|
||||||
+14
-30
@@ -1,36 +1,6 @@
|
|||||||
# RUST_ENV : Mode environment aplikasi (misal: development, production)
|
|
||||||
# RUST_LOG : Level logging aplikasi Rust (misal: debug, info, warn, error)
|
|
||||||
# PORT : Port yang digunakan backend service
|
|
||||||
# SURREALDB_URL : URL koneksi ke SurrealDB (gunakan ws:// untuk WebSocket)
|
|
||||||
# SURREALDB_USERNAME : Username untuk SurrealDB
|
|
||||||
# SURREALDB_PASSWORD : Password untuk SurrealDB
|
|
||||||
# SURREALDB_NAMESPACE : Namespace SurrealDB yang digunakan
|
|
||||||
# SURREALDB_DBNAME : Nama database SurrealDB
|
|
||||||
# ACCESS_TOKEN_SECRET : Secret key untuk JWT access token (ganti dengan nilai rahasia)
|
|
||||||
# REFRESH_TOKEN_SECRET : Secret key untuk JWT refresh token (ganti dengan nilai rahasia)
|
|
||||||
# SMTP_EMAIL : Email yang digunakan untuk mengirim email (SMTP)
|
|
||||||
# SMTP_PASSWORD : Password email SMTP
|
|
||||||
# SMTP_NAME : Nama pengirim email (misal: nama aplikasi)
|
|
||||||
# SMTP_HOST : Host SMTP server (misal: smtp.gmail.com)
|
|
||||||
# REDISDB_URL : URL/host Redis database
|
|
||||||
# FE_URL : URL frontend aplikasi (misal: http://localhost)
|
|
||||||
# MINIO_ENDPOINT : Endpoint MinIO object storage
|
|
||||||
# MINIO_BUCKET_NAME : Nama bucket default MinIO
|
|
||||||
# MINIO_ACCESS_KEY : Access key MinIO atau MINIO_ROOT_USER
|
|
||||||
# MINIO_SECRET_KEY : Secret key MinIO atau MINIO_ROOT_PASSWORD
|
|
||||||
# MINIO_ROOT_USER : Username root MinIO (default: minioadmin)
|
|
||||||
# MINIO_ROOT_PASSWORD : Password root MinIO (default: minioadmin)
|
|
||||||
# GOOGLE_CLIENT_ID : Client ID OAuth Google (untuk login Google)
|
|
||||||
# GOOGLE_CLIENT_SECRET : Client Secret OAuth Google
|
|
||||||
|
|
||||||
RUST_ENV=development
|
RUST_ENV=development
|
||||||
RUST_LOG=debug
|
RUST_LOG=debug
|
||||||
PORT=4099
|
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
|
ACCESS_TOKEN_SECRET=your-access-token-secret-key-here
|
||||||
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
||||||
SMTP_EMAIL=your-email@example.com
|
SMTP_EMAIL=your-email@example.com
|
||||||
@@ -47,3 +17,17 @@ MINIO_SECURE=false
|
|||||||
|
|
||||||
GOOGLE_CLIENT_ID="your_google_client_id"
|
GOOGLE_CLIENT_ID="your_google_client_id"
|
||||||
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
||||||
|
POOL_SIZE=10
|
||||||
|
CONNECT_TIMEOUT=30
|
||||||
|
IDLE_TIMEOUT=60
|
||||||
|
MAX_LIFETIME=1800
|
||||||
|
STATEMENT_TIMEOUT=30000
|
||||||
|
IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000
|
||||||
|
SSLMODE=require
|
||||||
|
RETRY_ATTEMPTS=3
|
||||||
|
RETRY_DELAY=1
|
||||||
|
GOOGLE_REDIRECT_URL=http://localhost:8000/api/v1/auth/google/callback
|
||||||
|
|
||||||
|
CDN_URL=https://cdn.asepharyana.tech
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://gacha.imphnen.dev,https://imphnen.dev,https://dimentorin.imphnen.dev
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
name: Deploy to Ancikri
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Build the project
|
|
||||||
run: cargo build --release
|
|
||||||
|
|
||||||
- name: Stop service on VPS before upload
|
|
||||||
uses: appleboy/ssh-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
|
||||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
|
||||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
|
||||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
echo "Stopping the service before uploading the binary"
|
|
||||||
sudo systemctl stop imphnen-backend-service
|
|
||||||
|
|
||||||
- name: Upload artifact to VPS
|
|
||||||
uses: appleboy/scp-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
|
||||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
|
||||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
|
||||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
|
||||||
source: ./target/release/*
|
|
||||||
target: /opt/imphnen-backend-service/imphnen-backend-service
|
|
||||||
rm: true
|
|
||||||
overwrite: true
|
|
||||||
|
|
||||||
- name: Deploy to server
|
|
||||||
uses: appleboy/ssh-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
|
||||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
|
||||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
|
||||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Restarting the service"
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
|
|
||||||
sudo systemctl restart imphnen-backend-service
|
|
||||||
|
|
||||||
echo "Deployment completed successfully"
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: Nix Build & Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['develop']
|
||||||
|
pull_request:
|
||||||
|
branches: ['develop']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup Cachix
|
||||||
|
uses: cachix/cachix-action@v15
|
||||||
|
with:
|
||||||
|
name: msdqn
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: nix build .#default -o result
|
||||||
|
|
||||||
|
- name: Push to Cachix
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||||
|
run: cachix push msdqn result
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/develop'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
env:
|
||||||
|
INFRA_DEPLOY_KEY: ${{ secrets.INFRA_DEPLOY_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${INFRA_DEPLOY_KEY}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
ssh-keyscan 167.235.70.37 >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Update infra flake.lock
|
||||||
|
run: |
|
||||||
|
export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes"
|
||||||
|
git clone git@github.com:IMPHNEN/imphnen-infrastructure.git /tmp/infra
|
||||||
|
cd /tmp/infra
|
||||||
|
nix flake update imphnen-backend
|
||||||
|
if git diff --quiet flake.lock; then
|
||||||
|
echo "flake.lock unchanged, skipping"
|
||||||
|
else
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "chore: update imphnen-backend-service to ${GITHUB_SHA::7}"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key \
|
||||||
|
-o ConnectTimeout=30 \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
-o ServerAliveInterval=30 \
|
||||||
|
-o ServerAliveCountMax=40 \
|
||||||
|
root@167.235.70.37 \
|
||||||
|
'nixos-rebuild switch --flake github:IMPHNEN/imphnen-infrastructure#hetzner --refresh 2>&1 | tail -50'
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: rm -f ~/.ssh/deploy_key
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
name: Rust
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["develop"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["develop"]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --verbose
|
|
||||||
Generated
+1835
-2456
File diff suppressed because it is too large
Load Diff
+33
-18
@@ -1,18 +1,20 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"tests",
|
"imphnen-entities",
|
||||||
"imphnen-entities", # Most basic - core data structures
|
"imphnen-macros",
|
||||||
"imphnen-libs", # Depends on entities
|
"imphnen-libs",
|
||||||
"imphnen-utils", # Depends on libs and entities
|
"imphnen-storage",
|
||||||
"imphnen-middleware",# Utility for permissions
|
"imphnen-email",
|
||||||
"imphnen-iam", # Core auth service, depends on libs, utils, entities
|
"imphnen-utils",
|
||||||
"imphnen-cms", # Content management, depends on core services
|
"imphnen-middleware",
|
||||||
"imphnen-gacha", # Game mechanics, depends on core services
|
"imphnen-iam",
|
||||||
"imphnen-dimentorin",# Learning platform, depends on core services
|
"imphnen-cms",
|
||||||
"imphnen-hackathon", # Hackathon service, depends on core services
|
"imphnen-gacha",
|
||||||
"imphnen-gateway", # API gateway, depends on all services
|
"imphnen-dimentorin",
|
||||||
"imphnen-backend", # Main application, depends on all services
|
"imphnen-hackathon",
|
||||||
|
"imphnen-gateway",
|
||||||
|
"imphnen-backend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -21,24 +23,28 @@ async-trait = "0.1.83"
|
|||||||
oauth2 = "5.0.0"
|
oauth2 = "5.0.0"
|
||||||
reqwest = { version = "0.12.23", features = ["json"] }
|
reqwest = { version = "0.12.23", features = ["json"] }
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
axum = { version = "0.8.4", features = ["multipart"] }
|
axum = { version = "0.8.4", features = ["multipart", "macros"] }
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
tokio = { version = "1.47.1", features = ["full"] }
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||||
jsonwebtoken = "9.3.1"
|
jsonwebtoken = "9.3.1"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
utoipa = { version = "5.4.0", features = ["axum_extras", "uuid", "chrono"] }
|
||||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||||
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
|
||||||
thiserror = "2.0.14"
|
thiserror = "2.0.14"
|
||||||
anyhow = "1.0.99"
|
anyhow = "1.0.99"
|
||||||
rand = { version = "0.9.2", features = ["std", "alloc"] }
|
rand = { version = "0.9.2", features = ["std", "alloc"] }
|
||||||
rand_distr = "0.5.1"
|
rand_distr = "0.5.1"
|
||||||
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
|
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
validator = { version = "0.20.0", features = ["derive"] }
|
zod-rs = { version = "0.4", features = ["macros"] }
|
||||||
|
zod-rs-util = "0.4"
|
||||||
|
paginator-rs = "0.2"
|
||||||
|
paginator-utils = "0.2"
|
||||||
|
paginator-sea-orm = { version = "0.2", features = ["sqlx-postgres", "runtime-tokio"] }
|
||||||
|
paginator-axum = "0.2"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
axum-test = "17.3.0"
|
axum-test = "17.3.0"
|
||||||
@@ -59,6 +65,9 @@ urlencoding = "2.1"
|
|||||||
hyper = "1.6.0"
|
hyper = "1.6.0"
|
||||||
hyper-util = "0.1.16"
|
hyper-util = "0.1.16"
|
||||||
minio = "0.3.0"
|
minio = "0.3.0"
|
||||||
|
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros", "with-chrono", "uuid"] }
|
||||||
|
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "uuid", "chrono", "json", "macros"] }
|
||||||
|
num_cpus = "1.16.0"
|
||||||
|
|
||||||
|
|
||||||
tokio-test = "0.4.4"
|
tokio-test = "0.4.4"
|
||||||
@@ -70,7 +79,6 @@ tokio-tungstenite = "0.27"
|
|||||||
url = "2.5"
|
url = "2.5"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
http = "1.3"
|
http = "1.3"
|
||||||
tests = { path = "./tests" }
|
|
||||||
imphnen-iam = { path = "./imphnen-iam" }
|
imphnen-iam = { path = "./imphnen-iam" }
|
||||||
imphnen-cms = { path = "./imphnen-cms" }
|
imphnen-cms = { path = "./imphnen-cms" }
|
||||||
imphnen-libs = { path = "./imphnen-libs" }
|
imphnen-libs = { path = "./imphnen-libs" }
|
||||||
@@ -80,11 +88,18 @@ imphnen-gateway = { path = "./imphnen-gateway" }
|
|||||||
imphnen-backend = { path = "./imphnen-backend" }
|
imphnen-backend = { path = "./imphnen-backend" }
|
||||||
imphnen-entities = { path = "./imphnen-entities" }
|
imphnen-entities = { path = "./imphnen-entities" }
|
||||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||||
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
|
||||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||||
|
imphnen-macros = { path = "./imphnen-macros" }
|
||||||
|
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
||||||
|
imphnen-storage = { path = "./imphnen-storage" }
|
||||||
|
imphnen-email = { path = "./imphnen-email" }
|
||||||
|
bcrypt = "0.15"
|
||||||
|
image = { version = "0.25", features = ["png", "jpeg"] }
|
||||||
|
qrcode = { version = "0.14", default-features = false, features = ["image"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|
||||||
|
|||||||
+20
-12
@@ -11,30 +11,38 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
RUN mkdir -p imphnen-backend/src imphnen-cms/src imphnen-dimentorin/src \
|
RUN mkdir -p \
|
||||||
imphnen-entities/src imphnen-gacha/src imphnen-gateway/src \
|
imphnen-backend/src \
|
||||||
imphnen-iam/src imphnen-libs/src imphnen-middleware/src \
|
imphnen-cms/src \
|
||||||
imphnen-utils/src tests/src && \
|
imphnen-dimentorin/src \
|
||||||
|
imphnen-email/src \
|
||||||
|
imphnen-entities/src \
|
||||||
|
imphnen-gacha/src \
|
||||||
|
imphnen-gateway/src \
|
||||||
|
imphnen-hackathon/src \
|
||||||
|
imphnen-iam/src \
|
||||||
|
imphnen-libs/src \
|
||||||
|
imphnen-macros/src \
|
||||||
|
imphnen-middleware/src \
|
||||||
|
imphnen-storage/src \
|
||||||
|
imphnen-utils/src && \
|
||||||
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
||||||
find . -name "src" -type d -exec sh -c 'echo "// dummy" > "$1/lib.rs"' _ {} \;
|
find . -name "src" -type d -exec sh -c 'touch "$1/lib.rs"' _ {} \;
|
||||||
|
|
||||||
RUN echo '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
|
||||||
|
|
||||||
|
|
||||||
RUN echo -e '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
|
||||||
|
|
||||||
|
|
||||||
COPY imphnen-backend ./imphnen-backend
|
COPY imphnen-backend ./imphnen-backend
|
||||||
COPY imphnen-cms ./imphnen-cms
|
COPY imphnen-cms ./imphnen-cms
|
||||||
COPY imphnen-dimentorin ./imphnen-dimentorin
|
COPY imphnen-dimentorin ./imphnen-dimentorin
|
||||||
|
COPY imphnen-email ./imphnen-email
|
||||||
COPY imphnen-entities ./imphnen-entities
|
COPY imphnen-entities ./imphnen-entities
|
||||||
COPY imphnen-gacha ./imphnen-gacha
|
COPY imphnen-gacha ./imphnen-gacha
|
||||||
COPY imphnen-gateway ./imphnen-gateway
|
COPY imphnen-gateway ./imphnen-gateway
|
||||||
|
COPY imphnen-hackathon ./imphnen-hackathon
|
||||||
COPY imphnen-iam ./imphnen-iam
|
COPY imphnen-iam ./imphnen-iam
|
||||||
COPY imphnen-libs ./imphnen-libs
|
COPY imphnen-libs ./imphnen-libs
|
||||||
|
COPY imphnen-macros ./imphnen-macros
|
||||||
COPY imphnen-middleware ./imphnen-middleware
|
COPY imphnen-middleware ./imphnen-middleware
|
||||||
|
COPY imphnen-storage ./imphnen-storage
|
||||||
COPY imphnen-utils ./imphnen-utils
|
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" \
|
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 && \
|
cargo build -p imphnen-backend --release && \
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
@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,30 +0,0 @@
|
|||||||
function Set-TempEnvFromDotEnv {
|
|
||||||
param (
|
|
||||||
[string]$envFilePath
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-Not (Test-Path $envFilePath)) {
|
|
||||||
Write-Error "The .env file at path '$envFilePath' does not exist."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$envContent = Get-Content $envFilePath
|
|
||||||
|
|
||||||
foreach ($line in $envContent) {
|
|
||||||
$trimmedLine = $line.Trim()
|
|
||||||
|
|
||||||
if (-Not [string]::IsNullOrWhiteSpace($trimmedLine) -and -Not $trimmedLine.StartsWith("#")) {
|
|
||||||
$keyValue = $trimmedLine -split "=", 2
|
|
||||||
if ($keyValue.Length -eq 2) {
|
|
||||||
$key = $keyValue[0].Trim()
|
|
||||||
$value = $keyValue[1].Trim()
|
|
||||||
[System.Environment]::SetEnvironmentVariable($key, $value, [System.EnvironmentVariableTarget]::Process)
|
|
||||||
Write-Host "Set temporary environment variable: $key=$value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "All environment variables from '$envFilePath' have been set temporarily."
|
|
||||||
}
|
|
||||||
|
|
||||||
Set-TempEnvFromDotEnv -envFilePath ".env"
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set_temp_env_from_dotenv() {
|
|
||||||
local env_file_path="$1"
|
|
||||||
|
|
||||||
if [[ ! -f "$env_file_path" ]]; then
|
|
||||||
echo "Error: The .env file at path '$env_file_path' does not exist."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
||||||
trimmed_line=$(echo "$line" | xargs)
|
|
||||||
|
|
||||||
if [[ -n "$trimmed_line" && ! "$trimmed_line" =~ ^# ]]; then
|
|
||||||
key=$(echo "$trimmed_line" | cut -d '=' -f 1 | xargs)
|
|
||||||
value=$(echo "$trimmed_line" | cut -d '=' -f 2- | xargs)
|
|
||||||
export "$key=$value"
|
|
||||||
echo "Set temporary environment variable: $key=$value"
|
|
||||||
fi
|
|
||||||
done < "$env_file_path"
|
|
||||||
|
|
||||||
echo "All environment variables from '$env_file_path' have been set temporarily."
|
|
||||||
}
|
|
||||||
|
|
||||||
set_temp_env_from_dotenv ".env"
|
|
||||||
+22
-12
@@ -1,15 +1,25 @@
|
|||||||
{pkgs ? import <nixpkgs> {}}: let
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
let
|
||||||
rustDeps = pkgs.callPackage ./Cargo.nix {};
|
swaggerUi = pkgs.fetchurl {
|
||||||
packageEntry = rustDeps.workspaceMembers.${manifest.name};
|
url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip";
|
||||||
deps = packageEntry.build.cargoDeps or null;
|
hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw=";
|
||||||
|
};
|
||||||
in
|
in
|
||||||
pkgs.rustPlatform.buildRustPackage {
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
pname = manifest.name;
|
pname = "imphnen-backend";
|
||||||
version = manifest.version;
|
version = (pkgs.lib.importTOML ./imphnen-backend/Cargo.toml).package.version;
|
||||||
cargoDeps = deps;
|
|
||||||
src = pkgs.lib.cleanSource ./.;
|
src = pkgs.lib.cleanSource ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
nativeBuildInputs = [pkgs.openssl pkgs.pkg-config];
|
cargoBuildFlags = [
|
||||||
buildInputs = [pkgs.openssl];
|
"--package"
|
||||||
}
|
"imphnen-backend"
|
||||||
|
"--bin"
|
||||||
|
"api"
|
||||||
|
];
|
||||||
|
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||||
|
buildInputs = [ pkgs.openssl ];
|
||||||
|
preBuild = ''
|
||||||
|
export SWAGGER_UI_DOWNLOAD_URL="file://${swaggerUi}"
|
||||||
|
'';
|
||||||
|
doCheck = false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script deploy semua binary hasil build Rust (untuk Linux)
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
REMOTE_USER="asephs"
|
|
||||||
REMOTE_HOST="70.153.9.42"
|
|
||||||
REMOTE_PATH="/home/asephs/imphnen-backend-service"
|
|
||||||
|
|
||||||
# Build release binary
|
|
||||||
echo "🔧 Building Rust project..."
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# Filter hanya file executable tanpa ekstensi .exe, .rlib, atau .d
|
|
||||||
BINARIES=$(find target/release -maxdepth 1 -type f ! -name "*.exe" ! -name "*.rlib" ! -name "*.d")
|
|
||||||
|
|
||||||
if [ -z "$BINARIES" ]; then
|
|
||||||
echo "❌ Tidak ada binary Linux (.exe/.rlib/.d diabaikan)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload semua binary yang valid ke server
|
|
||||||
echo "🚀 Mengirim binary ke server..."
|
|
||||||
rsync -avz --compress-level=9 --progress \
|
|
||||||
$BINARIES \
|
|
||||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
|
|
||||||
|
|
||||||
# Jalankan ulang service utama di VPS
|
|
||||||
echo "♻️ Restart service utama di server..."
|
|
||||||
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
|
||||||
set -e
|
|
||||||
cd /home/asephs/imphnen-backend-service
|
|
||||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
||||||
if [ -f ~/.bashrc ]; then
|
|
||||||
source ~/.bashrc
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Pastikan semua binary bisa dieksekusi
|
|
||||||
chmod +x target/release/*
|
|
||||||
|
|
||||||
# Restart service utama (misalnya api.d)
|
|
||||||
if pm2 list | grep -q api; then
|
|
||||||
pm2 restart api --update-env
|
|
||||||
else
|
|
||||||
pm2 start target/release/api.d --name api
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Deploy semua binary Linux selesai ke $REMOTE_HOST:$REMOTE_PATH"
|
|
||||||
+19
-5
@@ -7,13 +7,26 @@ services:
|
|||||||
- "${PORT}:${PORT}"
|
- "${PORT}:${PORT}"
|
||||||
env_file: ".env"
|
env_file: ".env"
|
||||||
depends_on:
|
depends_on:
|
||||||
- surrealdb
|
- postgres
|
||||||
|
|
||||||
surrealdb:
|
postgres:
|
||||||
image: surrealdb/surrealdb:latest
|
image: postgres:15-alpine
|
||||||
command: start --log trace --user root --pass root
|
container_name: imphnen_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-imphnen}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-imphnen}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
minio:
|
minio:
|
||||||
image: minio/minio:latest
|
image: minio/minio:latest
|
||||||
@@ -29,4 +42,5 @@ services:
|
|||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
postgres_data:
|
||||||
minio_data:
|
minio_data:
|
||||||
|
|||||||
+9
-8
@@ -1,19 +1,20 @@
|
|||||||
{pkgs, ...}: let
|
{ pkgs, ... }:
|
||||||
|
let
|
||||||
baseImage = pkgs.ociTools.pullImage {
|
baseImage = pkgs.ociTools.pullImage {
|
||||||
imageName = "ubuntu";
|
imageName = "ubuntu";
|
||||||
tag = "latest";
|
tag = "latest";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
pkgs.dockerTools.buildImage {
|
pkgs.dockerTools.buildImage {
|
||||||
name = "imphnen-cms-api";
|
name = "imphnen-backend-service";
|
||||||
|
|
||||||
fromImage = baseImage;
|
fromImage = baseImage;
|
||||||
|
|
||||||
copyToRoot = pkgs.buildEnv {
|
copyToRoot = pkgs.buildEnv {
|
||||||
name = "imphnen-cms-api";
|
name = "imphnen-backend-service";
|
||||||
paths = [
|
paths = [
|
||||||
(pkgs.stdenv.mkDerivation {
|
(pkgs.stdenv.mkDerivation {
|
||||||
name = "imphnen-cms-api";
|
name = "imphnen-backend-service";
|
||||||
src = ./src;
|
src = ./src;
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
@@ -29,14 +30,14 @@ in
|
|||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
cp target/release/najm-course-api $out/bin/
|
cp target/release/imphnen-backend-service $out/bin/
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
Cmd = ["/bin/imphnen-cms-api"];
|
Cmd = [ "/bin/imphnen-backend-service" ];
|
||||||
WorkingDir = "/bin";
|
WorkingDir = "/bin";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Dimentorin — Catatan Temuan Infra (Dev Audit, 2026-08-04)
|
||||||
|
|
||||||
|
Dokumen ini mencatat temuan yang membutuhkan perhatian tim sebelum produksi.
|
||||||
|
Semua diuji lokal (Postgres `dimentorin`, backend :4099).
|
||||||
|
|
||||||
|
## 1. SMTP email verification broken (blocker aktivasi user baru) — ✅ FIXED (2026-08-05, Google App Password)
|
||||||
|
|
||||||
|
- Endpoint `POST /v1/iam/auth/send-otp` gagal: `SMTP transport error (535): Username and Password not accepted` — kredensial `.env` (`SMTP_EMAIL=dev@example.com`, `SMTP_PASSWORD=dev`) ditolak Google SMTP.
|
||||||
|
- `POST /v1/iam/auth/verify-email` tetap butuh OTP untuk memanggil, tapi lihat poin 2.
|
||||||
|
- **Dampak**: mentee/mentor baru tak bisa menerima OTP lewat email → tak bisa aktivasi → tak bisa login, kecuali via verify-email langsung.
|
||||||
|
- **Diperlukan**: SMTP credential institution yang valid (Gmail App Password atau SMTP relay), sebaiknya dari BWS secret management, bukan hardcode.
|
||||||
|
|
||||||
|
## 2. ✅ FIXED — verify-email TIDAK memverifikasi OTP (security issue)
|
||||||
|
|
||||||
|
**Status: FIXED di branch feat/dimentorin-postgres (2026-08-04).**
|
||||||
|
|
||||||
|
`imphnen-iam/src/auth/application/mod.rs` → `verify_email()`:
|
||||||
|
|
||||||
|
- OTP sekarang dipersist ke tabel **`app_otp_cache`** (entity baru `imphnen-entities/src/seaorm/common/otp_cache.rs`, resource `app_otp_cache` sudah direncanakan di `ResourceEnum::OtpCache`).
|
||||||
|
- `register()` & `resend_otp()` menyimpan `otp_hash` + `expires_at` setelah email terkirim (kalau email gagal, tidak ada OTP yatim / OTP lama tidak di-overwrite).
|
||||||
|
- `verify_email()` memanggil `OtpManager::validate_otp_hash(stored_hash, expires_at, payload.otp)` sebelum set `is_active`. `validate_otp_hash` ditambahkan ke `OtpManager` (pure hash+expiry tanpa perlu plaintext code).
|
||||||
|
- OTP **single-use**: di-delete setelah verifikasi sukses. Reuse / OTP tanpa cache / OTP expired semua ditolak (400).
|
||||||
|
- Uji lokal (Postgres, :4099): OTP salah → 400 "Invalid or expired OTP", user tetap inactive; OTP benar → 200 "Email verified successfully", user aktif, OTP dihapus; verify ulang → 400 "User already active"; email tanpa OTP → 400 "No OTP issued".
|
||||||
|
- Tabel dibuat via SQL manual (`create_schema.rs` ditambah `otp_cache` untuk bootstrap penuh).
|
||||||
|
|
||||||
|
## 3. (OK, sudah benar) Register mentor + booking
|
||||||
|
|
||||||
|
- `POST /v1/dimentorin/mentors/create` → 200, user + mentor profile dibuat, status `pending`, user tak tampil di list public sampai verified.
|
||||||
|
- `POST /v1/dimentorin/mentors/{id}/sessions/create` → 200, session pending.
|
||||||
|
- Kedua endpoint fungsional setelah fix UUID (commit 9b5efef).
|
||||||
|
|
||||||
|
## Rekomendasi
|
||||||
|
|
||||||
|
Tangani #1 dan #2 sebelum go-live. #2 adalah kelas bug "OTP di-generate tapi tak dipakai" — sisi verifikasi email saat ini tidak lebih dari form "set is_active=true tanpa autentikasi".
|
||||||
|
|
||||||
|
## 4. ✅ DONE — Payment flow (alur bisnis menjual)
|
||||||
|
|
||||||
|
**Status: DONE di feat/dimentorin-postgres (2026-08-05).**
|
||||||
|
|
||||||
|
- Tabel `app_payments`: amount (dari `mentoring_rate` mentor) + service_fee 2000 + total; method `va`/`qris`/`manual`; provider `manual` default (swap Midtrans/Xendit nanti — cukup ganti nilai `provider` dan implementasi `generate_external_ref`/notifikasi webhook).
|
||||||
|
- Routes protected: `POST /payments/sessions/{id}/create`, `GET /payments/me`, `GET /payments/{id}`, `POST /payments/{id}/confirm`.
|
||||||
|
- Guard: mentee hanya bisa akses payment miliknya (403 kalau bukan); confirm hanya Admin / Admin Pembayaran.
|
||||||
|
- `confirm_payment` otomatis mengubah session terkait `pending` -> `confirmed` (loop bisnis lengkap: book -> bayar -> sesi terkonfirmasi -> feedback).
|
||||||
|
- FE: PaymentStep pilih VA/QRIS, rate real dari `mentoring_rate`; modal appointment: book -> create payment -> tampil VA/QR dengan `external_ref` + total + expiry -> success. Service lib: `postCreatePayment/getMyPayments/getPaymentById/postConfirmPayment`.
|
||||||
|
- E2E verified (lokal :4099): create VA dan QRIS, confirm 200, re-confirm 409, non-admin 403, akses payment orang lain 403, session auto-confirmed.
|
||||||
|
- TODO produksi: isi kredensial payment gateway (Midtrans/Xendit) + webhook callback; SMTP masih blocker (#1).
|
||||||
|
|
||||||
|
## 5. Payment lifecycle complete: auto-paid refresh + dashboards (2026-08-05)
|
||||||
|
- POST /payments/{id}/refresh: polls Midtrans v2/{order_id}/status; settlement/capture -> payment paid + session confirmed automatically (e2e verified via sandbox simulator: VA paid -> refresh -> paid + confirmed)
|
||||||
|
- GET /payments/session/{id}: payments for one session, accessible by that session's mentee or mentor (powers both dashboards)
|
||||||
|
- Session mentor may confirm their own payments (previously admins only) — verified e2e
|
||||||
|
- FE: /mentoring/my-sessions (mentee) + /mentoring/mentor-dashboard (mentor), both with live payment status and refresh/confirm actions; QRIS step renders real QR from qr_string
|
||||||
|
|
||||||
|
|
||||||
|
## 6. Materi + AI Agent RAG (2026-08-05, commit 164c186 BE / 7294040 FE)
|
||||||
|
|
||||||
|
Fitur yang "harusnya ada" menurut user (Figma hanya berisi Design System, halaman Materi/AI Agent tidak ada di file) — dibangun dari pemahaman alur bisnis mentoring.
|
||||||
|
|
||||||
|
**Backend — modul materials** (`imphnen-dimentorin/src/materials/`):
|
||||||
|
- `app_materials` table: mentor_id, title, slug, category, description, content, cover_url, is_published
|
||||||
|
- Pola articles: domain/repository/service + postgres repo + DTO ZodValidate
|
||||||
|
- Routes: GET /materials (public, published), /materials/{id|slug|categories}, POST/PUT/DELETE (auth, author-only)
|
||||||
|
- ENV baru: AI_LLM_BASE_URL/API_KEY/MODEL, AI_EMBEDDING_MODEL, QDRANT_URL
|
||||||
|
|
||||||
|
**Backend — AI agent RAG** (`imphnen-dimentorin/src/ai_agent/`):
|
||||||
|
- Chunking materi (700 chars, overlap 80) -> embed via 9router `gemini/gemini-embedding-001` (3072 dim!)
|
||||||
|
- Qdrant collection `dimentorin_materi` (3072d cosine, point id = u64 dari uuid xor index — Qdrant TOLAK string non-UUID)
|
||||||
|
- Chat: embed question -> search top-4 -> LLM (`text` -> gemini-3.1-flash-lite) jawab dengan konteks + sources
|
||||||
|
- Routes: POST /ai/chat, POST /ai/materials/{id}/index, POST /ai/reindex
|
||||||
|
- 9router chat SELALU SSE-streaming walau tanpa stream:true — parser harus agregate `data:` lines
|
||||||
|
|
||||||
|
**Verified e2e**: reindex 3 chunks; chat 'ownership' -> source materi Rust 0.88; chat 'endpoint axum' -> Axum 0.82; browser: list materi, detail, chatbox jawab + sources 84/64/61%.
|
||||||
|
|
||||||
|
**Pitfall**: embedding Gemini = 3072 dim (bukan 768); Qdrant point id harus u64/UUID; model embedding yang berfungsi di 9router = `gemini/gemini-embedding-001` (llama-nemotron -> 'No credentials for provider: openai').
|
||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739020877,
|
"lastModified": 1775036866,
|
||||||
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -5,28 +5,43 @@
|
|||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs =
|
||||||
|
{
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
}: let
|
}:
|
||||||
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
let
|
||||||
pkgsFor = system:
|
supportedSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
"aarch64-linux"
|
||||||
|
];
|
||||||
|
pkgsFor =
|
||||||
|
system:
|
||||||
import nixpkgs {
|
import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config = {
|
config.allowUnfree = true;
|
||||||
allowUnfree = true;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
packages = forAllSystems (system: {
|
packages = forAllSystems (system: {
|
||||||
default = (pkgsFor system).callPackage ./default.nix {};
|
default = (pkgsFor system).callPackage ./default.nix { };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
overlays.default = final: _prev: {
|
||||||
|
imphnen-backend = final.callPackage ./default.nix { };
|
||||||
|
};
|
||||||
|
|
||||||
|
nixosModules.backend = ./nixos-module.nix;
|
||||||
|
|
||||||
devShells = forAllSystems (system: {
|
devShells = forAllSystems (system: {
|
||||||
default = (pkgsFor system).callPackage ./shell.nix {};
|
default = (pkgsFor system).callPackage ./shell.nix { };
|
||||||
});
|
});
|
||||||
|
|
||||||
dockerImages = forAllSystems (system: {
|
dockerImages = forAllSystems (system: {
|
||||||
tryOutApi = (pkgsFor system).callPackage ./docker.nix {};
|
tryOutApi = (pkgsFor system).callPackage ./docker.nix { };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "imphnen-backend"
|
name = "imphnen-backend"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@@ -8,8 +8,8 @@ name = "api"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "clear_db_test"
|
name = "create_schema"
|
||||||
path = "src/bin/clear_db_test.rs"
|
path = "src/bin/create_schema.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "seeder"
|
name = "seeder"
|
||||||
@@ -39,19 +39,16 @@ path = "src/bin/seed_roles.rs"
|
|||||||
name = "seed_roles_permissions"
|
name = "seed_roles_permissions"
|
||||||
path = "src/bin/seed_roles_permissions.rs"
|
path = "src/bin/seed_roles_permissions.rs"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "seed_teams"
|
|
||||||
path = "src/bin/seed_teams.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "seed_hackathons"
|
|
||||||
path = "src/bin/seed_hackathons.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "seed_test_data"
|
name = "seed_test_data"
|
||||||
path = "src/bin/seed_test_data.rs"
|
path = "src/bin/seed_test_data.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "test_postgres"
|
||||||
|
path = "src/bin/test_postgres.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
sea-orm.workspace = true
|
||||||
imphnen-libs.workspace = true
|
imphnen-libs.workspace = true
|
||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
imphnen-gateway.workspace = true
|
imphnen-gateway.workspace = true
|
||||||
@@ -60,16 +57,13 @@ imphnen-iam.workspace = true
|
|||||||
imphnen-cms.workspace = true
|
imphnen-cms.workspace = true
|
||||||
imphnen-gacha.workspace = true
|
imphnen-gacha.workspace = true
|
||||||
imphnen-dimentorin.workspace = true
|
imphnen-dimentorin.workspace = true
|
||||||
imphnen-hackathon.workspace = true
|
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
validator.workspace = true
|
|
||||||
axum-test.workspace = true
|
axum-test.workspace = true
|
||||||
surrealdb.workspace = true
|
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
@@ -80,6 +74,3 @@ env_logger.workspace = true
|
|||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
uuid.workspace=true
|
uuid.workspace=true
|
||||||
tokio-tungstenite.workspace = true
|
|
||||||
url.workspace = true
|
|
||||||
futures-util.workspace = true
|
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ use imphnen_libs::axum_init;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
axum_init(|postgres_db| async {
|
||||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
gateway_service(postgres_db).await
|
||||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::{ConnectionTrait, Statement};
|
||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let dry_run = args
|
||||||
|
.iter()
|
||||||
|
.any(|s| s == "--dry-run" || s == "--no-exec" || s == "--dry");
|
||||||
|
let force = args.iter().any(|s| s == "--force" || s == "-f");
|
||||||
|
|
||||||
|
println!("🔎 Clear DB script - WARNING: This will remove data from tables\n");
|
||||||
|
println!("Note: script now runs by default (no --yes required). To preview without executing, use --dry-run.\n");
|
||||||
|
|
||||||
|
let tables = vec![
|
||||||
|
"gacha_claims",
|
||||||
|
"gacha_rolls",
|
||||||
|
"gacha_items",
|
||||||
|
"gacha_credits",
|
||||||
|
"audit_logs",
|
||||||
|
"rate_limits",
|
||||||
|
"testimonials",
|
||||||
|
"events",
|
||||||
|
"app_mentors",
|
||||||
|
"app_sessions",
|
||||||
|
"app_roles_permissions",
|
||||||
|
"app_permissions",
|
||||||
|
"app_roles",
|
||||||
|
"app_users",
|
||||||
|
];
|
||||||
|
|
||||||
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
let mut existing_tables: Vec<&str> = vec![];
|
||||||
|
for t in tables.iter() {
|
||||||
|
let check_sql = format!(
|
||||||
|
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{}') as exists;",
|
||||||
|
t
|
||||||
|
);
|
||||||
|
let stmt = Statement::from_string(db.get_database_backend(), check_sql);
|
||||||
|
if let Ok(Some(row)) = pg_conn.query_one(stmt).await {
|
||||||
|
let exists_val: Option<bool> = row.try_get("", "exists").ok();
|
||||||
|
if exists_val.unwrap_or(false) {
|
||||||
|
existing_tables.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing_tables.is_empty() {
|
||||||
|
println!("No configured tables found to clear - nothing to do.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let truncate_sql = format!(
|
||||||
|
"TRUNCATE TABLE {} RESTART IDENTITY CASCADE;",
|
||||||
|
existing_tables.join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("The script will run the following SQL (on the DB configured by env vars):\n\n{}", truncate_sql);
|
||||||
|
|
||||||
|
let env_name = imphnen_libs::ENV.rust_env.clone();
|
||||||
|
if env_name == "production" && !force {
|
||||||
|
println!("Security: RUST_ENV=production; the script will NOT run without --force. Use --force to override.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
println!("Dry run enabled. No changes applied. To execute, re-run without --dry-run or use --force (in production).");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Executing truncate...\n");
|
||||||
|
|
||||||
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
|
let stmt = Statement::from_string(db.get_database_backend(), truncate_sql);
|
||||||
|
match pg_conn.execute(stmt).await {
|
||||||
|
Ok(_) => println!("✅ Successfully cleared DB tables"),
|
||||||
|
Err(e) => println!("❌ Failed to clear DB tables: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
|
|
||||||
use url::Url;
|
|
||||||
use futures_util::{StreamExt, SinkExt};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
// Menentukan kredensial dan detail koneksi secara langsung sebagai string statis
|
|
||||||
static SURREALDB_URL_WS: &str = "ws://localhost:8000/rpc";
|
|
||||||
static SURREALDB_USERNAME: &str = "root";
|
|
||||||
static SURREALDB_PASSWORD: &str = "root";
|
|
||||||
static SURREALDB_NAMESPACE: &str = "test";
|
|
||||||
static SURREALDB_DBNAME: &str = "test";
|
|
||||||
|
|
||||||
// Daftar tabel sebagai variabel static yang tidak dapat diubah
|
|
||||||
static TABLES_TO_CLEAR: &[&str] = &[
|
|
||||||
"app_events", "users", "roles", "permissions", "gacha_rolls",
|
|
||||||
"mentor_users", "gacha_claims", "gacha_credits", "gacha_items",
|
|
||||||
"mentor_profiles", "roles_permissions", "testimonials",
|
|
||||||
];
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// Tidak perlu memuat env lagi, karena kita menggunakan nilai hardcoded
|
|
||||||
// imphnen_libs::enviroment::load_env(); // Baris ini tidak lagi dibutuhkan
|
|
||||||
// let env = Env::new(); // Baris ini tidak lagi dibutuhkan
|
|
||||||
|
|
||||||
println!("DEBUG: URL WS: {}", SURREALDB_URL_WS);
|
|
||||||
println!("DEBUG: Username: {}", SURREALDB_USERNAME);
|
|
||||||
println!("DEBUG: Namespace: {}", SURREALDB_NAMESPACE);
|
|
||||||
println!("DEBUG: Database: {}", SURREALDB_DBNAME);
|
|
||||||
|
|
||||||
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
|
|
||||||
|
|
||||||
let (ws_stream, _) = connect_async(url.as_str()).await?;
|
|
||||||
let (mut write, mut read) = ws_stream.split();
|
|
||||||
|
|
||||||
// Authenticate (signin)
|
|
||||||
let signin_query = json!({
|
|
||||||
"method": "signin",
|
|
||||||
"params": [{
|
|
||||||
"user": SURREALDB_USERNAME, // Menggunakan SURREALDB_USERNAME statis
|
|
||||||
"pass": SURREALDB_PASSWORD, // Menggunakan SURREALDB_PASSWORD statis
|
|
||||||
}],
|
|
||||||
"id": 1
|
|
||||||
}).to_string();
|
|
||||||
println!("DEBUG: Sending signin query: {}", signin_query);
|
|
||||||
write.send(Message::Text(signin_query.into())).await?;
|
|
||||||
|
|
||||||
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
|
|
||||||
let signin_response_msg = signin_response?;
|
|
||||||
let signin_response_str = signin_response_msg.to_text()?;
|
|
||||||
println!("DEBUG: Signin response: {}", signin_response_str);
|
|
||||||
if signin_response_str.contains("\"error\":") {
|
|
||||||
return Err(format!("Signin failed: {}", signin_response_str).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use namespace and database
|
|
||||||
let use_query = json!({
|
|
||||||
"method": "use",
|
|
||||||
"params": [SURREALDB_NAMESPACE, SURREALDB_DBNAME], // Menggunakan NS & DB statis
|
|
||||||
"id": 2
|
|
||||||
}).to_string();
|
|
||||||
println!("DEBUG: Sending use query: {}", use_query);
|
|
||||||
write.send(Message::Text(use_query.into())).await?;
|
|
||||||
|
|
||||||
let use_response = read.next().await.ok_or("Failed to read use response")?;
|
|
||||||
let use_response_msg = use_response?;
|
|
||||||
let use_response_str = use_response_msg.to_text()?;
|
|
||||||
println!("DEBUG: Use response: {}", use_response_str);
|
|
||||||
if use_response_str.contains("\"error\":") {
|
|
||||||
return Err(format!("USE command failed: {}", use_response_str).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("INFO: Attempting to clear database tables via WebSocket...");
|
|
||||||
|
|
||||||
let mut all_clear = true;
|
|
||||||
for (i, table) in TABLES_TO_CLEAR.iter().enumerate() {
|
|
||||||
let remove_query = format!("REMOVE TABLE {};", table);
|
|
||||||
let query_json = json!({
|
|
||||||
"method": "query",
|
|
||||||
"params": [remove_query],
|
|
||||||
"id": i + 3
|
|
||||||
}).to_string();
|
|
||||||
|
|
||||||
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
|
|
||||||
write.send(Message::Text(query_json.into())).await?;
|
|
||||||
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
match response_result {
|
|
||||||
Ok(msg) => {
|
|
||||||
let response_str = msg.to_text()?;
|
|
||||||
if response_str.contains("\"error\":") {
|
|
||||||
println!("WARN: Failed to REMOVE TABLE {}: {}. Attempting DELETE type::{}.", table, response_str, table);
|
|
||||||
let delete_all_query = format!("DELETE FROM {};", table);
|
|
||||||
let delete_all_json = json!({
|
|
||||||
"method": "query",
|
|
||||||
"params": [delete_all_query],
|
|
||||||
"id": i + 300
|
|
||||||
}).to_string();
|
|
||||||
|
|
||||||
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
|
|
||||||
write.send(Message::Text(delete_all_json.into())).await?;
|
|
||||||
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
|
|
||||||
|
|
||||||
match delete_response_result {
|
|
||||||
Ok(delete_msg) => {
|
|
||||||
let delete_response_str = delete_msg.to_text()?;
|
|
||||||
if delete_response_str.contains("\"error\":") {
|
|
||||||
println!("ERROR: Failed to DELETE type::{} : {}", table, delete_response_str);
|
|
||||||
all_clear = false;
|
|
||||||
} else {
|
|
||||||
println!("INFO: Successfully DELETED type:: table: {}", table);
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(delete_e) => {
|
|
||||||
println!("ERROR: Error receiving response for DELETE type:: table {}: {}", table, delete_e);
|
|
||||||
all_clear = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("INFO: Successfully REMOVED TABLE: {}", table);
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("ERROR: Error receiving response for REMOVE TABLE {}: {}", table, e);
|
|
||||||
all_clear = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if table is empty after deletion attempt
|
|
||||||
let select_query = format!("SELECT * FROM {} LIMIT 1;", table);
|
|
||||||
let select_json = json!({
|
|
||||||
"method": "query",
|
|
||||||
"params": [select_query],
|
|
||||||
"id": i + 1000
|
|
||||||
}).to_string();
|
|
||||||
write.send(Message::Text(select_json.into())).await?;
|
|
||||||
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
|
|
||||||
match select_response_result {
|
|
||||||
Ok(select_msg) => {
|
|
||||||
let select_response_str = select_msg.to_text()?;
|
|
||||||
if select_response_str.contains("does not exist") {
|
|
||||||
println!("CHECK: Table '{}' does not exist after clear attempt (success).", table);
|
|
||||||
} else if select_response_str.contains("\"result\":[]") || select_response_str.contains("\"result\":[[]]") {
|
|
||||||
println!("CHECK: Table '{}' is empty after clear attempt.", table);
|
|
||||||
} else {
|
|
||||||
println!("WARNING: Table '{}' is NOT empty after clear attempt! Response: {}", table, select_response_str);
|
|
||||||
all_clear = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("ERROR: Error receiving response for SELECT check on table {}: {}", table, e);
|
|
||||||
all_clear = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("INFO: Database clearing complete.");
|
|
||||||
|
|
||||||
if !all_clear {
|
|
||||||
eprintln!("ERROR: One or more tables could not be cleared. Check logs for details.");
|
|
||||||
return Err("Database clearing failed for one or more tables.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_entities::seaorm::{auth, common, gacha};
|
||||||
|
use imphnen_libs::postgres::PostgresConfig;
|
||||||
|
use sea_orm::sea_query::Table;
|
||||||
|
use sea_orm::{ConnectionTrait, Database, DbBackend, EntityTrait, Schema};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🛠️ Creating database schema...");
|
||||||
|
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let db = Database::connect(&config.database_url).await?;
|
||||||
|
let builder = db.get_database_backend();
|
||||||
|
|
||||||
|
println!(" Database connected. Creating/updating tables...");
|
||||||
|
|
||||||
|
drop_and_create_table(&db, builder, "app_roles", auth::roles::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_permissions", auth::permissions::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_users", auth::users::Entity).await?;
|
||||||
|
drop_and_create_table(
|
||||||
|
&db,
|
||||||
|
builder,
|
||||||
|
"app_roles_permissions",
|
||||||
|
auth::roles_permissions::Entity,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_mentors", auth::mentors::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_sessions", auth::sessions::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_articles", common::articles::Entity)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
drop_and_create_table(&db, builder, "events", common::events::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "testimonials", common::testimonials::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "audit_logs", common::audit_log::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "rate_limits", common::rate_limit::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "otp_cache", common::otp_cache::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "payments", common::payments::Entity)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
drop_and_create_table(&db, builder, "gacha_credits", gacha::gacha_credits::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "gacha_items", gacha::gacha_items::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "gacha_rolls", gacha::gacha_rolls::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "gacha_claims", gacha::gacha_claims::Entity)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("✅ Schema creation completed.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn drop_and_create_table<E>(
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
builder: DbBackend,
|
||||||
|
name: &str,
|
||||||
|
entity: E,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
where
|
||||||
|
E: EntityTrait,
|
||||||
|
{
|
||||||
|
let schema = Schema::new(builder);
|
||||||
|
|
||||||
|
let drop_stmt = Table::drop().table(entity).if_exists().cascade().to_owned();
|
||||||
|
db.execute(builder.build(&drop_stmt)).await?;
|
||||||
|
println!(" Dropped table if exists: {}", name);
|
||||||
|
|
||||||
|
let mut create_stmt = schema.create_table_from_entity(entity);
|
||||||
|
create_stmt.if_not_exists();
|
||||||
|
|
||||||
|
db.execute(builder.build(&create_stmt)).await?;
|
||||||
|
println!(" ✅ Created table: {}", name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use imphnen_libs::jsonwebtoken::encode_access_token;
|
use imphnen_libs::jsonwebtoken::encode_access_token;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
@@ -8,7 +10,6 @@ fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
let sub = args[1].clone();
|
let sub = args[1].clone();
|
||||||
// Use sub as both sub and user_id
|
|
||||||
match encode_access_token(sub.clone(), sub.clone()) {
|
match encode_access_token(sub.clone(), sub.clone()) {
|
||||||
Ok(token) => println!("{}", token),
|
Ok(token) => println!("{}", token),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::common::articles::{
|
||||||
|
ActiveModel as ArticleActiveModel, Entity as ArticlesEntity,
|
||||||
|
};
|
||||||
|
use imphnen_libs::postgres::PostgresConfig;
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue, Database};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let db = Database::connect(&config.database_url).await?;
|
||||||
|
|
||||||
|
let articles = vec![
|
||||||
|
("Cara Memulai Karier di UI/UX Design", "cara-memulai-karier-ui-ux-design", "UI/UX & Design", "Panduan lengkap untuk masuk ke dunia UI/UX design, dari skill yang dibutuhkan hingga portofolio.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit. UI/UX design adalah bidang yang menjanjikan. Artikel ini membahas langkah awal memulai karier sebagai UI/UX designer, tools yang wajib dikuasai seperti Figma, serta cara membangun portofolio yang menarik bagi perekrut."),
|
||||||
|
("Belajar Rust: Panduan Pemula 2026", "belajar-rust-panduan-pemula-2026", "Software/Web Dev", "Bahasa pemrograman Rust sedang naik daun. Pelajari konsep ownership dan borrow checker.", "Rust adalah bahasa pemrograman yang fokus pada performa dan keamanan memori. Dalam artikel ini kita membahas ownership, borrowing, dan cara setup environment Rust di Linux dan Windows, serta contoh project sederhana."),
|
||||||
|
("Mengenal Machine Learning untuk Data Analyst", "mengenal-machine-learning-data-analyst", "Data & AI", "Peran Data Analyst berevolusi dengan hadirnya machine learning. Simak panduannya.", "Machine learning membuka peluang besar bagi data analyst. Artikel ini menjelaskan perbedaan data analysis dan machine learning, serta roadmap belajar dari Python, pandas, sampai scikit-learn."),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (title, slug, category, excerpt, content) in articles {
|
||||||
|
let am = ArticleActiveModel {
|
||||||
|
id: ActiveValue::Set(Uuid::new_v4()),
|
||||||
|
title: ActiveValue::Set(title.to_string()),
|
||||||
|
slug: ActiveValue::Set(slug.to_string()),
|
||||||
|
category: ActiveValue::Set(category.to_string()),
|
||||||
|
excerpt: ActiveValue::Set(excerpt.to_string()),
|
||||||
|
content: ActiveValue::Set(content.to_string()),
|
||||||
|
cover_url: ActiveValue::Set(None),
|
||||||
|
author_name: ActiveValue::Set(Some("IMPHNEN Editorial".to_string())),
|
||||||
|
is_published: ActiveValue::Set(true),
|
||||||
|
created_at: ActiveValue::Set(Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(Utc::now()),
|
||||||
|
};
|
||||||
|
am.insert(&db).await?;
|
||||||
|
println!("✅ Inserted article: {}", slug);
|
||||||
|
}
|
||||||
|
println!("🟢 All articles seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
#![allow(clippy::all)]
|
||||||
use imphnen_utils::{get_iso_date};
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::common::events::{
|
||||||
|
ActiveModel as EventsActiveModel, Entity as EventEntity,
|
||||||
|
};
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter,
|
||||||
|
};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use uuid::Uuid;
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
db.signin(Root {
|
let db = &pg_conn.conn;
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let events = vec![
|
let events = vec![
|
||||||
(
|
(
|
||||||
@@ -58,6 +58,96 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
"2025-09-20T13:00:00Z",
|
"2025-09-20T13:00:00Z",
|
||||||
"2025-09-22T15:00:00Z",
|
"2025-09-22T15:00:00Z",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Rust Programming Bootcamp",
|
||||||
|
"Intensive 3-day bootcamp to master Rust fundamentals and advanced concepts.",
|
||||||
|
"https://rust-bootcamp.example.com",
|
||||||
|
200.0,
|
||||||
|
Some("Bandung Digital Valley".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-10-01T09:00:00Z",
|
||||||
|
"2025-10-03T17:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"AI & Machine Learning Summit",
|
||||||
|
"Global summit discussing the future of AI and its impact on industries.",
|
||||||
|
"https://ai-summit.example.com",
|
||||||
|
300.0,
|
||||||
|
Some("Bali Nusa Dua Convention Center".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-11-15T08:00:00Z",
|
||||||
|
"2025-11-17T18:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Cybersecurity Awareness Webinar",
|
||||||
|
"Free webinar on best practices for personal and corporate cybersecurity.",
|
||||||
|
"https://cybersecurity-webinar.example.com",
|
||||||
|
0.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2025-12-05T14:00:00Z",
|
||||||
|
"2025-12-05T16:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Cloud Computing Workshop",
|
||||||
|
"Hands-on workshop on deploying scalable applications using AWS and Azure.",
|
||||||
|
"https://cloud-workshop.example.com",
|
||||||
|
120.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2026-01-20T10:00:00Z",
|
||||||
|
"2026-01-22T15:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Blockchain for Finance",
|
||||||
|
"Exploring the applications of blockchain technology in the financial sector.",
|
||||||
|
"https://blockchain-finance.example.com",
|
||||||
|
180.0,
|
||||||
|
Some("Jakarta Ritz-Carlton".to_string()),
|
||||||
|
false,
|
||||||
|
"2026-02-10T09:00:00Z",
|
||||||
|
"2026-02-11T17:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Game Development Jam",
|
||||||
|
"48-hour game development marathon for indie developers.",
|
||||||
|
"https://game-jam.example.com",
|
||||||
|
50.0,
|
||||||
|
Some("Yogyakarta Creative Hub".to_string()),
|
||||||
|
false,
|
||||||
|
"2026-03-15T18:00:00Z",
|
||||||
|
"2026-03-17T18:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"UX/UI Design Principles",
|
||||||
|
"Masterclass on creating intuitive and user-friendly interfaces.",
|
||||||
|
"https://uxui-design.example.com",
|
||||||
|
90.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2026-04-05T13:00:00Z",
|
||||||
|
"2026-04-07T16:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Data Science Fundamentals",
|
||||||
|
"Introduction to data analysis, visualization, and statistical modeling.",
|
||||||
|
"https://data-science.example.com",
|
||||||
|
110.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2026-05-12T10:00:00Z",
|
||||||
|
"2026-05-14T15:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"IoT Innovation Expo",
|
||||||
|
"Showcase of the latest Internet of Things devices and solutions.",
|
||||||
|
"https://iot-expo.example.com",
|
||||||
|
50.0,
|
||||||
|
Some("Surabaya Expo Center".to_string()),
|
||||||
|
false,
|
||||||
|
"2026-06-20T09:00:00Z",
|
||||||
|
"2026-06-22T18:00:00Z",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (
|
for (
|
||||||
@@ -67,29 +157,41 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
price,
|
price,
|
||||||
location,
|
location,
|
||||||
is_online,
|
is_online,
|
||||||
start_date,
|
start_date_str,
|
||||||
end_date,
|
end_date_str,
|
||||||
) in events
|
) in events
|
||||||
{
|
{
|
||||||
let uuid = Uuid::new_v4().to_string(); // Generate new UUID
|
let existing = EventEntity::find()
|
||||||
let event = EventsSchema {
|
.filter(<EventEntity as EntityTrait>::Column::Name.eq(name))
|
||||||
id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID
|
.one(db)
|
||||||
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?;
|
.await?;
|
||||||
|
if existing.is_some() {
|
||||||
|
println!("ℹ️ Skipping (already exists): {name}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uuid = Uuid::new_v4();
|
||||||
|
let mut event_model: EventsActiveModel = Default::default();
|
||||||
|
event_model.id = Set(uuid);
|
||||||
|
event_model.name = Set(name.to_string());
|
||||||
|
event_model.description = Set(description.to_string());
|
||||||
|
event_model.detail_link = Set(detail_link.to_string());
|
||||||
|
event_model.price = Set(price);
|
||||||
|
event_model.is_online = Set(is_online);
|
||||||
|
event_model.location = Set(location.clone());
|
||||||
|
event_model.start_date = Set(
|
||||||
|
chrono::DateTime::parse_from_rfc3339(start_date_str)?
|
||||||
|
.with_timezone(&chrono::Utc),
|
||||||
|
);
|
||||||
|
event_model.end_date = Set(
|
||||||
|
chrono::DateTime::parse_from_rfc3339(end_date_str)?
|
||||||
|
.with_timezone(&chrono::Utc),
|
||||||
|
);
|
||||||
|
event_model.is_deleted = Set(false);
|
||||||
|
event_model.created_at = Set(Utc::now());
|
||||||
|
event_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
|
event_model.insert(db).await?;
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"✅ Inserted event: {} ({})",
|
"✅ Inserted event: {} ({})",
|
||||||
|
|||||||
@@ -1,51 +1,84 @@
|
|||||||
use imphnen_utils::{get_iso_date};
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_entities::seaorm::gacha::gacha_items::ActiveModel as GachaItemActiveModel;
|
||||||
|
use imphnen_entities::seaorm::gacha::gacha_rolls::ActiveModel as GachaRollActiveModel;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::ActiveModelTrait;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::ConnectionTrait;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::opt::auth::Root;
|
use uuid::Uuid;
|
||||||
use surrealdb::sql::Thing;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let config = PostgresConfig::from_env()?;
|
||||||
use surrealdb::engine::any;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = &pg_conn.conn;
|
||||||
db.signin(Root {
|
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_gacha_items', $id)")
|
let check_item_sql =
|
||||||
.bind(("id", "1"))
|
"SELECT id FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1' LIMIT 1";
|
||||||
.await?;
|
let item_result = pg_conn
|
||||||
db.query("DELETE type::thing('app_gacha_rolls', $id)")
|
.query_one(sea_orm::Statement::from_string(
|
||||||
.bind(("id", "test-gacha-roll-001"))
|
db.get_database_backend(),
|
||||||
.await?;
|
check_item_sql,
|
||||||
let gacha_item_id = "1";
|
))
|
||||||
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?;
|
.await?;
|
||||||
|
let gacha_item_uuid = if let Some(ref row) = item_result {
|
||||||
|
row.try_get("", "id")?
|
||||||
|
} else {
|
||||||
|
let _ = pg_conn
|
||||||
|
.execute(sea_orm::Statement::from_string(
|
||||||
|
db.get_database_backend(),
|
||||||
|
"DELETE FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1'".to_string(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let new_uuid = Uuid::new_v4();
|
||||||
|
let mut item_model: GachaItemActiveModel = Default::default();
|
||||||
|
item_model.id = Set(new_uuid);
|
||||||
|
item_model.item_code = Set("ITEM_TEST_1".to_string());
|
||||||
|
item_model.name = Set("Test Gacha Item".to_string());
|
||||||
|
item_model.description = Set("Test item for gacha".to_string());
|
||||||
|
item_model.rarity = Set("common".to_string());
|
||||||
|
item_model.type_ = Set("item".to_string());
|
||||||
|
item_model.category = Set("test".to_string());
|
||||||
|
item_model.value = Set(1);
|
||||||
|
item_model.weight = Set(1.0);
|
||||||
|
item_model.stock = Set(10);
|
||||||
|
item_model.is_limited = Set(false);
|
||||||
|
item_model.created_at = Set(chrono::Utc::now());
|
||||||
|
item_model.updated_at = Set(chrono::Utc::now());
|
||||||
|
item_model.insert(db).await?;
|
||||||
println!("Gacha Item seeded successfully!");
|
println!("Gacha Item seeded successfully!");
|
||||||
|
new_uuid
|
||||||
|
};
|
||||||
|
|
||||||
let gacha_roll_id = "test-gacha-roll-001";
|
let gacha_roll_id = Uuid::new_v4();
|
||||||
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")
|
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||||
.bind(("id", gacha_roll_id))
|
roll_model.id = Set(gacha_roll_id);
|
||||||
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
|
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
.bind(("quantity", 10))
|
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||||
.bind(("weight", 1.0))
|
roll_model.item_id = Set(gacha_item_uuid);
|
||||||
.bind(("is_deleted", false))
|
roll_model.weight = Set(1.0);
|
||||||
.bind(("created_at", get_iso_date()))
|
roll_model.quantity = Set(10);
|
||||||
.bind(("updated_at", get_iso_date()))
|
roll_model.is_deleted = Set(false);
|
||||||
.await?;
|
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.insert(db).await?;
|
||||||
println!("Gacha Roll seeded successfully!");
|
println!("Gacha Roll seeded successfully!");
|
||||||
|
let gacha_roll_id = Uuid::new_v4();
|
||||||
|
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||||
|
roll_model.id = Set(gacha_roll_id);
|
||||||
|
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
|
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||||
|
roll_model.item_id = Set(gacha_item_uuid);
|
||||||
|
roll_model.weight = Set(1.0);
|
||||||
|
roll_model.quantity = Set(10);
|
||||||
|
roll_model.is_deleted = Set(false);
|
||||||
|
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
|
roll_model.insert(db).await?;
|
||||||
println!("✅ Gacha items and rolls seeded.");
|
println!("✅ Gacha items and rolls seeded.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,368 +0,0 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
|
|
||||||
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
|
|
||||||
HackathonStatus, HackathonEventType, HackathonPhase, SubmissionStatus, Prize
|
|
||||||
};
|
|
||||||
use imphnen_utils::get_iso_date;
|
|
||||||
use std::error::Error;
|
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
let env = &imphnen_libs::environment::ENV;
|
|
||||||
use surrealdb::engine::any;
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
|
||||||
db.signin(Root {
|
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Sample hackathon data
|
|
||||||
let hackathons = vec![
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
"AI Innovation Challenge 2025",
|
|
||||||
"Build the next generation of AI-powered applications that solve real-world problems.",
|
|
||||||
"2025-10-15T09:00:00Z",
|
|
||||||
"2025-10-17T18:00:00Z",
|
|
||||||
"2025-10-01T23:59:59Z",
|
|
||||||
Some(100),
|
|
||||||
HackathonStatus::RegistrationOpen,
|
|
||||||
Some("Artificial Intelligence & Machine Learning".to_string()),
|
|
||||||
Some("1. All code must be original\n2. Teams can have 2-5 members\n3. Projects must use AI/ML technologies".to_string()),
|
|
||||||
Some(vec![
|
|
||||||
Prize { position: 1, title: "Grand Prize".to_string(), description: Some("Winner gets full scholarship".to_string()), value: Some("$10,000".to_string()) },
|
|
||||||
Prize { position: 2, title: "Second Place".to_string(), description: Some("Runner-up prize".to_string()), value: Some("$5,000".to_string()) },
|
|
||||||
Prize { position: 3, title: "Third Place".to_string(), description: Some("Third place prize".to_string()), value: Some("$2,500".to_string()) },
|
|
||||||
]),
|
|
||||||
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-002",
|
|
||||||
"Green Tech Hackathon",
|
|
||||||
"Develop sustainable technology solutions for environmental challenges.",
|
|
||||||
"2025-11-20T10:00:00Z",
|
|
||||||
"2025-11-22T17:00:00Z",
|
|
||||||
"2025-11-05T23:59:59Z",
|
|
||||||
Some(75),
|
|
||||||
HackathonStatus::Draft,
|
|
||||||
Some("Sustainability & Green Technology".to_string()),
|
|
||||||
Some("Focus on renewable energy, waste reduction, and environmental monitoring.".to_string()),
|
|
||||||
Some(vec![
|
|
||||||
Prize { position: 1, title: "Eco Champion".to_string(), description: Some("Best environmental impact".to_string()), value: Some("$7,500".to_string()) },
|
|
||||||
Prize { position: 2, title: "Innovation Award".to_string(), description: Some("Most innovative solution".to_string()), value: Some("$3,500".to_string()) },
|
|
||||||
]),
|
|
||||||
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample hackathon events
|
|
||||||
let hackathon_events = vec![
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
"event-001",
|
|
||||||
"Opening Ceremony",
|
|
||||||
Some("Welcome and kickoff event for the AI Innovation Challenge".to_string()),
|
|
||||||
HackathonEventType::Ceremony,
|
|
||||||
"2025-10-15T09:00:00Z",
|
|
||||||
"2025-10-15T10:00:00Z",
|
|
||||||
Some("Main Auditorium".to_string()),
|
|
||||||
None,
|
|
||||||
Some(150),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
"event-002",
|
|
||||||
"AI Workshop: Getting Started",
|
|
||||||
Some("Introduction to AI frameworks and tools".to_string()),
|
|
||||||
HackathonEventType::Workshop,
|
|
||||||
"2025-10-15T14:00:00Z",
|
|
||||||
"2025-10-15T16:00:00Z",
|
|
||||||
None,
|
|
||||||
Some("https://zoom.us/meeting/ai-workshop".to_string()),
|
|
||||||
Some(80),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
"event-003",
|
|
||||||
"Judging Session",
|
|
||||||
Some("Final project presentations and judging".to_string()),
|
|
||||||
HackathonEventType::Judging,
|
|
||||||
"2025-10-17T14:00:00Z",
|
|
||||||
"2025-10-17T17:00:00Z",
|
|
||||||
Some("Innovation Lab".to_string()),
|
|
||||||
None,
|
|
||||||
Some(100),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample hackathon timeline
|
|
||||||
let hackathon_timeline = vec![
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
HackathonPhase::Registration,
|
|
||||||
"Registration Phase",
|
|
||||||
Some("Register your team and submit initial project ideas".to_string()),
|
|
||||||
"2025-10-01T00:00:00Z",
|
|
||||||
"2025-10-10T23:59:59Z",
|
|
||||||
true,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
HackathonPhase::Ideation,
|
|
||||||
"Ideation & Planning",
|
|
||||||
Some("Brainstorm and plan your AI solution".to_string()),
|
|
||||||
"2025-10-11T00:00:00Z",
|
|
||||||
"2025-10-14T23:59:59Z",
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
HackathonPhase::Development,
|
|
||||||
"Development Sprint",
|
|
||||||
Some("Build your AI-powered application".to_string()),
|
|
||||||
"2025-10-15T00:00:00Z",
|
|
||||||
"2025-10-16T23:59:59Z",
|
|
||||||
false,
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
HackathonPhase::Submission,
|
|
||||||
"Project Submission",
|
|
||||||
Some("Submit your final project and demo video".to_string()),
|
|
||||||
"2025-10-17T00:00:00Z",
|
|
||||||
"2025-10-17T12:00:00Z",
|
|
||||||
false,
|
|
||||||
4,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
HackathonPhase::Judging,
|
|
||||||
"Judging & Awards",
|
|
||||||
Some("Presentations and prize ceremony".to_string()),
|
|
||||||
"2025-10-17T13:00:00Z",
|
|
||||||
"2025-10-17T18:00:00Z",
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample hackathon submissions
|
|
||||||
let hackathon_submissions = vec![
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
"team-dev-001",
|
|
||||||
"AI-Powered Health Monitor",
|
|
||||||
"A machine learning application that predicts health risks using wearable device data.",
|
|
||||||
Some("https://github.com/team-dev/ai-health-monitor".to_string()),
|
|
||||||
Some("https://demo.ai-health-monitor.com".to_string()),
|
|
||||||
None,
|
|
||||||
vec!["Python".to_string(), "TensorFlow".to_string(), "React".to_string()],
|
|
||||||
SubmissionStatus::Submitted,
|
|
||||||
"2025-10-17T11:30:00Z",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"hackathon-001",
|
|
||||||
"team-design-001",
|
|
||||||
"Smart City Traffic Optimizer",
|
|
||||||
"AI system that optimizes traffic flow using computer vision and predictive analytics.",
|
|
||||||
Some("https://github.com/team-design/smart-traffic".to_string()),
|
|
||||||
Some("https://demo.smart-traffic.com".to_string()),
|
|
||||||
Some("https://slides.smart-traffic.com/presentation".to_string()),
|
|
||||||
vec!["JavaScript".to_string(), "Node.js".to_string(), "OpenCV".to_string()],
|
|
||||||
SubmissionStatus::UnderReview,
|
|
||||||
"2025-10-17T10:45:00Z",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Seed hackathons
|
|
||||||
for (
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
registration_deadline,
|
|
||||||
max_participants,
|
|
||||||
status,
|
|
||||||
theme,
|
|
||||||
rules,
|
|
||||||
prizes,
|
|
||||||
organizers,
|
|
||||||
) in hackathons {
|
|
||||||
db.query("DELETE type::thing('app_hackathons', $id)")
|
|
||||||
.bind(("id", id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let hackathon = HackathonSchema {
|
|
||||||
id: Thing::from(("app_hackathons", id)),
|
|
||||||
name: name.into(),
|
|
||||||
description: description.into(),
|
|
||||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
|
||||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
|
||||||
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
|
|
||||||
max_participants,
|
|
||||||
status,
|
|
||||||
theme,
|
|
||||||
rules,
|
|
||||||
prizes,
|
|
||||||
previous_winners: None,
|
|
||||||
organizers,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
|
|
||||||
.content(hackathon)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted hackathon: {name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed hackathon events
|
|
||||||
for (
|
|
||||||
hackathon_id,
|
|
||||||
event_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
event_type,
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
location,
|
|
||||||
virtual_link,
|
|
||||||
max_attendees,
|
|
||||||
is_mandatory,
|
|
||||||
) in hackathon_events {
|
|
||||||
db.query("DELETE type::thing('app_hackathon_events', $id)")
|
|
||||||
.bind(("id", event_id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let event = HackathonEventsSchema {
|
|
||||||
id: Thing::from(("app_hackathon_events", event_id)),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
|
||||||
title: title.into(),
|
|
||||||
description,
|
|
||||||
event_type,
|
|
||||||
start_time: DateTime::parse_from_rfc3339(start_time)?.with_timezone(&Utc),
|
|
||||||
end_time: DateTime::parse_from_rfc3339(end_time)?.with_timezone(&Utc),
|
|
||||||
location,
|
|
||||||
virtual_link,
|
|
||||||
max_attendees,
|
|
||||||
is_mandatory,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", event_id))
|
|
||||||
.content(event)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted hackathon event: {title}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed hackathon timeline
|
|
||||||
for (
|
|
||||||
hackathon_id,
|
|
||||||
phase,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
is_active,
|
|
||||||
order,
|
|
||||||
) in hackathon_timeline {
|
|
||||||
let timeline_id = format!("timeline-{}-{}", hackathon_id, order);
|
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
|
|
||||||
.bind(("id", timeline_id.clone()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let timeline = HackathonTimelineSchema {
|
|
||||||
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
|
||||||
phase,
|
|
||||||
title: title.into(),
|
|
||||||
description,
|
|
||||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
|
||||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
|
||||||
is_active,
|
|
||||||
order,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
|
|
||||||
.content(timeline)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted hackathon timeline: {title}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed hackathon submissions
|
|
||||||
for (
|
|
||||||
hackathon_id,
|
|
||||||
team_id,
|
|
||||||
project_name,
|
|
||||||
description,
|
|
||||||
repository_url,
|
|
||||||
demo_url,
|
|
||||||
slides_url,
|
|
||||||
technologies,
|
|
||||||
submission_status,
|
|
||||||
submitted_at,
|
|
||||||
) in hackathon_submissions {
|
|
||||||
let submission_id = format!("submission-{}-{}", hackathon_id, team_id);
|
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
|
|
||||||
.bind(("id", submission_id.clone()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let submission = HackathonSubmissionsSchema {
|
|
||||||
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
|
||||||
judge_feedback: None,
|
|
||||||
team_id: Some(Thing::from(("app_teams", team_id))),
|
|
||||||
project_name: Some(project_name.into()),
|
|
||||||
description: Some(description.into()),
|
|
||||||
repository_url,
|
|
||||||
upload_file_url: None,
|
|
||||||
demo_url,
|
|
||||||
slides_url,
|
|
||||||
technologies: Some(technologies),
|
|
||||||
contact_instagram: None,
|
|
||||||
contact_twitter: None,
|
|
||||||
contact_linkedin: None,
|
|
||||||
contact_facebook: None,
|
|
||||||
contact_youtube: None,
|
|
||||||
contact_tiktok: None,
|
|
||||||
contact_other: None,
|
|
||||||
submission_status: Some(submission_status),
|
|
||||||
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
|
|
||||||
.content(submission)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted hackathon submission: {project_name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("✅ All Hackathons seeded");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,92 +1,88 @@
|
|||||||
use imphnen_utils::{get_iso_date, hash_password};
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::roles::{
|
||||||
|
Column as RoleColumn, Entity as RoleEntity,
|
||||||
|
};
|
||||||
|
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||||
|
use imphnen_libs::hash_password;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, EntityTrait,
|
||||||
|
QueryFilter,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::opt::auth::Root;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let config = PostgresConfig::from_env()?;
|
||||||
use surrealdb::engine::any;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = &pg_conn.conn;
|
||||||
db.signin(Root {
|
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_mentors', $id)")
|
let _ = pg_conn
|
||||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
.execute(sea_orm::Statement::from_string(
|
||||||
.await?;
|
db.get_database_backend(),
|
||||||
|
"DELETE FROM app_mentors WHERE id = 'e6f78d23-83bf-5c2b-bcd4-001345678901'"
|
||||||
db.query("DELETE type::thing('app_users', $id)")
|
.to_string(),
|
||||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
use surrealdb::sql::Thing;
|
|
||||||
db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, legal_name = $legal_name, domicile = $domicile, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, last_education = $last_education, linkedin_url = $linkedin_url, github_url = $github_url, cv_url = $cv_url, portfolio_url = $portfolio_url, 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(("legal_name", "Mentor User"))
|
|
||||||
.bind(("domicile", "Jakarta, Indonesia"))
|
|
||||||
// .bind(("identity_document_url", "https://example.com/ktp.jpg"))
|
|
||||||
.bind(("phone_for_verification", "081234567890"))
|
|
||||||
.bind(("bio", "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant."))
|
|
||||||
.bind(("last_education", "S1 Teknik Informatika"))
|
|
||||||
.bind(("linkedin_url", "https://linkedin.com/in/mentor"))
|
|
||||||
.bind(("github_url", "https://github.com/mentor"))
|
|
||||||
.bind(("cv_url", Option::<String>::None))
|
|
||||||
.bind(("portfolio_url", Option::<String>::None))
|
|
||||||
.bind(("created_at", get_iso_date()))
|
|
||||||
.bind(("updated_at", get_iso_date()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
db.query("CREATE type::thing('app_mentors', $id) SET user_id = $user_id, industries = $industries, expertise = $expertise, languages = $languages, current_company = $current_company, current_role = $current_role, years_of_experience = $years_of_experience, topics_of_interest = $topics_of_interest, preferred_mentee_level = $preferred_mentee_level, preferred_mentoring_formats = $preferred_mentoring_formats, availability_commitment = $availability_commitment, mentoring_rate = $mentoring_rate, status = $status, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
|
||||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
|
||||||
.bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))))
|
|
||||||
.bind(("industries", vec!["Software", "Education"]))
|
|
||||||
.bind(("expertise", vec!["Rust", "Microservices"]))
|
|
||||||
.bind(("languages", vec!["Indonesian", "English"]))
|
|
||||||
.bind(("current_company", "PT Contoh"))
|
|
||||||
.bind(("current_role", "Senior Backend Engineer"))
|
|
||||||
.bind(("years_of_experience", 5))
|
|
||||||
.bind(("topics_of_interest", vec!["Rust Programming", "Backend Development"]))
|
|
||||||
.bind(("preferred_mentee_level", vec!["beginner", "intermediate"]))
|
|
||||||
.bind(("preferred_mentoring_formats", vec!["online", "offline"]))
|
|
||||||
.bind(("availability_commitment", "2 jam per minggu untuk mentoring online dan offline"))
|
|
||||||
.bind(("mentoring_rate", json!({
|
|
||||||
"amount": 100000,
|
|
||||||
"currency": "IDR",
|
|
||||||
"per_duration": "hour"
|
|
||||||
})))
|
|
||||||
.bind(("status", "verified"))
|
|
||||||
.bind(("is_deleted", false))
|
|
||||||
.bind(("created_at", get_iso_date()))
|
|
||||||
.bind(("updated_at", get_iso_date()))
|
|
||||||
.await?;
|
|
||||||
println!("Mentor created successfully!");
|
|
||||||
println!("Updating user with mentor_id...");
|
|
||||||
|
|
||||||
db.query("UPDATE type::thing('app_users', $id) SET mentor_id = $mentor_id")
|
|
||||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
|
||||||
.bind((
|
|
||||||
"mentor_id",
|
|
||||||
Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
|
|
||||||
))
|
))
|
||||||
.await?;
|
.await
|
||||||
println!("User updated with mentor_id successfully!");
|
.ok();
|
||||||
|
let _ = pg_conn
|
||||||
|
.execute(sea_orm::Statement::from_string(
|
||||||
|
db.get_database_backend(),
|
||||||
|
"DELETE FROM app_users WHERE email = 'mentor@example.com'".to_string(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let role = RoleEntity::find()
|
||||||
|
.filter(RoleColumn::Name.eq("Mentor"))
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
.ok_or("Role 'Mentor' not found")?;
|
||||||
|
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
|
let mut user_model: UsersActiveModel = Default::default();
|
||||||
|
user_model.id = Set(user_id);
|
||||||
|
user_model.email = Set("mentor@example.com".to_string());
|
||||||
|
user_model.password_hash = Set(hash_password("password").unwrap());
|
||||||
|
user_model.username = Set("mentor@example.com".to_string());
|
||||||
|
user_model.first_name = Set(Some("Mentor".to_string()));
|
||||||
|
user_model.last_name = Set(Some("User".to_string()));
|
||||||
|
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
||||||
|
user_model.is_active = Set(true);
|
||||||
|
user_model.is_verified = Set(true);
|
||||||
|
user_model.role_id = Set(Some(role.id));
|
||||||
|
user_model.created_at = Set(chrono::Utc::now());
|
||||||
|
user_model.updated_at = Set(chrono::Utc::now());
|
||||||
|
user_model.insert(db).await?;
|
||||||
|
|
||||||
|
let mentor_id = Uuid::new_v4();
|
||||||
|
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||||
|
mentor_model.id = Set(mentor_id);
|
||||||
|
mentor_model.user_id = Set(user_id);
|
||||||
|
mentor_model.industries = Set(Some(json!(["Software", "Education"])));
|
||||||
|
mentor_model.expertise = Set(Some(json!(["Rust", "Microservices"])));
|
||||||
|
mentor_model.languages = Set(Some(json!(["Indonesian", "English"])));
|
||||||
|
mentor_model.current_company = Set(Some("PT Contoh".to_string()));
|
||||||
|
mentor_model.current_role = Set(Some("Senior Backend Engineer".to_string()));
|
||||||
|
mentor_model.years_of_experience = Set(Some(5));
|
||||||
|
mentor_model.topics_of_interest =
|
||||||
|
Set(Some(json!(["Rust Programming", "Backend Development"])));
|
||||||
|
mentor_model.preferred_mentee_level = Set(Some("beginner".to_string()));
|
||||||
|
mentor_model.preferred_mentoring_formats = Set(Some(json!(["online", "offline"])));
|
||||||
|
mentor_model.availability_commitment = Set(Some(
|
||||||
|
"2 jam per minggu untuk mentoring online dan offline".to_string(),
|
||||||
|
));
|
||||||
|
mentor_model.mentoring_rate = Set(Some(100000.0));
|
||||||
|
mentor_model.status = Set(Some("verified".to_string()));
|
||||||
|
mentor_model.is_deleted = Set(false);
|
||||||
|
mentor_model.created_at = Set(chrono::Utc::now());
|
||||||
|
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||||
|
mentor_model.insert(db).await?;
|
||||||
|
println!("Mentor created successfully!");
|
||||||
|
|
||||||
println!("✅ Inserted mentor user: mentor@example.com");
|
println!("✅ Inserted mentor user: mentor@example.com");
|
||||||
println!("✅ Mentor user seeded");
|
println!("✅ Mentor user seeded");
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::auth::permissions::ActiveModel as PermissionActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::permissions::Entity as PermissionEntity;
|
||||||
use imphnen_iam::PermissionsEnum;
|
use imphnen_iam::PermissionsEnum;
|
||||||
use imphnen_utils::{get_iso_date};
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use serde_json::json;
|
use sea_orm::ActiveModelTrait;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use uuid::Uuid;
|
||||||
use surrealdb::opt::auth::Root;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let config = PostgresConfig::from_env()?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
db.signin(Root {
|
let db = &pg_conn.conn;
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for permission in [
|
for permission in [
|
||||||
PermissionsEnum::ReadListUsers,
|
PermissionsEnum::ReadListUsers,
|
||||||
@@ -56,18 +54,22 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
PermissionsEnum::DeleteMentors,
|
PermissionsEnum::DeleteMentors,
|
||||||
PermissionsEnum::Administrator,
|
PermissionsEnum::Administrator,
|
||||||
] {
|
] {
|
||||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
let parsed_id =
|
||||||
.bind(("id", permission.id()))
|
Uuid::parse_str(&permission.id()).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
.bind((
|
|
||||||
"data",
|
let existing = PermissionEntity::find_by_id(parsed_id).one(db).await?;
|
||||||
json!({
|
if existing.is_some() {
|
||||||
"name": permission.to_string(),
|
println!("ℹ️ Skipping (already exists): {permission}");
|
||||||
"is_deleted": false,
|
continue;
|
||||||
"created_at": get_iso_date(),
|
}
|
||||||
"updated_at": get_iso_date()
|
|
||||||
}),
|
let mut perm_model: PermissionActiveModel = Default::default();
|
||||||
))
|
perm_model.id = Set(parsed_id);
|
||||||
.await?;
|
perm_model.name = Set(permission.to_string());
|
||||||
|
perm_model.is_deleted = Set(false);
|
||||||
|
perm_model.created_at = Set(Utc::now());
|
||||||
|
perm_model.updated_at = Set(Utc::now());
|
||||||
|
perm_model.insert(db).await?;
|
||||||
println!("✅ Inserted: {permission}");
|
println!("✅ Inserted: {permission}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
use imphnen_utils::{get_iso_date};
|
use chrono::Utc;
|
||||||
use serde_json::json;
|
use imphnen_entities::seaorm::auth::roles::{Entity as RoleEntity, RoleBuilder};
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use uuid::Uuid;
|
||||||
use surrealdb::opt::auth::Root;
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let config = PostgresConfig::from_env()?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
db.signin(Root {
|
let db = &pg_conn.conn;
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let roles = vec![
|
let roles = vec![
|
||||||
(
|
(
|
||||||
@@ -55,23 +50,28 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (id, name, _created_at, _updated_at) in roles {
|
for (id, name, _created_at_str, _updated_at_str) in roles {
|
||||||
db.query("DELETE type::thing('app_roles', $id)")
|
let uuid = Uuid::parse_str(id).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
.bind(("id", id))
|
|
||||||
.await?;
|
let existing = RoleEntity::find_by_id(uuid).one(db).await?;
|
||||||
db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
|
if existing.is_some() {
|
||||||
.bind(("id", id))
|
println!("ℹ️ Skipping (already exists): {name}");
|
||||||
.bind((
|
continue;
|
||||||
"data",
|
}
|
||||||
json!({
|
|
||||||
"name": name,
|
let role_model = RoleBuilder::new()
|
||||||
"permissions": [],
|
.name(name.to_string())
|
||||||
"is_deleted": false,
|
.description("System generated role".to_string())
|
||||||
"created_at": get_iso_date(),
|
.permissions(vec![])
|
||||||
"updated_at": get_iso_date(),
|
.is_default(false)
|
||||||
}),
|
.build()?;
|
||||||
))
|
let mut role_model = role_model;
|
||||||
.await?;
|
role_model.id = Set(uuid);
|
||||||
|
role_model.is_system_role = Set(true);
|
||||||
|
role_model.created_at = Set(Utc::now());
|
||||||
|
role_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
|
role_model.insert(db).await?;
|
||||||
println!("✅ Inserted role: {name}");
|
println!("✅ Inserted role: {name}");
|
||||||
}
|
}
|
||||||
println!("✅ All Roles seeded");
|
println!("✅ All Roles seeded");
|
||||||
|
|||||||
@@ -1,41 +1,32 @@
|
|||||||
use imphnen_iam::{get_iso_date, make_thing, PermissionsEnum};
|
use imphnen_entities::seaorm::auth::roles::ActiveModel as RoleActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::roles::Entity as RolesEntity;
|
||||||
|
use imphnen_iam::PermissionsEnum;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::ActiveModelTrait;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use surrealdb::engine::any;
|
use uuid::Uuid;
|
||||||
use surrealdb::opt::auth::Root;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let config = PostgresConfig::from_env()?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
db.signin(Root {
|
let db = &pg_conn.conn;
|
||||||
username: &env.surrealdb_username,
|
println!(
|
||||||
password: &env.surrealdb_password,
|
"✅ Index 'user_email_index' defined on table 'users' for column 'email'."
|
||||||
})
|
);
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
db.query("DEFINE INDEX user_email_index ON TABLE users COLUMNS email UNIQUE;")
|
|
||||||
|
|
||||||
.await?;
|
|
||||||
db.query("DEFINE INDEX role_name_idx ON TABLE roles COLUMNS name UNIQUE;")
|
|
||||||
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
|
|
||||||
|
|
||||||
let roles_permissions = vec![
|
let roles_permissions = vec![
|
||||||
(
|
(
|
||||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
vec![
|
vec![PermissionsEnum::Administrator],
|
||||||
// Only Administrator permission - grants access to everything
|
|
||||||
PermissionsEnum::Administrator,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
vec![
|
vec![
|
||||||
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
|
PermissionsEnum::ReadListUsers,
|
||||||
PermissionsEnum::ReadOwnMentorProfile,
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
PermissionsEnum::UpdateOwnMentorProfile,
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
PermissionsEnum::ReadOwnMentorStatus,
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
@@ -70,7 +61,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
(
|
(
|
||||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
vec![
|
vec![
|
||||||
// Staff should be able to list roles and permissions in tests
|
|
||||||
PermissionsEnum::ReadListRoles,
|
PermissionsEnum::ReadListRoles,
|
||||||
PermissionsEnum::ReadListPermissions,
|
PermissionsEnum::ReadListPermissions,
|
||||||
PermissionsEnum::ReadListUsers,
|
PermissionsEnum::ReadListUsers,
|
||||||
@@ -86,7 +76,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
PermissionsEnum::ReadDetailGachaRolls,
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
PermissionsEnum::CreateGachaRolls,
|
PermissionsEnum::CreateGachaRolls,
|
||||||
PermissionsEnum::ExecuteGachaRolls,
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
PermissionsEnum::ManageAllTeams,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -97,17 +86,22 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (role_id, permissions) in roles_permissions {
|
for (role_id, permissions) in roles_permissions {
|
||||||
let permission_refs: Vec<_> = permissions
|
let role_uuid = Uuid::parse_str(role_id).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
|
let json_permissions = JsonValue::Array(
|
||||||
|
permissions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|perm| make_thing("app_permissions", &perm.id()))
|
.map(|p| JsonValue::String(p.id()))
|
||||||
.collect();
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
if let Some(role_model) = RolesEntity::find_by_id(role_uuid).one(db).await? {
|
||||||
.bind(("role_id", role_id))
|
let mut am: RoleActiveModel = role_model.into();
|
||||||
.bind(("permissions", permission_refs))
|
am.permissions = Set(Some(json_permissions));
|
||||||
.bind(("updated_at", get_iso_date()))
|
am.update(db).await?;
|
||||||
.await?;
|
|
||||||
println!("✅ Permissions updated for role: {role_id}");
|
println!("✅ Permissions updated for role: {role_id}");
|
||||||
|
} else {
|
||||||
|
println!("⚠️ Role with id {role_id} not found, skipping permissions update");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("✅ All roles permissions updated!");
|
println!("✅ All roles permissions updated!");
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
use imphnen_iam::v1::teams::TeamsSchema;
|
|
||||||
use imphnen_utils::get_iso_date;
|
|
||||||
use std::error::Error;
|
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
let env = &imphnen_libs::environment::ENV;
|
|
||||||
use surrealdb::engine::any;
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
|
||||||
db.signin(Root {
|
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let teams = vec![
|
|
||||||
(
|
|
||||||
"team-dev-001",
|
|
||||||
"Development Team",
|
|
||||||
Some("Core development team for the platform".to_string()),
|
|
||||||
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
|
|
||||||
true,
|
|
||||||
Some(10),
|
|
||||||
Some(vec!["Rust".to_string(), "Backend".to_string()]),
|
|
||||||
Some("Remote".to_string()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"team-design-001",
|
|
||||||
"Design Team",
|
|
||||||
Some("UI/UX design team".to_string()),
|
|
||||||
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
|
|
||||||
true,
|
|
||||||
Some(5),
|
|
||||||
Some(vec!["Figma".to_string(), "Design".to_string()]),
|
|
||||||
Some("Remote".to_string()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"team-qa-001",
|
|
||||||
"Quality Assurance Team",
|
|
||||||
Some("Testing and quality assurance team".to_string()),
|
|
||||||
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
|
|
||||||
false,
|
|
||||||
Some(8),
|
|
||||||
Some(vec!["Testing".to_string(), "Automation".to_string()]),
|
|
||||||
Some("Remote".to_string()),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in teams {
|
|
||||||
db.query("DELETE type::thing('app_teams', $id)")
|
|
||||||
.bind(("id", id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let team = TeamsSchema {
|
|
||||||
id: Thing::from(("app_teams", id)),
|
|
||||||
name: name.into(),
|
|
||||||
description,
|
|
||||||
leader_id: Thing::from(("app_users", leader_id)),
|
|
||||||
is_open,
|
|
||||||
max_members,
|
|
||||||
skills_required,
|
|
||||||
location,
|
|
||||||
avatar: None,
|
|
||||||
website_url: None,
|
|
||||||
github_url: None,
|
|
||||||
is_active: true,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: get_iso_date(),
|
|
||||||
updated_at: get_iso_date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<TeamsSchema>>(("app_teams", id))
|
|
||||||
.content(team)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted team: {name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("✅ All Teams seeded");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,174 +1,78 @@
|
|||||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
#![allow(clippy::all)]
|
||||||
use imphnen_cms::v1::landing::testimonials::testimonials_schema::TestimonialsSchema;
|
|
||||||
use imphnen_dimentorin::v1::mentors::mentors_schema::MentorSchema;
|
|
||||||
use imphnen_dimentorin::v1::mentors::mentors_dto::MentoringRate;
|
|
||||||
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
|
|
||||||
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema,
|
|
||||||
HackathonStatus, HackathonEventType, HackathonPhase
|
|
||||||
};
|
|
||||||
use imphnen_utils::get_iso_date;
|
|
||||||
use std::error::Error;
|
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||||
|
use imphnen_entities::seaorm::common::events::ActiveModel as EventsActiveModel;
|
||||||
|
use imphnen_entities::seaorm::common::testimonials::ActiveModel as TestimonialsActiveModel;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::ActiveModelTrait;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::error::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let config = PostgresConfig::from_env()?;
|
||||||
use surrealdb::engine::any;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = &pg_conn.conn;
|
||||||
db.signin(Root {
|
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Seed Events - handle existing data
|
let uuid = Uuid::new_v4().to_string();
|
||||||
let event = EventsSchema {
|
let mut event_model: EventsActiveModel = Default::default();
|
||||||
id: Thing::from(("app_events", "1")),
|
event_model.id = Set(Uuid::parse_str(&uuid)?);
|
||||||
name: "Test Event".to_string(),
|
event_model.name = Set("Test Event".to_string());
|
||||||
description: "Test event description".to_string(),
|
event_model.description = Set("Test event description".to_string());
|
||||||
detail_link: "https://example.com/event".to_string(),
|
event_model.detail_link = Set("https://example.com/event".to_string());
|
||||||
price: 50.0,
|
event_model.price = Set(50.0);
|
||||||
is_online: true,
|
event_model.is_online = Set(true);
|
||||||
start_date: get_iso_date(),
|
event_model.start_date = Set(Utc::now());
|
||||||
end_date: get_iso_date(),
|
event_model.end_date = Set(Utc::now() + chrono::Duration::days(1));
|
||||||
location: None,
|
event_model.location = Set(None);
|
||||||
is_deleted: false,
|
event_model.is_deleted = Set(false);
|
||||||
created_at: get_iso_date(),
|
match event_model.insert(db).await {
|
||||||
updated_at: get_iso_date(),
|
|
||||||
};
|
|
||||||
match db.create::<Option<EventsSchema>>(("app_events", "1"))
|
|
||||||
.content(event)
|
|
||||||
.await {
|
|
||||||
Ok(_) => println!("✅ Inserted test event"),
|
Ok(_) => println!("✅ Inserted test event"),
|
||||||
Err(_) => println!("⚠️ Test event already exists, skipping"),
|
Err(_) => {
|
||||||
|
println!("⚠️ Test event already exists or could not be inserted, skipping")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Seed Testimonials - handle existing data
|
let mut testimonial_model: TestimonialsActiveModel = Default::default();
|
||||||
let testimonial = TestimonialsSchema {
|
testimonial_model.id =
|
||||||
id: Thing::from(("app_testimonials", "1")),
|
Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
||||||
user: Thing::from(("app_users", "c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")),
|
testimonial_model.user_id =
|
||||||
role: "Student".to_string(),
|
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
content: "This is a great platform!".to_string(),
|
testimonial_model.role = Set("Student".to_string());
|
||||||
is_deleted: false,
|
testimonial_model.content = Set("This is a great platform!".to_string());
|
||||||
created_at: get_iso_date(),
|
testimonial_model.is_deleted = Set(false);
|
||||||
updated_at: get_iso_date(),
|
match testimonial_model.insert(db).await {
|
||||||
};
|
|
||||||
match db.create::<Option<TestimonialsSchema>>(("app_testimonials", "1"))
|
|
||||||
.content(testimonial)
|
|
||||||
.await {
|
|
||||||
Ok(_) => println!("✅ Inserted test testimonial"),
|
Ok(_) => println!("✅ Inserted test testimonial"),
|
||||||
Err(_) => println!("⚠️ Test testimonial already exists, skipping"),
|
Err(_) => println!(
|
||||||
|
"⚠️ Test testimonial already exists or could not be inserted, skipping"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Seed Hackathon - handle existing data
|
let mentor_id = Uuid::new_v4();
|
||||||
let hackathon = HackathonSchema {
|
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||||
id: Thing::from(("app_hackathons", "1")),
|
mentor_model.id = Set(mentor_id);
|
||||||
name: "Test Hackathon".to_string(),
|
mentor_model.user_id =
|
||||||
description: "Test hackathon description".to_string(),
|
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
start_date: Utc::now() + chrono::Duration::days(30),
|
mentor_model.industries = Set(Some(json!(["Technology", "Education"])));
|
||||||
end_date: Utc::now() + chrono::Duration::days(37),
|
mentor_model.expertise = Set(Some(json!(["Software Development"])));
|
||||||
registration_deadline: Utc::now() + chrono::Duration::days(25),
|
mentor_model.languages = Set(Some(json!(["English", "Indonesian"])));
|
||||||
max_participants: Some(100),
|
mentor_model.current_company = Set(Some("Tech Corp".to_string()));
|
||||||
status: HackathonStatus::Draft,
|
mentor_model.current_role = Set(Some("Senior Engineer".to_string()));
|
||||||
theme: Some("Technology".to_string()),
|
mentor_model.years_of_experience = Set(Some(5));
|
||||||
rules: Some("Follow the rules".to_string()),
|
mentor_model.topics_of_interest = Set(Some(json!(["Rust", "Web Development"])));
|
||||||
prizes: Some(vec![]),
|
mentor_model.preferred_mentee_level = Set(Some("Beginner".to_string()));
|
||||||
previous_winners: Some(vec![]),
|
mentor_model.preferred_mentoring_formats = Set(Some(json!(["1:1", "Group"])));
|
||||||
organizers: vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
|
mentor_model.availability_commitment = Set(Some("Weekly".to_string()));
|
||||||
is_deleted: false,
|
mentor_model.mentoring_rate = Set(Some(100.0));
|
||||||
created_at: Some(get_iso_date()),
|
mentor_model.status = Set(Some("active".to_string()));
|
||||||
updated_at: Some(get_iso_date()),
|
mentor_model.is_deleted = Set(false);
|
||||||
};
|
mentor_model.created_at = Set(chrono::Utc::now());
|
||||||
match db.create::<Option<HackathonSchema>>(("app_hackathons", "1"))
|
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||||
.content(hackathon)
|
mentor_model.insert(db).await?;
|
||||||
.await {
|
println!("✅ Inserted test mentor via SeaORM");
|
||||||
Ok(_) => println!("✅ Inserted test hackathon"),
|
|
||||||
Err(_) => println!("⚠️ Test hackathon already exists, skipping"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Seed Hackathon Event
|
|
||||||
let hackathon_event = HackathonEventsSchema {
|
|
||||||
id: Thing::from(("app_hackathon_events", "test-event-001")),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", "1")),
|
|
||||||
title: "Test Event".to_string(),
|
|
||||||
description: Some("Test hackathon event description".to_string()),
|
|
||||||
event_type: HackathonEventType::Workshop,
|
|
||||||
start_time: Utc::now() + chrono::Duration::days(30),
|
|
||||||
end_time: Utc::now() + chrono::Duration::days(30) + chrono::Duration::hours(6),
|
|
||||||
location: Some("Online".to_string()),
|
|
||||||
virtual_link: None,
|
|
||||||
max_attendees: Some(50),
|
|
||||||
is_mandatory: false,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
// Try to create hackathon event, skip if already exists
|
|
||||||
match db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", "test-event-001"))
|
|
||||||
.content(hackathon_event)
|
|
||||||
.await {
|
|
||||||
Ok(_) => println!("✅ Inserted test hackathon event"),
|
|
||||||
Err(_) => println!("⚠️ Test hackathon event already exists, skipping"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Seed Hackathon Timeline
|
|
||||||
let hackathon_timeline = HackathonTimelineSchema {
|
|
||||||
id: Thing::from(("app_hackathon_timeline", "test-timeline-001")),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", "1")),
|
|
||||||
phase: HackathonPhase::Registration,
|
|
||||||
title: "Test Timeline".to_string(),
|
|
||||||
description: Some("Test hackathon timeline description".to_string()),
|
|
||||||
start_date: Utc::now(),
|
|
||||||
end_date: Utc::now() + chrono::Duration::days(7),
|
|
||||||
is_active: true,
|
|
||||||
order: 1,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to create hackathon timeline, skip if already exists
|
|
||||||
match db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", "test-timeline-001"))
|
|
||||||
.content(hackathon_timeline)
|
|
||||||
.await {
|
|
||||||
Ok(_) => println!("✅ Inserted test hackathon timeline"),
|
|
||||||
Err(_) => println!("⚠️ Test hackathon timeline already exists, skipping"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Seed Mentor - handle existing data
|
|
||||||
let mentor = MentorSchema {
|
|
||||||
id: Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
|
|
||||||
user_id: Some(Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))),
|
|
||||||
industries: vec!["Technology".to_string(), "Education".to_string()],
|
|
||||||
expertise: vec!["Software Development".to_string()],
|
|
||||||
languages: vec!["English".to_string(), "Indonesian".to_string()],
|
|
||||||
current_company: "Tech Corp".to_string(),
|
|
||||||
current_role: "Senior Engineer".to_string(),
|
|
||||||
years_of_experience: 5,
|
|
||||||
topics_of_interest: vec!["Rust".to_string(), "Web Development".to_string()],
|
|
||||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
|
||||||
preferred_mentoring_formats: vec!["1:1".to_string(), "Group".to_string()],
|
|
||||||
availability_commitment: "Weekly".to_string(),
|
|
||||||
mentoring_rate: MentoringRate {
|
|
||||||
amount: 100,
|
|
||||||
currency: "IDR".to_string(),
|
|
||||||
per_duration: "hour".to_string(),
|
|
||||||
},
|
|
||||||
status: "active".to_string(),
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: get_iso_date(),
|
|
||||||
updated_at: get_iso_date(),
|
|
||||||
};
|
|
||||||
match db.create::<Option<MentorSchema>>(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
|
||||||
.content(mentor)
|
|
||||||
.await {
|
|
||||||
Ok(_) => println!("✅ Inserted test mentor"),
|
|
||||||
Err(_) => println!("⚠️ Test mentor already exists, skipping"),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("✅ All test data seeded successfully");
|
println!("✅ All test data seeded successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,433 +0,0 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
|
|
||||||
HackathonSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
|
|
||||||
HackathonStatus, HackathonPhase, SubmissionStatus, Prize,
|
|
||||||
};
|
|
||||||
use imphnen_iam::{UsersSchema, v1::teams::TeamsSchema};
|
|
||||||
use imphnen_utils::{get_iso_date, hash_password};
|
|
||||||
use std::error::Error;
|
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
let env = &imphnen_libs::environment::ENV;
|
|
||||||
use surrealdb::engine::any;
|
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
|
||||||
db.signin(Root {
|
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Test users for submission testing
|
|
||||||
let test_users = vec![
|
|
||||||
(
|
|
||||||
"test-user-001",
|
|
||||||
"testuser1@example.com",
|
|
||||||
"Test User 1",
|
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059", // User role
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"test-user-002",
|
|
||||||
"testuser2@example.com",
|
|
||||||
"Test User 2",
|
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"test-user-003",
|
|
||||||
"testuser3@example.com",
|
|
||||||
"Test User 3",
|
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Test teams for submission testing
|
|
||||||
let test_teams = vec![
|
|
||||||
(
|
|
||||||
"test-team-001",
|
|
||||||
"Test Team Alpha",
|
|
||||||
Some("Team for testing hackathon submissions".to_string()),
|
|
||||||
"test-user-001", // team leader
|
|
||||||
true,
|
|
||||||
Some(5),
|
|
||||||
Some(vec!["JavaScript".to_string(), "React".to_string()]),
|
|
||||||
Some("Remote".to_string()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"test-team-002",
|
|
||||||
"Test Team Beta",
|
|
||||||
Some("Another team for testing submissions".to_string()),
|
|
||||||
"test-user-002",
|
|
||||||
true,
|
|
||||||
Some(4),
|
|
||||||
Some(vec!["Python".to_string(), "Django".to_string()]),
|
|
||||||
Some("Remote".to_string()),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Test hackathon for submission testing
|
|
||||||
let test_hackathons = vec![
|
|
||||||
(
|
|
||||||
"test-hackathon-001",
|
|
||||||
"Test Hackathon 2025",
|
|
||||||
"Hackathon for testing submission functionality.",
|
|
||||||
"2025-12-01T09:00:00Z",
|
|
||||||
"2025-12-03T18:00:00Z",
|
|
||||||
"2025-11-25T23:59:59Z",
|
|
||||||
Some(50),
|
|
||||||
HackathonStatus::RegistrationOpen,
|
|
||||||
Some("Testing & Development".to_string()),
|
|
||||||
Some("1. Test all submission features\n2. Teams can have 2-5 members\n3. Submit by deadline".to_string()),
|
|
||||||
Some(vec![
|
|
||||||
Prize { position: 1, title: "Test Winner".to_string(), description: Some("Best test submission".to_string()), value: Some("$1,000".to_string()) },
|
|
||||||
Prize { position: 2, title: "Test Runner-up".to_string(), description: Some("Second best submission".to_string()), value: Some("$500".to_string()) },
|
|
||||||
]),
|
|
||||||
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Test hackathon timeline
|
|
||||||
let test_timeline = vec![
|
|
||||||
(
|
|
||||||
"test-hackathon-001",
|
|
||||||
HackathonPhase::Registration,
|
|
||||||
"Registration Phase",
|
|
||||||
Some("Register your team for the test hackathon".to_string()),
|
|
||||||
"2025-11-20T00:00:00Z",
|
|
||||||
"2025-11-25T23:59:59Z",
|
|
||||||
true,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"test-hackathon-001",
|
|
||||||
HackathonPhase::Development,
|
|
||||||
"Development Phase",
|
|
||||||
Some("Build your test project".to_string()),
|
|
||||||
"2025-12-01T00:00:00Z",
|
|
||||||
"2025-12-02T23:59:59Z",
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"test-hackathon-001",
|
|
||||||
HackathonPhase::Submission,
|
|
||||||
"Submission Phase",
|
|
||||||
Some("Submit your test project".to_string()),
|
|
||||||
"2025-12-03T00:00:00Z",
|
|
||||||
"2025-12-03T12:00:00Z",
|
|
||||||
false,
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Test submissions
|
|
||||||
let test_submissions = vec![
|
|
||||||
(
|
|
||||||
"test-hackathon-001",
|
|
||||||
"test-team-001",
|
|
||||||
"Test Project Alpha",
|
|
||||||
"A comprehensive test project demonstrating all features.",
|
|
||||||
Some("https://github.com/test-team-alpha/test-project".to_string()),
|
|
||||||
Some("https://demo.test-project-alpha.com".to_string()),
|
|
||||||
Some("https://slides.test-project-alpha.com".to_string()),
|
|
||||||
vec!["JavaScript".to_string(), "React".to_string(), "Node.js".to_string()],
|
|
||||||
SubmissionStatus::Draft,
|
|
||||||
"2025-12-02T10:00:00Z",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"test-hackathon-001",
|
|
||||||
"test-team-002",
|
|
||||||
"Test Project Beta",
|
|
||||||
"Another test project with different technologies.",
|
|
||||||
Some("https://github.com/test-team-beta/test-project".to_string()),
|
|
||||||
Some("https://demo.test-project-beta.com".to_string()),
|
|
||||||
None,
|
|
||||||
vec!["Python".to_string(), "Django".to_string(), "PostgreSQL".to_string()],
|
|
||||||
SubmissionStatus::Submitted,
|
|
||||||
"2025-12-03T09:30:00Z",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Seed test users
|
|
||||||
for (id, email, fullname, role_id) in test_users {
|
|
||||||
db.query("DELETE type::thing('app_users', $id)")
|
|
||||||
.bind(("id", id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let user = UsersSchema {
|
|
||||||
id: Thing::from(("app_users", id)),
|
|
||||||
fullname: fullname.into(),
|
|
||||||
legal_name: Some(format!("{} Legal Name", fullname)),
|
|
||||||
email: email.into(),
|
|
||||||
password: hash_password("password").unwrap(),
|
|
||||||
avatar: Some("https://example.com/avatar.jpg".into()),
|
|
||||||
phone_number: "081234567890".into(),
|
|
||||||
phone_for_verification: Some("081234567890".into()),
|
|
||||||
is_active: true,
|
|
||||||
is_deleted: false,
|
|
||||||
mentor_id: None,
|
|
||||||
gender: Some("male".into()),
|
|
||||||
birthdate: Some("1995-01-01".into()),
|
|
||||||
domicile: Some("Jakarta, Indonesia".into()),
|
|
||||||
bio: Some(format!("{} is a test user for hackathon submissions.", fullname)),
|
|
||||||
last_education: Some("S1 Computer Science".into()),
|
|
||||||
linkedin_url: Some("https://linkedin.com/in/testuser".into()),
|
|
||||||
github_url: Some("https://github.com/testuser".into()),
|
|
||||||
cv_url: Some("https://example.com/cv.pdf".into()),
|
|
||||||
portfolio_url: Some("https://example.com/portfolio".into()),
|
|
||||||
website_url: Some("https://example.com/website".into()),
|
|
||||||
twitter_url: Some("https://twitter.com/testuser".into()),
|
|
||||||
location: Some("Jakarta, Indonesia".into()),
|
|
||||||
skills: Some(vec!["JavaScript".to_string(), "Python".to_string()]),
|
|
||||||
experience: None,
|
|
||||||
education: None,
|
|
||||||
career_status: Some("Developer".into()),
|
|
||||||
role: Thing::from(("app_roles", role_id)),
|
|
||||||
created_at: get_iso_date(),
|
|
||||||
updated_at: get_iso_date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<UsersSchema>>(("app_users", id))
|
|
||||||
.content(user)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted test user: {fullname} ({email})");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed test teams
|
|
||||||
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in test_teams {
|
|
||||||
db.query("DELETE type::thing('app_teams', $id)")
|
|
||||||
.bind(("id", id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let team = TeamsSchema {
|
|
||||||
id: Thing::from(("app_teams", id)),
|
|
||||||
name: name.into(),
|
|
||||||
description,
|
|
||||||
leader_id: Thing::from(("app_users", leader_id)),
|
|
||||||
is_open,
|
|
||||||
max_members,
|
|
||||||
skills_required,
|
|
||||||
location,
|
|
||||||
avatar: None,
|
|
||||||
website_url: None,
|
|
||||||
github_url: None,
|
|
||||||
is_active: true,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: get_iso_date(),
|
|
||||||
updated_at: get_iso_date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<TeamsSchema>>(("app_teams", id))
|
|
||||||
.content(team)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted test team: {name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed test hackathons
|
|
||||||
for (
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
registration_deadline,
|
|
||||||
max_participants,
|
|
||||||
status,
|
|
||||||
theme,
|
|
||||||
rules,
|
|
||||||
prizes,
|
|
||||||
organizers,
|
|
||||||
) in test_hackathons {
|
|
||||||
db.query("DELETE type::thing('app_hackathons', $id)")
|
|
||||||
.bind(("id", id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let hackathon = HackathonSchema {
|
|
||||||
id: Thing::from(("app_hackathons", id)),
|
|
||||||
name: name.into(),
|
|
||||||
description: description.into(),
|
|
||||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
|
||||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
|
||||||
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
|
|
||||||
max_participants,
|
|
||||||
status: status.clone(),
|
|
||||||
theme: theme.clone(),
|
|
||||||
rules: rules.clone(),
|
|
||||||
prizes: prizes.clone(),
|
|
||||||
previous_winners: None,
|
|
||||||
organizers: organizers.clone(),
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
|
|
||||||
.content(hackathon)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted test hackathon: {name}");
|
|
||||||
// Also create an alias canonical id 'test-hackathon' so tests referencing
|
|
||||||
// /v1/hackathons/test-hackathon/... can find a hackathon record.
|
|
||||||
if id != "test-hackathon" && id.starts_with("test-hackathon") {
|
|
||||||
let alias_id = "test-hackathon";
|
|
||||||
db.query("DELETE type::thing('app_hackathons', $id)")
|
|
||||||
.bind(("id", alias_id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let alias_hackathon = HackathonSchema {
|
|
||||||
id: Thing::from(("app_hackathons", alias_id)),
|
|
||||||
name: name.into(),
|
|
||||||
description: description.into(),
|
|
||||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
|
||||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
|
||||||
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
|
|
||||||
max_participants,
|
|
||||||
status: status.clone(),
|
|
||||||
theme: theme.clone(),
|
|
||||||
rules: rules.clone(),
|
|
||||||
prizes: prizes.clone(),
|
|
||||||
previous_winners: None,
|
|
||||||
organizers: organizers.clone(),
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonSchema>>(("app_hackathons", alias_id))
|
|
||||||
.content(alias_hackathon)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted test hackathon alias: {alias_id}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed test hackathon timeline
|
|
||||||
for (
|
|
||||||
hackathon_id,
|
|
||||||
phase,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
is_active,
|
|
||||||
order,
|
|
||||||
) in test_timeline {
|
|
||||||
let timeline_id = format!("test-timeline-{}-{}", hackathon_id, order);
|
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
|
|
||||||
.bind(("id", timeline_id.clone()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let timeline = HackathonTimelineSchema {
|
|
||||||
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
|
||||||
phase: phase.clone(),
|
|
||||||
title: title.into(),
|
|
||||||
description: description.clone(),
|
|
||||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
|
||||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
|
||||||
is_active,
|
|
||||||
order,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
|
|
||||||
.content(timeline)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted test hackathon timeline: {title}");
|
|
||||||
|
|
||||||
// Also create alias timeline entries for the canonical test id 'test-hackathon'
|
|
||||||
if hackathon_id != "test-hackathon" && hackathon_id.starts_with("test-hackathon") {
|
|
||||||
let alias_hackathon_id = "test-hackathon";
|
|
||||||
let alias_timeline_id = format!("test-timeline-{}-{}", alias_hackathon_id, order);
|
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
|
|
||||||
.bind(("id", alias_timeline_id.clone()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let alias_timeline = HackathonTimelineSchema {
|
|
||||||
id: Thing::from(("app_hackathon_timeline", alias_timeline_id.as_str())),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", alias_hackathon_id)),
|
|
||||||
phase: phase.clone(),
|
|
||||||
title: title.into(),
|
|
||||||
description: description.clone(),
|
|
||||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
|
||||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
|
||||||
is_active,
|
|
||||||
order,
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonTimelineSchema>>( ("app_hackathon_timeline", alias_timeline_id.clone()) )
|
|
||||||
.content(alias_timeline)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted test hackathon timeline alias: {alias_timeline_id}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed test hackathon submissions
|
|
||||||
for (
|
|
||||||
hackathon_id,
|
|
||||||
team_id,
|
|
||||||
project_name,
|
|
||||||
description,
|
|
||||||
repository_url,
|
|
||||||
demo_url,
|
|
||||||
slides_url,
|
|
||||||
technologies,
|
|
||||||
submission_status,
|
|
||||||
submitted_at,
|
|
||||||
) in test_submissions {
|
|
||||||
let submission_id = format!("test-submission-{}-{}", hackathon_id, team_id);
|
|
||||||
|
|
||||||
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
|
|
||||||
.bind(("id", submission_id.clone()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let submission = HackathonSubmissionsSchema {
|
|
||||||
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
|
|
||||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
|
||||||
team_id: Some(Thing::from(("app_teams", team_id))),
|
|
||||||
project_name: Some(project_name.into()),
|
|
||||||
description: Some(description.into()),
|
|
||||||
repository_url,
|
|
||||||
upload_file_url: None,
|
|
||||||
demo_url,
|
|
||||||
slides_url,
|
|
||||||
technologies: Some(technologies),
|
|
||||||
contact_instagram: None,
|
|
||||||
contact_twitter: None,
|
|
||||||
contact_linkedin: None,
|
|
||||||
contact_facebook: None,
|
|
||||||
contact_youtube: None,
|
|
||||||
contact_tiktok: None,
|
|
||||||
contact_other: None,
|
|
||||||
submission_status: Some(submission_status),
|
|
||||||
judge_feedback: None,
|
|
||||||
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
|
|
||||||
is_deleted: false,
|
|
||||||
created_at: Some(get_iso_date()),
|
|
||||||
updated_at: Some(get_iso_date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
|
|
||||||
.content(submission)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted test hackathon submission: {project_name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("✅ All test submission data seeded");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
use imphnen_iam::UsersSchema;
|
#![allow(clippy::all)]
|
||||||
use imphnen_utils::{get_iso_date, hash_password};
|
|
||||||
|
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||||
|
use imphnen_entities::seaorm::auth::users::Entity as UserEntity;
|
||||||
|
use imphnen_libs::hash_password;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, IntoActiveModel};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env = &imphnen_libs::environment::ENV;
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
use surrealdb::engine::any;
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
let db = any::connect(&env.surrealdb_url).await?;
|
let db = &pg_conn.conn;
|
||||||
db.signin(Root {
|
|
||||||
username: &env.surrealdb_username,
|
|
||||||
password: &env.surrealdb_password,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
db.use_ns(env.surrealdb_namespace.clone())
|
|
||||||
.use_db(env.surrealdb_dbname.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let users = vec![
|
let users = vec![
|
||||||
(
|
(
|
||||||
@@ -37,71 +36,129 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"testuser1-id",
|
"665a3cfc-ea5f-4bcd-8769-4a6d8d1451d4",
|
||||||
"testuser1@example.com",
|
"testuser1@example.com",
|
||||||
"Test User 1",
|
"Test User 1",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"testuser2-id",
|
"3972c139-a450-416c-93b0-c42539dc780f",
|
||||||
"testuser2@example.com",
|
"testuser2@example.com",
|
||||||
"Test User 2",
|
"Test User 2",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"testuser3-id",
|
"b426c0a9-0efb-4e26-b078-4f18767255f3",
|
||||||
"testuser3@example.com",
|
"testuser3@example.com",
|
||||||
"Test User 3",
|
"Test User 3",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"11111111-1111-1111-1111-111111111111",
|
||||||
|
"user4@example.com",
|
||||||
|
"User Four",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"22222222-2222-2222-2222-222222222222",
|
||||||
|
"user5@example.com",
|
||||||
|
"User Five",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"33333333-3333-3333-3333-333333333333",
|
||||||
|
"mentor2@example.com",
|
||||||
|
"Mentor Two",
|
||||||
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"44444444-4444-4444-4444-444444444444",
|
||||||
|
"staff2@example.com",
|
||||||
|
"Staff Two",
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"55555555-5555-5555-5555-555555555555",
|
||||||
|
"user6@example.com",
|
||||||
|
"User Six",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"66666666-6666-6666-6666-666666666666",
|
||||||
|
"user7@example.com",
|
||||||
|
"User Seven",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"77777777-7777-7777-7777-777777777777",
|
||||||
|
"user8@example.com",
|
||||||
|
"User Eight",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"88888888-8888-8888-8888-888888888888",
|
||||||
|
"user9@example.com",
|
||||||
|
"User Nine",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"99999999-9999-9999-9999-999999999999",
|
||||||
|
"user10@example.com",
|
||||||
|
"User Ten",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (id, email, fullname, role_id) in users {
|
for (id, email, fullname, role_id_str) in users {
|
||||||
db.query("DELETE type::thing('app_users', $id)")
|
let role_uuid = Some(
|
||||||
.bind(("id", id))
|
Uuid::parse_str(role_id_str)
|
||||||
.await?;
|
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?,
|
||||||
|
);
|
||||||
|
|
||||||
let user = UsersSchema {
|
let uid = Uuid::parse_str(id)?;
|
||||||
id: Thing::from(("app_users", id)),
|
|
||||||
fullname: fullname.into(),
|
let names: Vec<&str> = fullname.split_whitespace().collect();
|
||||||
legal_name: Some(format!("{} Legal Name", fullname)),
|
let first_name = names.first().map(|s| s.to_string());
|
||||||
email: email.into(),
|
let last_name = if names.len() > 1 {
|
||||||
password: hash_password("password").unwrap(),
|
Some(names[1..].join(" "))
|
||||||
avatar: Some("https://example.com/avatar.jpg".into()),
|
} else {
|
||||||
phone_number: "081234567890".into(),
|
None
|
||||||
phone_for_verification: Some("081234567890".into()),
|
|
||||||
is_active: true,
|
|
||||||
is_deleted: false,
|
|
||||||
mentor_id: None,
|
|
||||||
gender: Some("male".into()),
|
|
||||||
birthdate: Some("1990-05-15".into()),
|
|
||||||
domicile: Some("Jakarta, Indonesia".into()),
|
|
||||||
// identity_document_url: None, // Sudah tidak dipakai, bisa dihapus dari schema jika tidak diperlukan
|
|
||||||
bio: Some(format!("{} adalah user dengan data pribadi lengkap untuk testing.", fullname)),
|
|
||||||
last_education: Some("S1 Teknik Informatika".into()),
|
|
||||||
linkedin_url: Some("https://linkedin.com/in/user".into()),
|
|
||||||
github_url: Some("https://github.com/user".into()),
|
|
||||||
cv_url: Some("https://example.com/cv.pdf".into()),
|
|
||||||
portfolio_url: Some("https://example.com/portfolio".into()),
|
|
||||||
website_url: Some("https://example.com/website".into()),
|
|
||||||
twitter_url: Some("https://twitter.com/user".into()),
|
|
||||||
location: Some("Jakarta, Indonesia".into()),
|
|
||||||
skills: Some(vec!["JavaScript".into(), "React".into(), "Node.js".into()]),
|
|
||||||
experience: None,
|
|
||||||
education: None,
|
|
||||||
career_status: Some("Senior Developer".into()),
|
|
||||||
role: Thing::from(("app_roles", role_id)),
|
|
||||||
created_at: get_iso_date(),
|
|
||||||
updated_at: get_iso_date(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
db.create::<Option<UsersSchema>>(("app_users", id))
|
let password = "password";
|
||||||
.content(user)
|
let hashed = hash_password(password).unwrap();
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("✅ Inserted user: {fullname} ({email})");
|
let existing_user = UserEntity::find_by_id(uid).one(db).await?;
|
||||||
|
let is_update = existing_user.is_some();
|
||||||
|
|
||||||
|
let mut user_model: UsersActiveModel = if let Some(existing) = existing_user {
|
||||||
|
println!("🔄 Updating user: {fullname} ({email})");
|
||||||
|
existing.into_active_model()
|
||||||
|
} else {
|
||||||
|
println!("✅ Inserting user: {fullname} ({email})");
|
||||||
|
let mut active: UsersActiveModel = Default::default();
|
||||||
|
active.id = Set(uid);
|
||||||
|
active.created_at = Set(Utc::now());
|
||||||
|
active
|
||||||
|
};
|
||||||
|
|
||||||
|
user_model.email = Set(email.to_string());
|
||||||
|
user_model.password_hash = Set(hashed);
|
||||||
|
user_model.username = Set(email.to_string());
|
||||||
|
user_model.first_name = Set(first_name);
|
||||||
|
user_model.last_name = Set(last_name);
|
||||||
|
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
||||||
|
user_model.is_verified = Set(true);
|
||||||
|
user_model.is_active = Set(true);
|
||||||
|
user_model.role_id = Set(role_uuid);
|
||||||
|
user_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
|
if is_update {
|
||||||
|
user_model.update(db).await?;
|
||||||
|
} else {
|
||||||
|
user_model.insert(db).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("✅ All Users seeded");
|
println!("✅ All Users seeded");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
||||||
println!("🔧 Seeding: {bin}");
|
println!("🔧 Seeding: {bin}");
|
||||||
let status = Command::new("cargo").args(["run", "--bin", bin]).status()?;
|
#[cfg(target_os = "windows")]
|
||||||
|
let status = Command::new(format!("./target/release/{}.exe", bin)).status()?;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let status = Command::new(format!("./target/release/{}", bin)).status()?;
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
Err(format!("❌ Failed to run seed: {bin}").into())
|
Err(format!("❌ Failed to run seed: {bin}").into())
|
||||||
@@ -20,7 +25,6 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
run_seed("seed_roles_permissions")?;
|
run_seed("seed_roles_permissions")?;
|
||||||
run_seed("seed_users")?;
|
run_seed("seed_users")?;
|
||||||
run_seed("seed_events")?;
|
run_seed("seed_events")?;
|
||||||
run_seed("seed_hackathons")?;
|
|
||||||
run_seed("seed_gacha_rolls")?;
|
run_seed("seed_gacha_rolls")?;
|
||||||
run_seed("seed_mentor_user")?;
|
run_seed("seed_mentor_user")?;
|
||||||
run_seed("seed_test_data")?;
|
run_seed("seed_test_data")?;
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::auth::roles::{
|
||||||
|
Entity as RolesEntity, Model as RoleModel,
|
||||||
|
};
|
||||||
|
use imphnen_entities::seaorm::auth::users::{
|
||||||
|
Entity as UsersEntity, Model as UserModel,
|
||||||
|
};
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection, PostgresError};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, DbErr, EntityTrait, PaginatorTrait, Set, TransactionTrait,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🚀 Starting PostgreSQL Connection Test");
|
||||||
|
println!("=====================================");
|
||||||
|
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
println!("✅ Configuration loaded successfully");
|
||||||
|
println!(
|
||||||
|
" Database URL: {}",
|
||||||
|
config
|
||||||
|
.database_url
|
||||||
|
.replace("postgres://", "postgres://****:****@")
|
||||||
|
);
|
||||||
|
println!(" Pool size: {}", config.pool_size);
|
||||||
|
println!(" Connect timeout: {}s", config.connect_timeout);
|
||||||
|
println!(" Retry attempts: {}", config.retry_attempts);
|
||||||
|
|
||||||
|
println!("\n🔌 Testing PostgreSQL connection...");
|
||||||
|
match test_connection(config).await {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("✅ All PostgreSQL tests passed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("❌ PostgreSQL test failed: {}", e);
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_connection(config: PostgresConfig) -> Result<(), PostgresError> {
|
||||||
|
println!(" Creating PostgreSQL connection...");
|
||||||
|
let postgres_conn = PostgresConnection::new(config).await?;
|
||||||
|
let connection = Arc::new(postgres_conn);
|
||||||
|
println!(" ✅ Connection established successfully");
|
||||||
|
|
||||||
|
println!(" Testing basic connectivity...");
|
||||||
|
test_basic_connectivity(&connection).await?;
|
||||||
|
println!(" ✅ Basic connectivity test passed");
|
||||||
|
|
||||||
|
println!(" Testing table existence...");
|
||||||
|
test_table_existence(&connection).await?;
|
||||||
|
println!(" ✅ Table existence test passed");
|
||||||
|
|
||||||
|
println!(" Testing CRUD operations...");
|
||||||
|
test_crud_operations(&connection).await?;
|
||||||
|
println!(" ✅ CRUD operations test passed");
|
||||||
|
|
||||||
|
println!(" Testing transaction support...");
|
||||||
|
test_transactions(&connection).await?;
|
||||||
|
println!(" ✅ Transaction support test passed");
|
||||||
|
|
||||||
|
println!(" Testing error handling...");
|
||||||
|
test_error_handling(&connection).await?;
|
||||||
|
println!(" ✅ Error handling test passed");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_basic_connectivity(
|
||||||
|
connection: &Arc<PostgresConnection>,
|
||||||
|
) -> Result<(), PostgresError> {
|
||||||
|
let statement = sea_orm::Statement::from_string(
|
||||||
|
connection.get_database_backend(),
|
||||||
|
"SELECT 1 as test_value, current_timestamp as current_time".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = connection.query_one(statement).await?.ok_or_else(|| {
|
||||||
|
PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
|
"No results returned".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let test_value: Option<i32> = result.try_get("", "test_value").ok();
|
||||||
|
let current_time: Option<String> = result.try_get("", "current_time").ok();
|
||||||
|
|
||||||
|
if test_value != Some(1) {
|
||||||
|
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
|
format!("Expected test_value=1, got {:?}", test_value),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_time.is_none() {
|
||||||
|
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
|
"Expected current_time to be set".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" 📝 Query result: test_value={:?}, current_time={:?}",
|
||||||
|
test_value, current_time
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_table_existence(
|
||||||
|
connection: &Arc<PostgresConnection>,
|
||||||
|
) -> Result<(), PostgresError> {
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
|
||||||
|
println!(" 📋 Checking users table...");
|
||||||
|
let user_count = UsersEntity::find()
|
||||||
|
.count(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
println!(
|
||||||
|
" 📊 Users table accessible, current count: {}",
|
||||||
|
user_count
|
||||||
|
);
|
||||||
|
|
||||||
|
println!(" 📋 Checking roles table...");
|
||||||
|
let role_count = RolesEntity::find()
|
||||||
|
.count(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
println!(
|
||||||
|
" 📊 Roles table accessible, current count: {}",
|
||||||
|
role_count
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_crud_operations(
|
||||||
|
connection: &Arc<PostgresConnection>,
|
||||||
|
) -> Result<(), PostgresError> {
|
||||||
|
use sea_orm::{ActiveModelTrait, Set};
|
||||||
|
|
||||||
|
println!(" ➕ Creating test user...");
|
||||||
|
let test_user_id = Uuid::new_v4();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||||
|
id: Set(test_user_id),
|
||||||
|
email: Set(format!("test_user_{}@example.com", test_user_id)),
|
||||||
|
password_hash: Set("test_password_hash".to_string()),
|
||||||
|
username: Set(format!("testuser_{}", test_user_id)),
|
||||||
|
first_name: Set(Some("Test".to_string())),
|
||||||
|
last_name: Set(Some("User".to_string())),
|
||||||
|
avatar_url: Set(None),
|
||||||
|
is_verified: Set(false),
|
||||||
|
is_active: Set(true),
|
||||||
|
metadata: Set(None),
|
||||||
|
role_id: Set(None),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
|
deleted_at: Set(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let created_user = user_model
|
||||||
|
.insert(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
|
||||||
|
println!(" ✅ Created user with ID: {}", created_user.id);
|
||||||
|
|
||||||
|
println!(" 🔍 Reading test user...");
|
||||||
|
let found_user = UsersEntity::find_by_id(test_user_id)
|
||||||
|
.one(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
|
"User not found after creation".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" ✅ Found user: {} ({})",
|
||||||
|
found_user.username, found_user.email
|
||||||
|
);
|
||||||
|
|
||||||
|
println!(" ✏️ Updating test user...");
|
||||||
|
let mut update_model: imphnen_entities::seaorm::auth::users::ActiveModel =
|
||||||
|
found_user.into();
|
||||||
|
update_model.first_name = Set(Some("Updated".to_string()));
|
||||||
|
update_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
|
let updated_user = update_model
|
||||||
|
.update(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" ✅ Updated user first name to: {:?}",
|
||||||
|
updated_user.first_name
|
||||||
|
);
|
||||||
|
|
||||||
|
println!(" 🗑️ Deleting test user...");
|
||||||
|
UsersEntity::delete_by_id(updated_user.id)
|
||||||
|
.exec(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
|
|
||||||
|
println!(" ✅ Test user deleted successfully");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_transactions(
|
||||||
|
connection: &Arc<PostgresConnection>,
|
||||||
|
) -> Result<(), PostgresError> {
|
||||||
|
println!(" 💰 Testing transaction support...");
|
||||||
|
|
||||||
|
let transaction_result = connection
|
||||||
|
.conn
|
||||||
|
.transaction(|txn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let test_user_id = Uuid::new_v4();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||||
|
id: Set(test_user_id),
|
||||||
|
email: Set(format!("transaction_test_{}@example.com", test_user_id)),
|
||||||
|
password_hash: Set("transaction_password_hash".to_string()),
|
||||||
|
username: Set(format!("transaction_user_{}", test_user_id)),
|
||||||
|
first_name: Set(Some("Transaction".to_string())),
|
||||||
|
last_name: Set(Some("Test".to_string())),
|
||||||
|
avatar_url: Set(None),
|
||||||
|
is_verified: Set(false),
|
||||||
|
is_active: Set(true),
|
||||||
|
metadata: Set(None),
|
||||||
|
role_id: Set(None),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
|
deleted_at: Set(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _created_user = user_model.insert(txn).await?;
|
||||||
|
|
||||||
|
Err::<(), DbErr>(DbErr::Custom("Simulated transaction failure".to_string()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match transaction_result {
|
||||||
|
Err(e) => {
|
||||||
|
let e_text = format!("{:?}", e);
|
||||||
|
if e_text.contains("Simulated transaction failure") {
|
||||||
|
println!(" ✅ Transaction failed as expected, rollback successful");
|
||||||
|
} else {
|
||||||
|
return Err(PostgresError::OperationFailed(format!(
|
||||||
|
"Unexpected transaction result: {}",
|
||||||
|
e_text
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
return Err(PostgresError::OperationFailed(
|
||||||
|
"Unexpected transaction result: transaction unexpectedly succeeded"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_exists = UsersEntity::find_by_id(Uuid::nil())
|
||||||
|
.one(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
if user_exists {
|
||||||
|
println!(" ⚠️ User found despite rollback - this might indicate an issue");
|
||||||
|
} else {
|
||||||
|
println!(" ✅ Transaction rollback verified - user not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_error_handling(
|
||||||
|
connection: &Arc<PostgresConnection>,
|
||||||
|
) -> Result<(), PostgresError> {
|
||||||
|
println!(" ⚠️ Testing error handling...");
|
||||||
|
|
||||||
|
println!(" 🔍 Testing invalid UUID handling...");
|
||||||
|
let invalid_uuid = Uuid::nil();
|
||||||
|
|
||||||
|
match UsersEntity::find_by_id(invalid_uuid)
|
||||||
|
.one(&connection.conn)
|
||||||
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?
|
||||||
|
{
|
||||||
|
Some(_) => {
|
||||||
|
println!(" ✅ Found user with nil UUID (expected in some cases)")
|
||||||
|
}
|
||||||
|
None => println!(" ✅ No user found with nil UUID (expected)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" 🔍 Testing invalid query handling...");
|
||||||
|
let invalid_statement = sea_orm::Statement::from_string(
|
||||||
|
connection.get_database_backend(),
|
||||||
|
"SELECT * FROM non_existent_table".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match connection.execute(invalid_statement).await {
|
||||||
|
Err(_) => println!(" ✅ Invalid query properly handled with error"),
|
||||||
|
Ok(_) => println!(" ⚠️ Invalid query unexpectedly succeeded"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod test_utils {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn create_test_config() -> PostgresConfig {
|
||||||
|
PostgresConfig {
|
||||||
|
database_url: "postgres://postgres:postgres@localhost:5432/imphnen_test"
|
||||||
|
.to_string(),
|
||||||
|
pool_size: 5,
|
||||||
|
connect_timeout: 10,
|
||||||
|
idle_timeout: 30,
|
||||||
|
max_lifetime: Some(600),
|
||||||
|
retry_attempts: 2,
|
||||||
|
retry_delay: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_test_user_model() -> UserModel {
|
||||||
|
UserModel {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
||||||
|
password_hash: "test_password_hash".to_string(),
|
||||||
|
username: format!("testuser_{}", Uuid::new_v4()),
|
||||||
|
first_name: Some("Test".to_string()),
|
||||||
|
last_name: Some("User".to_string()),
|
||||||
|
avatar_url: None,
|
||||||
|
is_verified: false,
|
||||||
|
is_active: true,
|
||||||
|
metadata: None,
|
||||||
|
role_id: None,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
deleted_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_test_role_model() -> RoleModel {
|
||||||
|
RoleModel {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: format!("test_role_{}", Uuid::new_v4()),
|
||||||
|
description: "Test role description".to_string(),
|
||||||
|
permissions: Some(serde_json::json!(["test.permission"])),
|
||||||
|
is_system_role: false,
|
||||||
|
is_default: false,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
deleted_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_test_config() {
|
||||||
|
let config = test_utils::create_test_config();
|
||||||
|
assert_eq!(config.pool_size, 5);
|
||||||
|
assert_eq!(config.connect_timeout, 10);
|
||||||
|
assert!(config.database_url.contains("imphnen_test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_test_user_model() {
|
||||||
|
let user = test_utils::create_test_user_model();
|
||||||
|
assert!(!user.email.is_empty());
|
||||||
|
assert!(!user.username.is_empty());
|
||||||
|
assert!(user.is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_test_role_model() {
|
||||||
|
let role = test_utils::create_test_role_model();
|
||||||
|
assert!(!role.name.is_empty());
|
||||||
|
assert!(role.permissions.is_some());
|
||||||
|
assert!(!role.is_system_role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ use imphnen_libs::axum_init;
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
let _ =
|
||||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
axum_init(|postgres_conn| async { gateway_service(postgres_conn).await }).await;
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-3
@@ -1,9 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "imphnen-cms"
|
name = "imphnen-cms"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait.workspace = true
|
||||||
imphnen-iam.workspace = true
|
imphnen-iam.workspace = true
|
||||||
imphnen-libs.workspace = true
|
imphnen-libs.workspace = true
|
||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
@@ -14,9 +15,9 @@ serde_json.workspace = true
|
|||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
validator.workspace = true
|
zod-rs.workspace = true
|
||||||
|
zod-rs-util.workspace = true
|
||||||
axum-test.workspace = true
|
axum-test.workspace = true
|
||||||
surrealdb.workspace = true
|
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
@@ -25,6 +26,16 @@ tower-http.workspace = true
|
|||||||
utoipa-swagger-ui.workspace = true
|
utoipa-swagger-ui.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
sea-orm.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
|
||||||
|
paginator-rs.workspace = true
|
||||||
|
paginator-utils.workspace = true
|
||||||
|
paginator-sea-orm.workspace = true
|
||||||
|
paginator-axum.workspace = true
|
||||||
|
sqlx.workspace = true
|
||||||
|
image.workspace = true
|
||||||
|
qrcode.workspace = true
|
||||||
|
|
||||||
[package.metadata.validator.regex]
|
[package.metadata.validator.regex]
|
||||||
VALID_URL_REGEX = "^https?://"
|
VALID_URL_REGEX = "^https?://"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use crate::events::domain::{EventEntity, EventRepository, EventService};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct EventServiceImpl {
|
||||||
|
repo: Arc<dyn EventRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn EventRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventService for EventServiceImpl {
|
||||||
|
async fn list(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||||
|
self.repo.find_all(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||||
|
self.repo.find_by_id(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
self.repo.create(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
self.repo.update(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.delete(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod event_service;
|
||||||
|
|
||||||
|
pub use event_service::EventServiceImpl;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct EventEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod event;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use event::EventEntity;
|
||||||
|
pub use repository::EventRepository;
|
||||||
|
pub use service::EventService;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use super::event::EventEntity;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait EventRepository: Send + Sync {
|
||||||
|
async fn find_all(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use super::event::EventEntity;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait EventService: Send + Sync {
|
||||||
|
async fn list(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||||
|
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
use crate::events::domain::event::EventEntity;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use imphnen_libs::ZodValidate;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsCreateRequestDto {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub is_online: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for EventsCreateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EventsCreateRequestDto> for EventEntity {
|
||||||
|
fn from(dto: EventsCreateRequestDto) -> Self {
|
||||||
|
EventEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description,
|
||||||
|
detail_link: dto.detail_link,
|
||||||
|
price: dto.price,
|
||||||
|
is_online: dto.is_online,
|
||||||
|
is_deleted: false,
|
||||||
|
location: dto.location,
|
||||||
|
start_date: dto.start_date,
|
||||||
|
end_date: dto.end_date,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsUpdateRequestDto {
|
||||||
|
pub name: String,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for EventsUpdateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub start_date: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EventEntity> for EventsListItemDto {
|
||||||
|
fn from(e: EventEntity) -> Self {
|
||||||
|
EventsListItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
name: e.name,
|
||||||
|
description: e.description,
|
||||||
|
detail_link: e.detail_link,
|
||||||
|
price: e.price,
|
||||||
|
is_online: e.is_online,
|
||||||
|
start_date: e.start_date.to_rfc3339(),
|
||||||
|
end_date: e.end_date.to_rfc3339(),
|
||||||
|
created_at: e.created_at.to_rfc3339(),
|
||||||
|
location: e.location,
|
||||||
|
is_deleted: e.is_deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub start_date: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EventEntity> for EventsDetailItemDto {
|
||||||
|
fn from(e: EventEntity) -> Self {
|
||||||
|
EventsDetailItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
name: e.name,
|
||||||
|
description: e.description,
|
||||||
|
detail_link: e.detail_link,
|
||||||
|
price: e.price,
|
||||||
|
is_online: e.is_online,
|
||||||
|
start_date: e.start_date.to_rfc3339(),
|
||||||
|
end_date: e.end_date.to_rfc3339(),
|
||||||
|
created_at: e.created_at.to_rfc3339(),
|
||||||
|
updated_at: e.updated_at.to_rfc3339(),
|
||||||
|
location: e.location,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
use super::dto::{
|
||||||
|
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||||
|
EventsUpdateRequestDto,
|
||||||
|
};
|
||||||
|
use crate::events::domain::EventService;
|
||||||
|
use axum::{
|
||||||
|
Extension,
|
||||||
|
extract::Path,
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_utils::{ApiMessage, ApiPaginated, ApiSuccess};
|
||||||
|
use paginator_axum::PaginationQuery;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/landing/cms/events",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get event list")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn get_event_list(
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
PaginationQuery(params): PaginationQuery,
|
||||||
|
) -> Response {
|
||||||
|
match service.list(params).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let mapped = PaginatorResponse {
|
||||||
|
data: result
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(EventsListItemDto::from)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
meta: result.meta,
|
||||||
|
};
|
||||||
|
ApiPaginated(mapped).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string())
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/landing/cms/events/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn get_event_by_id(
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
let uuid = match Uuid::parse_str(&id) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
return ApiMessage::new(
|
||||||
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
|
format!("Invalid UUID: {e}"),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match service.get(uuid).await {
|
||||||
|
Ok(event) => ApiSuccess(EventsDetailItemDto::from(event)).into_response(),
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string())
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/landing/cms/events/create",
|
||||||
|
request_body = EventsCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "[ADMIN] Create new event")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_event(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let entity = payload.into();
|
||||||
|
service.create(entity).await?;
|
||||||
|
Ok(ApiMessage::created("Event created"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/landing/cms/events/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
request_body = EventsUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Update event")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn patch_update_event(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
let existing = service.get(uuid).await?;
|
||||||
|
let entity = crate::events::domain::EventEntity {
|
||||||
|
id: existing.id,
|
||||||
|
name: payload.name,
|
||||||
|
description: payload.description,
|
||||||
|
detail_link: payload.detail_link,
|
||||||
|
price: payload.price,
|
||||||
|
is_online: payload.is_online,
|
||||||
|
location: payload.location,
|
||||||
|
start_date: payload.start_date,
|
||||||
|
end_date: payload.end_date,
|
||||||
|
is_deleted: existing.is_deleted,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
service.update(entity).await?;
|
||||||
|
Ok(ApiMessage::ok("Event updated"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/landing/cms/events/delete/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Soft delete event")
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn delete_event(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
service.delete(uuid).await?;
|
||||||
|
Ok(ApiMessage::ok("Event deleted"))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::{events_protected_routes, events_public_routes};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use super::handlers::{
|
||||||
|
delete_event, get_event_by_id, get_event_list, patch_update_event,
|
||||||
|
post_create_event,
|
||||||
|
};
|
||||||
|
use crate::events::application::EventServiceImpl;
|
||||||
|
use crate::events::domain::EventService;
|
||||||
|
use crate::events::infrastructure::persistence::PostgresEventRepository;
|
||||||
|
use axum::{
|
||||||
|
Extension, Router,
|
||||||
|
routing::{delete, get, patch, post},
|
||||||
|
};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn build_service(db: DatabaseConnection) -> Arc<dyn EventService> {
|
||||||
|
let repo = Arc::new(PostgresEventRepository::new(db));
|
||||||
|
Arc::new(EventServiceImpl::new(repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events_public_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/events", get(get_event_list))
|
||||||
|
.route("/events/detail/{id}", get(get_event_by_id))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events_protected_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/events/create", post(post_create_event))
|
||||||
|
.route("/events/update/{id}", patch(patch_update_event))
|
||||||
|
.route("/events/delete/{id}", delete(delete_event))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod postgres_event_repository;
|
||||||
|
|
||||||
|
pub use postgres_event_repository::PostgresEventRepository;
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
use crate::events::domain::{event::EventEntity, repository::EventRepository};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_entities::seaorm::common::events::{
|
||||||
|
ActiveModel as EventsActiveModel, Column as EventsColumn, Entity as EventsEntity,
|
||||||
|
Model as EventsModel,
|
||||||
|
};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, Order, PaginatorTrait, QueryOrder};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn to_entity(model: EventsModel) -> EventEntity {
|
||||||
|
EventEntity {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name,
|
||||||
|
description: model.description,
|
||||||
|
detail_link: model.detail_link,
|
||||||
|
price: model.price,
|
||||||
|
is_online: model.is_online,
|
||||||
|
is_deleted: model.is_deleted,
|
||||||
|
location: model.location,
|
||||||
|
start_date: model.start_date,
|
||||||
|
end_date: model.end_date,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresEventRepository {
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresEventRepository {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db: Arc::new(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventRepository for PostgresEventRepository {
|
||||||
|
async fn find_all(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
|
let mut query = EventsEntity::find().filter(EventsColumn::IsDeleted.eq(false));
|
||||||
|
|
||||||
|
if let Some(ref search) = params.search {
|
||||||
|
query = query.filter(EventsColumn::Name.contains(&search.query));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = match params.sort_by.as_deref() {
|
||||||
|
Some("name") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Desc) => query.order_by(EventsColumn::Name, Order::Desc),
|
||||||
|
_ => query.order_by(EventsColumn::Name, Order::Asc),
|
||||||
|
},
|
||||||
|
_ => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => {
|
||||||
|
query.order_by(EventsColumn::CreatedAt, Order::Asc)
|
||||||
|
}
|
||||||
|
_ => query.order_by(EventsColumn::CreatedAt, Order::Desc),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
|
let total = paginator
|
||||||
|
.num_items()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let events = paginator
|
||||||
|
.fetch_page((page - 1) as u64)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let data = events.into_iter().map(to_entity).collect();
|
||||||
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
|
Ok(PaginatorResponse { data, meta })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||||
|
let event = EventsEntity::find_by_id(id)
|
||||||
|
.filter(EventsColumn::IsDeleted.eq(false))
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(to_entity(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
let active_model = EventsActiveModel {
|
||||||
|
id: ActiveValue::Set(entity.id),
|
||||||
|
name: ActiveValue::Set(entity.name),
|
||||||
|
description: ActiveValue::Set(entity.description),
|
||||||
|
detail_link: ActiveValue::Set(entity.detail_link),
|
||||||
|
price: ActiveValue::Set(entity.price),
|
||||||
|
is_online: ActiveValue::Set(entity.is_online),
|
||||||
|
is_deleted: ActiveValue::Set(false),
|
||||||
|
location: ActiveValue::Set(entity.location),
|
||||||
|
start_date: ActiveValue::Set(entity.start_date),
|
||||||
|
end_date: ActiveValue::Set(entity.end_date),
|
||||||
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
EventsEntity::insert(active_model)
|
||||||
|
.exec(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
|
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(entity.id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.name = ActiveValue::Set(entity.name);
|
||||||
|
active_model.description = ActiveValue::Set(entity.description);
|
||||||
|
active_model.detail_link = ActiveValue::Set(entity.detail_link);
|
||||||
|
active_model.price = ActiveValue::Set(entity.price);
|
||||||
|
active_model.is_online = ActiveValue::Set(entity.is_online);
|
||||||
|
active_model.location = ActiveValue::Set(entity.location);
|
||||||
|
active_model.start_date = ActiveValue::Set(entity.start_date);
|
||||||
|
active_model.end_date = ActiveValue::Set(entity.end_date);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
|
||||||
|
pub use infrastructure::http::{events_protected_routes, events_public_routes};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
pub mod v1;
|
pub mod events;
|
||||||
|
pub mod roadmap;
|
||||||
|
pub mod testimonials;
|
||||||
|
pub mod qr;
|
||||||
|
|
||||||
pub use v1::landing;
|
pub use events::{events_protected_routes, events_public_routes};
|
||||||
pub use v1::landing::events;
|
pub use roadmap::{roadmap_protected_routes, roadmap_public_routes};
|
||||||
pub use v1::landing::testimonials;
|
pub use testimonials::{testimonials_protected_routes, testimonials_public_routes};
|
||||||
pub use v1::landing::events::events_public_routes;
|
pub use qr::qr_router;
|
||||||
pub use v1::landing::events::events_protected_routes;
|
|
||||||
pub use v1::landing::testimonials::testimonials_public_routes;
|
|
||||||
pub use v1::landing::testimonials::testimonials_protected_routes;
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use image::{DynamicImage, GenericImageView, ImageFormat, imageops};
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use qrcode::QrCode;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::qr::campaigns::domain::{
|
||||||
|
entity::{CampaignEntity, CreateCampaignInput},
|
||||||
|
repository::CampaignRepository,
|
||||||
|
service::QrCampaignService,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct QrCampaignServiceImpl {
|
||||||
|
repo: Arc<dyn CampaignRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QrCampaignServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn CampaignRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl QrCampaignService for QrCampaignServiceImpl {
|
||||||
|
async fn create(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
created_by: Uuid,
|
||||||
|
) -> Result<CampaignEntity, AppError> {
|
||||||
|
let qr = QrCode::new(url.as_bytes())
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let qr_img = qr
|
||||||
|
.render::<image::Luma<u8>>()
|
||||||
|
.min_dimensions(256, 256)
|
||||||
|
.build();
|
||||||
|
let mut qr_bytes = Vec::new();
|
||||||
|
DynamicImage::ImageLuma8(qr_img)
|
||||||
|
.write_to(&mut Cursor::new(&mut qr_bytes), ImageFormat::Png)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let input = CreateCampaignInput {
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
created_by,
|
||||||
|
qr_code_data: qr_bytes,
|
||||||
|
};
|
||||||
|
self.repo.create(input).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_all(&self) -> Result<Vec<CampaignEntity>, AppError> {
|
||||||
|
self.repo.find_all().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError> {
|
||||||
|
self.repo.find_active_qr_data().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError> {
|
||||||
|
self.repo.set_active(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.delete(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_image(&self, image_bytes: Vec<u8>) -> Result<Vec<u8>, AppError> {
|
||||||
|
let qr_data = self
|
||||||
|
.repo
|
||||||
|
.find_active_qr_data()
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("No active campaign".to_string()))?;
|
||||||
|
|
||||||
|
let img = image::load_from_memory(&image_bytes)
|
||||||
|
.map_err(|_| AppError::BadRequestError("Invalid image format".to_string()))?;
|
||||||
|
|
||||||
|
let qr_img = image::load_from_memory(&qr_data).map_err(|_| {
|
||||||
|
AppError::InternalServerError("Failed to load QR data".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (w, h) = img.dimensions();
|
||||||
|
let qr_size = (std::cmp::min(w, h) / 5).max(100);
|
||||||
|
|
||||||
|
let qr_resized =
|
||||||
|
qr_img.resize_exact(qr_size, qr_size, imageops::FilterType::Nearest);
|
||||||
|
|
||||||
|
let mut output = img.to_rgba8();
|
||||||
|
let x = (w - qr_size - 10) as i64;
|
||||||
|
let y = (h - qr_size - 10) as i64;
|
||||||
|
imageops::overlay(&mut output, &qr_resized.to_rgba8(), x, y);
|
||||||
|
|
||||||
|
let mut out_bytes = Vec::new();
|
||||||
|
DynamicImage::ImageRgba8(output)
|
||||||
|
.write_to(&mut Cursor::new(&mut out_bytes), ImageFormat::Png)
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(out_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod campaign_service;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct CampaignEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub created_by: Uuid,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateCampaignInput {
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub created_by: Uuid,
|
||||||
|
pub qr_code_data: Vec<u8>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod entity;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::entity::{CampaignEntity, CreateCampaignInput};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait CampaignRepository: Send + Sync {
|
||||||
|
async fn create(
|
||||||
|
&self,
|
||||||
|
input: CreateCampaignInput,
|
||||||
|
) -> Result<CampaignEntity, AppError>;
|
||||||
|
async fn find_all(&self) -> Result<Vec<CampaignEntity>, AppError>;
|
||||||
|
async fn find_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError>;
|
||||||
|
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::entity::CampaignEntity;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait QrCampaignService: Send + Sync {
|
||||||
|
async fn create(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
created_by: Uuid,
|
||||||
|
) -> Result<CampaignEntity, AppError>;
|
||||||
|
async fn list_all(&self) -> Result<Vec<CampaignEntity>, AppError>;
|
||||||
|
async fn get_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError>;
|
||||||
|
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
async fn process_image(&self, image_bytes: Vec<u8>) -> Result<Vec<u8>, AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
pub struct CreateCampaignRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct CampaignResponse {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub created_by: Uuid,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
use axum::{
|
||||||
|
Extension, Json,
|
||||||
|
extract::{Multipart, Path},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use imphnen_utils::{errors::AppError, response_format::ApiSuccess};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::qr::{
|
||||||
|
campaigns::{
|
||||||
|
domain::service::QrCampaignService,
|
||||||
|
infrastructure::http::dto::CreateCampaignRequest,
|
||||||
|
},
|
||||||
|
middleware::qr_auth::QrAuthUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/qr/campaigns",
|
||||||
|
request_body = CreateCampaignRequest,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create a QR campaign",
|
||||||
|
example = json!({
|
||||||
|
"data": {
|
||||||
|
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
|
||||||
|
"name": "Imphnen Hackathon 2025",
|
||||||
|
"url": "https://imphnen.dev/register",
|
||||||
|
"is_active": false,
|
||||||
|
"created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"expires_at": "2025-12-31T23:59:59Z",
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
"version": "0.3.0"
|
||||||
|
})),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - admin only")
|
||||||
|
),
|
||||||
|
tag = "QR - Campaigns",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn create_campaign_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
Json(body): Json<CreateCampaignRequest>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
if auth_user.role != "admin" {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Admin access required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let campaign = service
|
||||||
|
.create(body.name, body.url, auth_user.user_id)
|
||||||
|
.await?;
|
||||||
|
Ok(imphnen_utils::response_format::ApiCreated(campaign).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/qr/campaigns",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin: list all QR campaigns",
|
||||||
|
example = json!({
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
|
||||||
|
"name": "Imphnen Hackathon 2025",
|
||||||
|
"url": "https://imphnen.dev/register",
|
||||||
|
"is_active": true,
|
||||||
|
"created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"expires_at": "2025-12-31T23:59:59Z",
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-05T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": "0.3.0"
|
||||||
|
})),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - admin only")
|
||||||
|
),
|
||||||
|
tag = "QR - Campaigns",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn list_campaigns_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
if auth_user.role != "admin" {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Admin access required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let campaigns = service.list_all().await?;
|
||||||
|
Ok(ApiSuccess(campaigns).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/qr/campaigns/{id}/activate",
|
||||||
|
params(("id" = Uuid, Path, description = "Campaign ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin: activate a campaign (deactivates all others)",
|
||||||
|
example = json!({
|
||||||
|
"data": {
|
||||||
|
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
|
||||||
|
"name": "Imphnen Hackathon 2025",
|
||||||
|
"url": "https://imphnen.dev/register",
|
||||||
|
"is_active": true,
|
||||||
|
"created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"expires_at": "2025-12-31T23:59:59Z",
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-05T10:00:00Z"
|
||||||
|
},
|
||||||
|
"version": "0.3.0"
|
||||||
|
})),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - admin only")
|
||||||
|
),
|
||||||
|
tag = "QR - Campaigns",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn activate_campaign_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
if auth_user.role != "admin" {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Admin access required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let campaign = service.set_active(id).await?;
|
||||||
|
Ok(ApiSuccess(campaign).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/qr/campaigns/{id}",
|
||||||
|
params(("id" = Uuid, Path, description = "Campaign ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin: delete a campaign",
|
||||||
|
example = json!({"message": "Campaign deleted successfully", "version": "0.3.0"})),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - admin only")
|
||||||
|
),
|
||||||
|
tag = "QR - Campaigns",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn delete_campaign_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
if auth_user.role != "admin" {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Admin access required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
service.delete(id).await?;
|
||||||
|
Ok(
|
||||||
|
imphnen_utils::response_format::ApiMessage::ok("Campaign deleted successfully")
|
||||||
|
.into_response(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/qr/campaigns/process-image",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Process QR code image — send multipart/form-data with field 'file'. Returns PNG image bytes.",
|
||||||
|
content_type = "image/png"),
|
||||||
|
(status = 400, description = "No file provided or invalid image"),
|
||||||
|
(status = 401, description = "Unauthorized")
|
||||||
|
),
|
||||||
|
tag = "QR - Campaigns",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn process_image_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||||
|
Extension(_auth_user): Extension<QrAuthUser>,
|
||||||
|
mut multipart: Multipart,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let mut image_bytes = Vec::new();
|
||||||
|
while let Some(field) = multipart
|
||||||
|
.next_field()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::BadRequestError(e.to_string()))?
|
||||||
|
{
|
||||||
|
if field.name() == Some("file") {
|
||||||
|
image_bytes = field
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::BadRequestError(e.to_string()))?
|
||||||
|
.to_vec();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if image_bytes.is_empty() {
|
||||||
|
return Err(AppError::BadRequestError("No file provided".to_string()));
|
||||||
|
}
|
||||||
|
let png_bytes = service.process_image(image_bytes).await?;
|
||||||
|
Ok(([(axum::http::header::CONTENT_TYPE, "image/png")], png_bytes).into_response())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use axum::{
|
||||||
|
Extension, Router,
|
||||||
|
middleware::from_fn,
|
||||||
|
routing::{delete, post, put},
|
||||||
|
};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::qr::{
|
||||||
|
campaigns::{
|
||||||
|
application::campaign_service::QrCampaignServiceImpl,
|
||||||
|
domain::{repository::CampaignRepository, service::QrCampaignService},
|
||||||
|
infrastructure::{
|
||||||
|
http::handlers::{
|
||||||
|
activate_campaign_handler, create_campaign_handler, delete_campaign_handler,
|
||||||
|
list_campaigns_handler, process_image_handler,
|
||||||
|
},
|
||||||
|
persistence::postgres_campaign_repository::PostgresCampaignRepository,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
middleware::qr_auth::qr_auth_middleware,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn qr_campaigns_routes(pool: Arc<PgPool>) -> Router {
|
||||||
|
let repo: Arc<dyn CampaignRepository> =
|
||||||
|
Arc::new(PostgresCampaignRepository::new(pool.clone()));
|
||||||
|
let service: Arc<dyn QrCampaignService> =
|
||||||
|
Arc::new(QrCampaignServiceImpl::new(repo));
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/campaigns",
|
||||||
|
post(create_campaign_handler).get(list_campaigns_handler),
|
||||||
|
)
|
||||||
|
.route("/campaigns/{id}/activate", put(activate_campaign_handler))
|
||||||
|
.route("/campaigns/{id}", delete(delete_campaign_handler))
|
||||||
|
.route("/campaigns/process-image", post(process_image_handler))
|
||||||
|
.layer(Extension(service))
|
||||||
|
.layer(from_fn(qr_auth_middleware))
|
||||||
|
.layer(Extension(pool))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod postgres_campaign_repository;
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::qr::campaigns::domain::{
|
||||||
|
entity::{CampaignEntity, CreateCampaignInput},
|
||||||
|
repository::CampaignRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(FromRow)]
|
||||||
|
struct CampaignRow {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub created_by: Uuid,
|
||||||
|
pub expires_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CampaignRow> for CampaignEntity {
|
||||||
|
fn from(row: CampaignRow) -> Self {
|
||||||
|
CampaignEntity {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
url: row.url,
|
||||||
|
is_active: row.is_active,
|
||||||
|
created_by: row.created_by,
|
||||||
|
expires_at: row.expires_at,
|
||||||
|
created_at: row.created_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresCampaignRepository {
|
||||||
|
pool: Arc<PgPool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresCampaignRepository {
|
||||||
|
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CampaignRepository for PostgresCampaignRepository {
|
||||||
|
async fn create(
|
||||||
|
&self,
|
||||||
|
input: CreateCampaignInput,
|
||||||
|
) -> Result<CampaignEntity, AppError> {
|
||||||
|
let mut tx = self
|
||||||
|
.pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query("UPDATE qr_campaigns SET is_active = false, updated_at = NOW()")
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let campaign = sqlx::query_as::<_, CampaignRow>(
|
||||||
|
"INSERT INTO qr_campaigns (id, name, url, qr_code_data, is_active, created_by, expires_at) \
|
||||||
|
VALUES ($1, $2, $3, $4, true, $5, NOW() + INTERVAL '30 days') \
|
||||||
|
RETURNING id, name, url, is_active, created_by, expires_at, created_at, updated_at",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.bind(&input.name)
|
||||||
|
.bind(&input.url)
|
||||||
|
.bind(&input.qr_code_data)
|
||||||
|
.bind(input.created_by)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
tx.commit()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(campaign.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_all(&self) -> Result<Vec<CampaignEntity>, AppError> {
|
||||||
|
sqlx::query_as::<_, CampaignRow>(
|
||||||
|
"SELECT id, name, url, is_active, created_by, expires_at, created_at, updated_at \
|
||||||
|
FROM qr_campaigns ORDER BY created_at DESC",
|
||||||
|
)
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||||
|
.map(|rows| rows.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError> {
|
||||||
|
let row = sqlx::query_as::<_, (Vec<u8>,)>(
|
||||||
|
"SELECT qr_code_data FROM qr_campaigns WHERE is_active = true LIMIT 1",
|
||||||
|
)
|
||||||
|
.fetch_optional(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(row.map(|r| r.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError> {
|
||||||
|
let mut tx = self
|
||||||
|
.pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query("UPDATE qr_campaigns SET is_active = false, updated_at = NOW()")
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let campaign = sqlx::query_as::<_, CampaignRow>(
|
||||||
|
"UPDATE qr_campaigns SET is_active = true, updated_at = NOW() WHERE id = $1 \
|
||||||
|
RETURNING id, name, url, is_active, created_by, expires_at, created_at, updated_at",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
tx.commit()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(campaign.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
sqlx::query("DELETE FROM qr_campaigns WHERE id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
pub use infrastructure::http::routes::qr_campaigns_routes;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod qr_auth;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::Request,
|
||||||
|
middleware::Next,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use imphnen_libs::decode_access_token;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct QrAuthUser {
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn qr_auth_middleware(
|
||||||
|
axum::Extension(pool): axum::Extension<Arc<PgPool>>,
|
||||||
|
mut request: Request<Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, Response> {
|
||||||
|
let auth_header = request
|
||||||
|
.headers()
|
||||||
|
.get("Authorization")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
(StatusCode::UNAUTHORIZED, "Missing Authorization header").into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
|
||||||
|
(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid Authorization header format",
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let token_data = decode_access_token(token).map_err(|_| {
|
||||||
|
(StatusCode::UNAUTHORIZED, "Invalid or expired token").into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let user_id = Uuid::parse_str(&token_data.claims.user_id).map_err(|_| {
|
||||||
|
(StatusCode::UNAUTHORIZED, "Invalid user ID in token").into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let _ = sqlx::query(
|
||||||
|
"INSERT INTO qr_users (id, email, name, role, provider) VALUES ($1, $2, $2, 'user', 'external') ON CONFLICT (id) DO NOTHING"
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(&token_data.claims.sub)
|
||||||
|
.execute(pool.as_ref())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let role: String = sqlx::query_scalar("SELECT role FROM qr_users WHERE id = $1")
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_optional(pool.as_ref())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| "user".to_string());
|
||||||
|
|
||||||
|
request
|
||||||
|
.extensions_mut()
|
||||||
|
.insert(QrAuthUser { user_id, role });
|
||||||
|
Ok(next.run(request).await)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pub mod campaigns;
|
||||||
|
pub mod middleware;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn qr_router(db: DatabaseConnection) -> Router {
|
||||||
|
let pool: Arc<PgPool> = Arc::new(db.get_postgres_connection_pool().clone());
|
||||||
|
Router::new()
|
||||||
|
.merge(users::infrastructure::http::routes::qr_users_routes(pool.clone()))
|
||||||
|
.merge(campaigns::infrastructure::http::routes::qr_campaigns_routes(pool))
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod user_service;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::qr::users::domain::{
|
||||||
|
entity::{UpdateUserInput, UserEntity},
|
||||||
|
repository::UserRepository,
|
||||||
|
service::QrUserService,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct QrUserServiceImpl {
|
||||||
|
repo: Arc<dyn UserRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QrUserServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn UserRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl QrUserService for QrUserServiceImpl {
|
||||||
|
async fn get_profile(&self, user_id: Uuid) -> Result<UserEntity, AppError> {
|
||||||
|
self
|
||||||
|
.repo
|
||||||
|
.find_by_id(user_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("User not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_profile(
|
||||||
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
input: UpdateUserInput,
|
||||||
|
) -> Result<UserEntity, AppError> {
|
||||||
|
if let Some(ref email) = input.email
|
||||||
|
&& email.trim().is_empty()
|
||||||
|
{
|
||||||
|
return Err(AppError::ValidationError(
|
||||||
|
"Email cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.repo.update(user_id, input).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_all(&self) -> Result<Vec<UserEntity>, AppError> {
|
||||||
|
self.repo.find_all().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_role(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
role: String,
|
||||||
|
) -> Result<UserEntity, AppError> {
|
||||||
|
self.repo.update_role(id, role).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.delete(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct UserEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub email: String,
|
||||||
|
pub name: String,
|
||||||
|
pub role: String,
|
||||||
|
pub provider: String,
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpdateUserInput {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod entity;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::entity::{UpdateUserInput, UserEntity};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait UserRepository: Send + Sync {
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<Option<UserEntity>, AppError>;
|
||||||
|
async fn find_all(&self) -> Result<Vec<UserEntity>, AppError>;
|
||||||
|
async fn update(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
input: UpdateUserInput,
|
||||||
|
) -> Result<UserEntity, AppError>;
|
||||||
|
async fn update_role(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
role: String,
|
||||||
|
) -> Result<UserEntity, AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::entity::{UpdateUserInput, UserEntity};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait QrUserService: Send + Sync {
|
||||||
|
async fn get_profile(&self, user_id: Uuid) -> Result<UserEntity, AppError>;
|
||||||
|
async fn update_profile(
|
||||||
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
input: UpdateUserInput,
|
||||||
|
) -> Result<UserEntity, AppError>;
|
||||||
|
async fn list_all(&self) -> Result<Vec<UserEntity>, AppError>;
|
||||||
|
async fn update_role(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
role: String,
|
||||||
|
) -> Result<UserEntity, AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateProfileRequest {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateRoleRequest {
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct UserResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub email: String,
|
||||||
|
pub name: String,
|
||||||
|
pub role: String,
|
||||||
|
pub provider: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
use axum::{
|
||||||
|
Extension, Json,
|
||||||
|
extract::Path,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use imphnen_utils::{errors::AppError, response_format::ApiSuccess};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::qr::{
|
||||||
|
middleware::qr_auth::QrAuthUser,
|
||||||
|
users::{
|
||||||
|
domain::{entity::UpdateUserInput, service::QrUserService},
|
||||||
|
infrastructure::http::dto::{UpdateProfileRequest, UpdateRoleRequest},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/qr/users/me",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get my QR user profile",
|
||||||
|
example = json!({
|
||||||
|
"data": {
|
||||||
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "Budi Santoso",
|
||||||
|
"role": "user",
|
||||||
|
"provider": "google",
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
"version": "0.3.0"
|
||||||
|
})),
|
||||||
|
(status = 401, description = "Unauthorized")
|
||||||
|
),
|
||||||
|
tag = "QR - Users",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn get_me_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let user = service.get_profile(auth_user.user_id).await?;
|
||||||
|
Ok(ApiSuccess(user).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/qr/users/me",
|
||||||
|
request_body = UpdateProfileRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update my QR user profile",
|
||||||
|
example = json!({
|
||||||
|
"data": {
|
||||||
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"email": "updated@example.com",
|
||||||
|
"name": "Budi Santoso Updated",
|
||||||
|
"role": "user",
|
||||||
|
"provider": "google",
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T00:00:00Z"
|
||||||
|
},
|
||||||
|
"version": "0.3.0"
|
||||||
|
})),
|
||||||
|
(status = 401, description = "Unauthorized")
|
||||||
|
),
|
||||||
|
tag = "QR - Users",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn update_me_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
Json(body): Json<UpdateProfileRequest>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let input = UpdateUserInput {
|
||||||
|
name: body.name,
|
||||||
|
email: body.email,
|
||||||
|
};
|
||||||
|
let user = service.update_profile(auth_user.user_id, input).await?;
|
||||||
|
Ok(ApiSuccess(user).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/qr/users",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin: list all QR users",
|
||||||
|
example = json!({
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "Budi Santoso",
|
||||||
|
"role": "user",
|
||||||
|
"provider": "google",
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4gb96g75-6828-5673-c4gd-3d074g77bgb7",
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"name": "Admin User",
|
||||||
|
"role": "admin",
|
||||||
|
"provider": "google",
|
||||||
|
"created_at": "2024-12-01T00:00:00Z",
|
||||||
|
"updated_at": "2024-12-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": "0.3.0"
|
||||||
|
})),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - admin only")
|
||||||
|
),
|
||||||
|
tag = "QR - Users",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn list_users_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
if auth_user.role != "admin" {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Admin access required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let users = service.list_all().await?;
|
||||||
|
Ok(ApiSuccess(users).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/qr/users/{id}/role",
|
||||||
|
params(("id" = Uuid, Path, description = "User ID")),
|
||||||
|
request_body = UpdateRoleRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin: update user role",
|
||||||
|
example = json!({
|
||||||
|
"data": {
|
||||||
|
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "Budi Santoso",
|
||||||
|
"role": "admin",
|
||||||
|
"provider": "google",
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-20T00:00:00Z"
|
||||||
|
},
|
||||||
|
"version": "0.3.0"
|
||||||
|
})),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - admin only")
|
||||||
|
),
|
||||||
|
tag = "QR - Users",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn update_role_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(body): Json<UpdateRoleRequest>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
if auth_user.role != "admin" {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Admin access required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let user = service.update_role(id, body.role).await?;
|
||||||
|
Ok(ApiSuccess(user).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/qr/users/{id}",
|
||||||
|
params(("id" = Uuid, Path, description = "User ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin: delete QR user",
|
||||||
|
example = json!({"message": "User deleted successfully", "version": "0.3.0"})),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - admin only")
|
||||||
|
),
|
||||||
|
tag = "QR - Users",
|
||||||
|
security(("Bearer" = []))
|
||||||
|
)]
|
||||||
|
pub async fn delete_user_handler(
|
||||||
|
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||||
|
Extension(auth_user): Extension<QrAuthUser>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
if auth_user.role != "admin" {
|
||||||
|
return Err(AppError::ForbiddenError(
|
||||||
|
"Admin access required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
service.delete(id).await?;
|
||||||
|
Ok(
|
||||||
|
imphnen_utils::response_format::ApiMessage::ok("User deleted successfully")
|
||||||
|
.into_response(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use axum::{
|
||||||
|
Extension, Router,
|
||||||
|
middleware::from_fn,
|
||||||
|
routing::{delete, get, put},
|
||||||
|
};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::qr::{
|
||||||
|
middleware::qr_auth::qr_auth_middleware,
|
||||||
|
users::{
|
||||||
|
application::user_service::QrUserServiceImpl,
|
||||||
|
domain::{repository::UserRepository, service::QrUserService},
|
||||||
|
infrastructure::{
|
||||||
|
http::handlers::{
|
||||||
|
delete_user_handler, get_me_handler, list_users_handler, update_me_handler,
|
||||||
|
update_role_handler,
|
||||||
|
},
|
||||||
|
persistence::postgres_user_repository::PostgresUserRepository,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn qr_users_routes(pool: Arc<PgPool>) -> Router {
|
||||||
|
let repo: Arc<dyn UserRepository> =
|
||||||
|
Arc::new(PostgresUserRepository::new(pool.clone()));
|
||||||
|
let service: Arc<dyn QrUserService> = Arc::new(QrUserServiceImpl::new(repo));
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.route("/users/me", get(get_me_handler).put(update_me_handler))
|
||||||
|
.route("/users", get(list_users_handler))
|
||||||
|
.route("/users/{id}/role", put(update_role_handler))
|
||||||
|
.route("/users/{id}", delete(delete_user_handler))
|
||||||
|
.layer(Extension(service))
|
||||||
|
.layer(from_fn(qr_auth_middleware))
|
||||||
|
.layer(Extension(pool))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod postgres_user_repository;
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::errors::AppError;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::qr::users::domain::{
|
||||||
|
entity::{UpdateUserInput, UserEntity},
|
||||||
|
repository::UserRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(FromRow)]
|
||||||
|
struct UserRow {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub email: String,
|
||||||
|
pub name: String,
|
||||||
|
pub role: String,
|
||||||
|
pub provider: String,
|
||||||
|
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UserRow> for UserEntity {
|
||||||
|
fn from(row: UserRow) -> Self {
|
||||||
|
UserEntity {
|
||||||
|
id: row.id,
|
||||||
|
email: row.email,
|
||||||
|
name: row.name,
|
||||||
|
role: row.role,
|
||||||
|
provider: row.provider,
|
||||||
|
created_at: row.created_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresUserRepository {
|
||||||
|
pool: Arc<PgPool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresUserRepository {
|
||||||
|
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UserRepository for PostgresUserRepository {
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<Option<UserEntity>, AppError> {
|
||||||
|
sqlx::query_as::<_, UserRow>(
|
||||||
|
"SELECT id, email, name, role, provider, created_at, updated_at FROM qr_users WHERE id = $1",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||||
|
.map(|opt| opt.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_all(&self) -> Result<Vec<UserEntity>, AppError> {
|
||||||
|
sqlx::query_as::<_, UserRow>(
|
||||||
|
"SELECT id, email, name, role, provider, created_at, updated_at FROM qr_users ORDER BY created_at DESC",
|
||||||
|
)
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||||
|
.map(|rows| rows.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
input: UpdateUserInput,
|
||||||
|
) -> Result<UserEntity, AppError> {
|
||||||
|
sqlx::query_as::<_, UserRow>(
|
||||||
|
"UPDATE qr_users SET name = COALESCE($1, name), email = COALESCE($2, email), updated_at = NOW() WHERE id = $3 RETURNING id, email, name, role, provider, created_at, updated_at",
|
||||||
|
)
|
||||||
|
.bind(input.name)
|
||||||
|
.bind(input.email)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_role(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
role: String,
|
||||||
|
) -> Result<UserEntity, AppError> {
|
||||||
|
sqlx::query_as::<_, UserRow>(
|
||||||
|
"UPDATE qr_users SET role = $1, updated_at = NOW() WHERE id = $2 RETURNING id, email, name, role, provider, created_at, updated_at",
|
||||||
|
)
|
||||||
|
.bind(role)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
sqlx::query("DELETE FROM qr_users WHERE id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
pub use infrastructure::http::routes::qr_users_routes;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod roadmap_service;
|
||||||
|
|
||||||
|
pub use roadmap_service::RoadmapServiceImpl;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::roadmap::domain::{RoadmapEntity, RoadmapRepository, RoadmapService};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct RoadmapServiceImpl {
|
||||||
|
repo: Arc<dyn RoadmapRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoadmapServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn RoadmapRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RoadmapService for RoadmapServiceImpl {
|
||||||
|
async fn list(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<RoadmapEntity>, AppError> {
|
||||||
|
self.repo.find_all(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, id: Uuid) -> Result<RoadmapEntity, AppError> {
|
||||||
|
self.repo.find_by_id(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError> {
|
||||||
|
self.repo.create(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError> {
|
||||||
|
self.repo.update(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.delete(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn vote(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
self.repo.increment_votes(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod roadmap;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use roadmap::RoadmapEntity;
|
||||||
|
pub use repository::RoadmapRepository;
|
||||||
|
pub use service::RoadmapService;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use super::roadmap::RoadmapEntity;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RoadmapRepository: Send + Sync {
|
||||||
|
async fn find_all(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<RoadmapEntity>, AppError>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<RoadmapEntity, AppError>;
|
||||||
|
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError>;
|
||||||
|
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
async fn increment_votes(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RoadmapEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub status: String,
|
||||||
|
pub votes: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use super::roadmap::RoadmapEntity;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::PaginationParams;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RoadmapService: Send + Sync {
|
||||||
|
async fn list(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<RoadmapEntity>, AppError>;
|
||||||
|
async fn get(&self, id: Uuid) -> Result<RoadmapEntity, AppError>;
|
||||||
|
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError>;
|
||||||
|
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
async fn vote(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
use crate::roadmap::domain::roadmap::RoadmapEntity;
|
||||||
|
use imphnen_libs::ZodValidate;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RoadmapCreateRequestDto {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
#[schema(example = "upcoming")]
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for RoadmapCreateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RoadmapCreateRequestDto> for RoadmapEntity {
|
||||||
|
fn from(dto: RoadmapCreateRequestDto) -> Self {
|
||||||
|
RoadmapEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
title: dto.title,
|
||||||
|
description: dto.description,
|
||||||
|
status: dto.status,
|
||||||
|
votes: 0,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RoadmapUpdateRequestDto {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
#[schema(example = "upcoming")]
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZodValidate for RoadmapUpdateRequestDto {
|
||||||
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RoadmapListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub status: String,
|
||||||
|
pub votes: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RoadmapEntity> for RoadmapListItemDto {
|
||||||
|
fn from(e: RoadmapEntity) -> Self {
|
||||||
|
RoadmapListItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
title: e.title,
|
||||||
|
description: e.description,
|
||||||
|
status: e.status,
|
||||||
|
votes: e.votes,
|
||||||
|
is_deleted: e.is_deleted,
|
||||||
|
created_at: e.created_at.to_rfc3339(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RoadmapDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub status: String,
|
||||||
|
pub votes: i32,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RoadmapEntity> for RoadmapDetailItemDto {
|
||||||
|
fn from(e: RoadmapEntity) -> Self {
|
||||||
|
RoadmapDetailItemDto {
|
||||||
|
id: e.id.to_string(),
|
||||||
|
title: e.title,
|
||||||
|
description: e.description,
|
||||||
|
status: e.status,
|
||||||
|
votes: e.votes,
|
||||||
|
created_at: e.created_at.to_rfc3339(),
|
||||||
|
updated_at: e.updated_at.to_rfc3339(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
use super::dto::{
|
||||||
|
RoadmapCreateRequestDto, RoadmapDetailItemDto, RoadmapListItemDto,
|
||||||
|
RoadmapUpdateRequestDto,
|
||||||
|
};
|
||||||
|
use crate::roadmap::domain::RoadmapService;
|
||||||
|
use axum::{
|
||||||
|
Extension,
|
||||||
|
extract::Path,
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_utils::{ApiMessage, ApiPaginated, ApiSuccess};
|
||||||
|
use paginator_axum::PaginationQuery;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/landing/cms/roadmap",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get roadmap list")
|
||||||
|
),
|
||||||
|
tag = "Roadmap"
|
||||||
|
)]
|
||||||
|
pub async fn get_roadmap_list(
|
||||||
|
Extension(service): Extension<Arc<dyn RoadmapService>>,
|
||||||
|
PaginationQuery(params): PaginationQuery,
|
||||||
|
) -> Response {
|
||||||
|
match service.list(params).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let mapped = PaginatorResponse {
|
||||||
|
data: result
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(RoadmapListItemDto::from)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
meta: result.meta,
|
||||||
|
};
|
||||||
|
ApiPaginated(mapped).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string())
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/landing/cms/roadmap/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Roadmap item ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Get roadmap item by ID", body = ResponseSuccessDto<RoadmapDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Roadmap"
|
||||||
|
)]
|
||||||
|
pub async fn get_roadmap_by_id(
|
||||||
|
Extension(service): Extension<Arc<dyn RoadmapService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
let uuid = match Uuid::parse_str(&id) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
return ApiMessage::new(
|
||||||
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
|
format!("Invalid UUID: {e}"),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match service.get(uuid).await {
|
||||||
|
Ok(item) => ApiSuccess(RoadmapDetailItemDto::from(item)).into_response(),
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string())
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/landing/cms/roadmap/create",
|
||||||
|
request_body = RoadmapCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "[ADMIN] Create new roadmap item")
|
||||||
|
),
|
||||||
|
tag = "Roadmap"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_roadmap(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn RoadmapService>>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<RoadmapCreateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let entity = payload.into();
|
||||||
|
service.create(entity).await?;
|
||||||
|
Ok(ApiMessage::created("Roadmap item created"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/landing/cms/roadmap/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Roadmap item ID")
|
||||||
|
),
|
||||||
|
request_body = RoadmapUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Update roadmap item")
|
||||||
|
),
|
||||||
|
tag = "Roadmap"
|
||||||
|
)]
|
||||||
|
pub async fn patch_update_roadmap(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn RoadmapService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
ValidatedJson(payload): ValidatedJson<RoadmapUpdateRequestDto>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
let existing = service.get(uuid).await?;
|
||||||
|
let entity = crate::roadmap::domain::RoadmapEntity {
|
||||||
|
id: existing.id,
|
||||||
|
title: payload.title,
|
||||||
|
description: payload.description,
|
||||||
|
status: payload.status,
|
||||||
|
votes: existing.votes,
|
||||||
|
is_deleted: existing.is_deleted,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
service.update(entity).await?;
|
||||||
|
Ok(ApiMessage::ok("Roadmap item updated"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(("Bearer" = [])),
|
||||||
|
path = "/v1/landing/cms/roadmap/delete/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Roadmap item ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[ADMIN] Soft delete roadmap item")
|
||||||
|
),
|
||||||
|
tag = "Roadmap"
|
||||||
|
)]
|
||||||
|
pub async fn delete_roadmap(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(service): Extension<Arc<dyn RoadmapService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
|
let uuid = Uuid::parse_str(&id)
|
||||||
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
|
service.delete(uuid).await?;
|
||||||
|
Ok(ApiMessage::ok("Roadmap item deleted"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/landing/cms/roadmap/vote/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Roadmap item ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "[PUBLIC] Vote for a roadmap item")
|
||||||
|
),
|
||||||
|
tag = "Roadmap"
|
||||||
|
)]
|
||||||
|
pub async fn post_vote_roadmap(
|
||||||
|
Extension(service): Extension<Arc<dyn RoadmapService>>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
let uuid = match Uuid::parse_str(&id) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
return ApiMessage::new(
|
||||||
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
|
format!("Invalid UUID: {e}"),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match service.vote(uuid).await {
|
||||||
|
Ok(()) => ApiMessage::ok("Vote recorded").into_response(),
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string())
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::{roadmap_protected_routes, roadmap_public_routes};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use super::handlers::{
|
||||||
|
delete_roadmap, get_roadmap_by_id, get_roadmap_list, patch_update_roadmap,
|
||||||
|
post_create_roadmap, post_vote_roadmap,
|
||||||
|
};
|
||||||
|
use crate::roadmap::application::RoadmapServiceImpl;
|
||||||
|
use crate::roadmap::domain::RoadmapService;
|
||||||
|
use crate::roadmap::infrastructure::persistence::PostgresRoadmapRepository;
|
||||||
|
use axum::{
|
||||||
|
Extension, Router,
|
||||||
|
routing::{delete, get, patch, post},
|
||||||
|
};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn build_service(db: DatabaseConnection) -> Arc<dyn RoadmapService> {
|
||||||
|
let repo = Arc::new(PostgresRoadmapRepository::new(db));
|
||||||
|
Arc::new(RoadmapServiceImpl::new(repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roadmap_public_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/roadmap", get(get_roadmap_list))
|
||||||
|
.route("/roadmap/detail/{id}", get(get_roadmap_by_id))
|
||||||
|
.route("/roadmap/vote/{id}", post(post_vote_roadmap))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roadmap_protected_routes(db: DatabaseConnection) -> Router {
|
||||||
|
let service = build_service(db);
|
||||||
|
Router::new()
|
||||||
|
.route("/roadmap/create", post(post_create_roadmap))
|
||||||
|
.route("/roadmap/update/{id}", patch(patch_update_roadmap))
|
||||||
|
.route("/roadmap/delete/{id}", delete(delete_roadmap))
|
||||||
|
.layer(Extension(service))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
|
pub mod persistence;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod postgres_roadmap_repository;
|
||||||
|
|
||||||
|
pub use postgres_roadmap_repository::PostgresRoadmapRepository;
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
use crate::roadmap::domain::{roadmap::RoadmapEntity, repository::RoadmapRepository};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_entities::seaorm::common::roadmap_items::{
|
||||||
|
ActiveModel as RoadmapActiveModel, Column as RoadmapColumn, Entity as RoadmapEntity_,
|
||||||
|
Model as RoadmapModel,
|
||||||
|
};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, Order, PaginatorTrait, QueryOrder};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn to_entity(model: RoadmapModel) -> RoadmapEntity {
|
||||||
|
RoadmapEntity {
|
||||||
|
id: model.id,
|
||||||
|
title: model.title,
|
||||||
|
description: model.description,
|
||||||
|
status: model.status,
|
||||||
|
votes: model.votes,
|
||||||
|
is_deleted: model.is_deleted,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresRoadmapRepository {
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresRoadmapRepository {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db: Arc::new(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RoadmapRepository for PostgresRoadmapRepository {
|
||||||
|
async fn find_all(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<RoadmapEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
|
let mut query = RoadmapEntity_::find().filter(RoadmapColumn::IsDeleted.eq(false));
|
||||||
|
|
||||||
|
if let Some(ref search) = params.search {
|
||||||
|
query = query.filter(RoadmapColumn::Title.contains(&search.query));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = match params.sort_by.as_deref() {
|
||||||
|
Some("title") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Desc) => query.order_by(RoadmapColumn::Title, Order::Desc),
|
||||||
|
_ => query.order_by(RoadmapColumn::Title, Order::Asc),
|
||||||
|
},
|
||||||
|
Some("votes") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => query.order_by(RoadmapColumn::Votes, Order::Asc),
|
||||||
|
_ => query.order_by(RoadmapColumn::Votes, Order::Desc),
|
||||||
|
},
|
||||||
|
_ => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => {
|
||||||
|
query.order_by(RoadmapColumn::CreatedAt, Order::Asc)
|
||||||
|
}
|
||||||
|
_ => query.order_by(RoadmapColumn::CreatedAt, Order::Desc),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
|
let total = paginator
|
||||||
|
.num_items()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let items = paginator
|
||||||
|
.fetch_page((page - 1) as u64)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let data = items.into_iter().map(to_entity).collect();
|
||||||
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
|
Ok(PaginatorResponse { data, meta })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<RoadmapEntity, AppError> {
|
||||||
|
let item = RoadmapEntity_::find_by_id(id)
|
||||||
|
.filter(RoadmapColumn::IsDeleted.eq(false))
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(to_entity(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError> {
|
||||||
|
let active_model = RoadmapActiveModel {
|
||||||
|
id: ActiveValue::Set(entity.id),
|
||||||
|
title: ActiveValue::Set(entity.title),
|
||||||
|
description: ActiveValue::Set(entity.description),
|
||||||
|
status: ActiveValue::Set(entity.status),
|
||||||
|
votes: ActiveValue::Set(0),
|
||||||
|
is_deleted: ActiveValue::Set(false),
|
||||||
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
RoadmapEntity_::insert(active_model)
|
||||||
|
.exec(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError> {
|
||||||
|
let mut active_model: RoadmapActiveModel = RoadmapEntity_::find_by_id(entity.id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.title = ActiveValue::Set(entity.title);
|
||||||
|
active_model.description = ActiveValue::Set(entity.description);
|
||||||
|
active_model.status = ActiveValue::Set(entity.status);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let mut active_model: RoadmapActiveModel = RoadmapEntity_::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn increment_votes(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let item = RoadmapEntity_::find_by_id(id)
|
||||||
|
.filter(RoadmapColumn::IsDeleted.eq(false))
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?;
|
||||||
|
|
||||||
|
let new_votes = item.votes + 1;
|
||||||
|
let mut active_model: RoadmapActiveModel = item.into();
|
||||||
|
active_model.votes = ActiveValue::Set(new_votes);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
|
||||||
|
pub use infrastructure::http::{roadmap_protected_routes, roadmap_public_routes};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user