Compare commits
109
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
97c2fce7be | ||
|
|
98c46611fb | ||
|
|
fbfe3dcd51 | ||
|
|
5cc5a3dfe1 | ||
|
|
95ae55c9df | ||
|
|
f7ca67d720 | ||
|
|
02421e9bc2 | ||
|
|
5e2b0d3caf | ||
|
|
b9a51ce6cc | ||
|
|
d4a6c4c9ea | ||
|
|
ece6499e2b | ||
|
|
1caaa8404b | ||
|
|
cb6eef2054 | ||
|
|
e77797d7bb | ||
|
|
d5ccf4cf75 | ||
|
|
1a36698962 | ||
|
|
15a2ee1950 | ||
|
|
bef8482402 | ||
|
|
768e583f81 | ||
|
|
1ca6d8f47c | ||
|
|
3fcfb3709e | ||
|
|
6915a97d79 | ||
|
|
6f596efadd | ||
|
|
b6b5f48055 | ||
|
|
789b20c278 | ||
|
|
884b679ba8 | ||
|
|
6ef624c169 | ||
|
|
c10443f881 | ||
|
|
466ba3391a | ||
|
|
8c2527d61e | ||
|
|
018124f7b3 | ||
|
|
7749f6fdec | ||
|
|
b27e4a4404 | ||
|
|
3dde67a4ac | ||
|
|
49abcf28c3 | ||
|
|
9cecc8fa05 | ||
|
|
a32f54873b | ||
|
|
af41a98111 | ||
|
|
4c435708c0 | ||
|
|
ac462ea771 | ||
|
|
3e52223730 | ||
|
|
01e479350b | ||
|
|
b0cf9817d7 | ||
|
|
e530eba60d | ||
|
|
ef1d63e893 | ||
|
|
5859af5294 | ||
|
|
c12da948aa | ||
|
|
3cde18360c | ||
|
|
14b22328de | ||
|
|
96debc210a | ||
|
|
b80662188f | ||
|
|
7b9ce6cda1 | ||
|
|
1bb2d6a419 | ||
|
|
14690a378f | ||
|
|
17cc4ccdba | ||
|
|
1b7368a4b7 | ||
|
|
ff2e14d5f9 | ||
|
|
012aff60ef | ||
|
|
f3d88e2ac9 | ||
|
|
0e48b6755f | ||
|
|
7056f8c8a6 | ||
|
|
4ecdcd12d2 | ||
|
|
dd4a73e360 | ||
|
|
49e0a89ad1 | ||
|
|
82b85abe8f | ||
|
|
df11aa5eb8 | ||
|
|
ac44867ec1 | ||
|
|
7205afe43b | ||
|
|
b1c678c72b | ||
|
|
fa3687c6be | ||
|
|
ba46a19a5a | ||
|
|
8cdbca24ba | ||
|
|
8f981572cd | ||
|
|
39b75f410f | ||
|
|
6da1b2a7e7 | ||
|
|
acccae9f7c | ||
|
|
e7c17784cc | ||
|
|
042e0b4eff |
@@ -0,0 +1,2 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld.exe"
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
target
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
.git
|
||||
.gitignore
|
||||
target
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
+15
-31
@@ -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_LOG=debug
|
||||
PORT=4099
|
||||
SURREALDB_URL=ws://localhost:8000/rpc
|
||||
SURREALDB_USERNAME=root
|
||||
SURREALDB_PASSWORD=root
|
||||
SURREALDB_NAMESPACE=test
|
||||
SURREALDB_DBNAME=test
|
||||
ACCESS_TOKEN_SECRET=your-access-token-secret-key-here
|
||||
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
||||
SMTP_EMAIL=your-email@example.com
|
||||
@@ -46,4 +16,18 @@ MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_SECURE=false
|
||||
|
||||
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
-1
@@ -1 +1 @@
|
||||
use flake --impure
|
||||
use flake --impure
|
||||
|
||||
@@ -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
|
||||
+16
-15
@@ -1,15 +1,16 @@
|
||||
# Build
|
||||
/target
|
||||
/resutl
|
||||
|
||||
# Nix
|
||||
/.direnv
|
||||
/Cargo.nix
|
||||
|
||||
# Environment
|
||||
.envrc
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.staging
|
||||
.env.production
|
||||
# Build
|
||||
/target
|
||||
/resutl
|
||||
|
||||
# Nix
|
||||
/.direnv
|
||||
/Cargo.nix
|
||||
|
||||
# Environment
|
||||
.envrc
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.staging
|
||||
.env.production
|
||||
**/**.log
|
||||
@@ -0,0 +1 @@
|
||||
/cache
|
||||
@@ -0,0 +1,71 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: rust
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "imphnen-backend-service"
|
||||
Generated
+1864
-2426
File diff suppressed because it is too large
Load Diff
+30
-12
@@ -1,42 +1,50 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"tests",
|
||||
members = [
|
||||
"imphnen-entities",
|
||||
"imphnen-macros",
|
||||
"imphnen-libs",
|
||||
"imphnen-storage",
|
||||
"imphnen-email",
|
||||
"imphnen-utils",
|
||||
"imphnen-middleware",
|
||||
"imphnen-iam",
|
||||
"imphnen-cms",
|
||||
"imphnen-libs",
|
||||
"imphnen-utils",
|
||||
"imphnen-gacha",
|
||||
"imphnen-dimentorin",
|
||||
"imphnen-hackathon",
|
||||
"imphnen-gateway",
|
||||
"imphnen-backend",
|
||||
"imphnen-entities",
|
||||
"imphnen-dimentorin",
|
||||
"imphnen-middleware",
|
||||
]
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.83"
|
||||
oauth2 = "5.0.0"
|
||||
reqwest = { version = "0.12.23", features = ["json"] }
|
||||
serde_json = "1.0.142"
|
||||
axum = { version = "0.8.4", features = ["multipart"] }
|
||||
axum = { version = "0.8.4", features = ["multipart", "macros"] }
|
||||
log = "0.4.27"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||
jsonwebtoken = "9.3.1"
|
||||
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"] }
|
||||
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
||||
thiserror = "2.0.14"
|
||||
anyhow = "1.0.99"
|
||||
rand = { version = "0.9.2", features = ["std", "alloc"] }
|
||||
rand_distr = "0.5.1"
|
||||
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
|
||||
http-body-util = "0.1.3"
|
||||
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"
|
||||
regex = "1.11.1"
|
||||
axum-test = "17.3.0"
|
||||
@@ -57,6 +65,9 @@ urlencoding = "2.1"
|
||||
hyper = "1.6.0"
|
||||
hyper-util = "0.1.16"
|
||||
minio = "0.3.0"
|
||||
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros", "with-chrono", "uuid"] }
|
||||
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "uuid", "chrono", "json", "macros"] }
|
||||
num_cpus = "1.16.0"
|
||||
|
||||
|
||||
tokio-test = "0.4.4"
|
||||
@@ -68,7 +79,6 @@ tokio-tungstenite = "0.27"
|
||||
url = "2.5"
|
||||
futures-util = "0.3"
|
||||
http = "1.3"
|
||||
tests = { path = "./tests" }
|
||||
imphnen-iam = { path = "./imphnen-iam" }
|
||||
imphnen-cms = { path = "./imphnen-cms" }
|
||||
imphnen-libs = { path = "./imphnen-libs" }
|
||||
@@ -79,9 +89,17 @@ imphnen-backend = { path = "./imphnen-backend" }
|
||||
imphnen-entities = { path = "./imphnen-entities" }
|
||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||
imphnen-macros = { path = "./imphnen-macros" }
|
||||
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]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
opt-level = "z"
|
||||
|
||||
|
||||
+21
-13
@@ -11,30 +11,38 @@ WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
RUN mkdir -p imphnen-backend/src imphnen-cms/src imphnen-dimentorin/src \
|
||||
imphnen-entities/src imphnen-gacha/src imphnen-gateway/src \
|
||||
imphnen-iam/src imphnen-libs/src imphnen-middleware/src \
|
||||
imphnen-utils/src tests/src && \
|
||||
RUN mkdir -p \
|
||||
imphnen-backend/src \
|
||||
imphnen-cms/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 && \
|
||||
find . -name "src" -type d -exec sh -c 'echo "// dummy" > "$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
|
||||
|
||||
find . -name "src" -type d -exec sh -c 'touch "$1/lib.rs"' _ {} \;
|
||||
|
||||
COPY imphnen-backend ./imphnen-backend
|
||||
COPY imphnen-cms ./imphnen-cms
|
||||
COPY imphnen-dimentorin ./imphnen-dimentorin
|
||||
COPY imphnen-email ./imphnen-email
|
||||
COPY imphnen-entities ./imphnen-entities
|
||||
COPY imphnen-gacha ./imphnen-gacha
|
||||
COPY imphnen-gateway ./imphnen-gateway
|
||||
COPY imphnen-hackathon ./imphnen-hackathon
|
||||
COPY imphnen-iam ./imphnen-iam
|
||||
COPY imphnen-libs ./imphnen-libs
|
||||
COPY imphnen-macros ./imphnen-macros
|
||||
COPY imphnen-middleware ./imphnen-middleware
|
||||
COPY imphnen-storage ./imphnen-storage
|
||||
COPY imphnen-utils ./imphnen-utils
|
||||
COPY tests ./tests
|
||||
|
||||
RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-units=1 -C strip=symbols" \
|
||||
cargo build -p imphnen-backend --release && \
|
||||
@@ -44,4 +52,4 @@ RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-un
|
||||
FROM scratch AS runner
|
||||
COPY --from=builder /app/target/release/api /api
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
ENTRYPOINT ["/api"]
|
||||
ENTRYPOINT ["/api"]
|
||||
|
||||
@@ -1,130 +1,130 @@
|
||||
# IMPHNEN Backend Service
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||
</p>
|
||||
|
||||
This repository serves as the **monorepo** for all backend services of IMPHNEN. It encompasses several main services:
|
||||
|
||||
1. **IMPHNEN-Backend** - Provides fundamental functionalities and shared resources for other services.
|
||||
2. **IMPHNEN-IAM** - Handles identity and access management across IMPHNEN applications.
|
||||
3. **IMPHNEN-CMS** - Supports the cms services by IMPHNEN [Landing Page website](https://imphnen.dev/).
|
||||
4. **IMPHNEN-Gacha** - Supports the gacha services by IMPHNEN [Gacha website](https://gacha.imphnen.dev/).
|
||||
5. **IMPHNEN-Dimentorin** - Supports the mentoring services by IMPHNEN [Dimentorin website](https://dimentorin.imphnen.dev/).
|
||||
6. **IMPHNEN-Gateway** - Acts as the API gateway, routing requests to appropriate services.
|
||||
7. **IMPHNEN-Middleware** - Acts as the middleware for the API Gateway, providing authentication and authorization.
|
||||
|
||||
## How to Install
|
||||
|
||||
1. **Clone the repository**:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/IMPHNEN/imphnen-backend-service.git
|
||||
cd imphnen-backend-service
|
||||
```
|
||||
|
||||
2. **Set up the environment**:
|
||||
|
||||
- Copy the example environment files:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
if you use windows based system
|
||||
|
||||
```sh
|
||||
./apply-env.ps1
|
||||
```
|
||||
|
||||
if you use unix based system
|
||||
|
||||
```sh
|
||||
source ./apply-env.sh
|
||||
```
|
||||
|
||||
- Modify the `.env` files with your specific configuration settings.
|
||||
|
||||
3. **Install dependencies**:
|
||||
|
||||
Ensure you have [Rust](https://www.rust-lang.org/) installed. Then, run:
|
||||
|
||||
```sh
|
||||
cargo fetch
|
||||
```
|
||||
|
||||
4. **Run the seeders**:
|
||||
|
||||
to run the seeders, run:
|
||||
|
||||
```sh
|
||||
cargo run --bin seeder
|
||||
```
|
||||
|
||||
## How to Run
|
||||
|
||||
### Development
|
||||
|
||||
To run the services in development mode:
|
||||
|
||||
1. **Start the database and other dependencies** using Docker Compose:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Run using cargo run**. For example, to run the Core Service:
|
||||
|
||||
```sh
|
||||
cargo run --bin api
|
||||
```
|
||||
|
||||
3. **Run using cargo watch**. For example, to run the Core Service:
|
||||
|
||||
```sh
|
||||
cargo watch -x "run --bin api"
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
For production deployment:
|
||||
|
||||
1. **Build the Docker image**:
|
||||
|
||||
```sh
|
||||
docker build -t imphnen-backend .
|
||||
```
|
||||
|
||||
2. **Run the Docker container**:
|
||||
|
||||
```sh
|
||||
docker run --name imphnen-backend -d --env-file .env -p 3000:3000 imphnen-backend
|
||||
```
|
||||
|
||||
Adjust the port and environment variables as needed.
|
||||
|
||||
## How to Run the Tests
|
||||
|
||||
1. **Run the tests**:
|
||||
|
||||
```sh
|
||||
cargo test -p tests
|
||||
```
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Fork the repository** and clone it locally.
|
||||
2. **Create a new branch** for your feature or fix:
|
||||
|
||||
```sh
|
||||
git checkout -b feat/your-feature-name
|
||||
```
|
||||
|
||||
3. **Make your changes**, commit them, and push to your forked repository.
|
||||
4. **Create a pull request** to the `develop` branch of this repository.
|
||||
|
||||
If you encounter any issues or have questions, feel free to create a new issue in the repository.
|
||||
|
||||
---
|
||||
|
||||
_Note: For detailed API documentation, please refer to our [API Docs](https://api.imphnen.dev/docs)._
|
||||
# IMPHNEN Backend Service
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||
</p>
|
||||
|
||||
This repository serves as the **monorepo** for all backend services of IMPHNEN. It encompasses several main services:
|
||||
|
||||
1. **IMPHNEN-Backend** - Provides fundamental functionalities and shared resources for other services.
|
||||
2. **IMPHNEN-IAM** - Handles identity and access management across IMPHNEN applications.
|
||||
3. **IMPHNEN-CMS** - Supports the cms services by IMPHNEN [Landing Page website](https://imphnen.dev/).
|
||||
4. **IMPHNEN-Gacha** - Supports the gacha services by IMPHNEN [Gacha website](https://gacha.imphnen.dev/).
|
||||
5. **IMPHNEN-Dimentorin** - Supports the mentoring services by IMPHNEN [Dimentorin website](https://dimentorin.imphnen.dev/).
|
||||
6. **IMPHNEN-Gateway** - Acts as the API gateway, routing requests to appropriate services.
|
||||
7. **IMPHNEN-Middleware** - Acts as the middleware for the API Gateway, providing authentication and authorization.
|
||||
|
||||
## How to Install
|
||||
|
||||
1. **Clone the repository**:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/IMPHNEN/imphnen-backend-service.git
|
||||
cd imphnen-backend-service
|
||||
```
|
||||
|
||||
2. **Set up the environment**:
|
||||
|
||||
- Copy the example environment files:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
if you use windows based system
|
||||
|
||||
```sh
|
||||
./apply-env.ps1
|
||||
```
|
||||
|
||||
if you use unix based system
|
||||
|
||||
```sh
|
||||
source ./apply-env.sh
|
||||
```
|
||||
|
||||
- Modify the `.env` files with your specific configuration settings.
|
||||
|
||||
3. **Install dependencies**:
|
||||
|
||||
Ensure you have [Rust](https://www.rust-lang.org/) installed. Then, run:
|
||||
|
||||
```sh
|
||||
cargo fetch
|
||||
```
|
||||
|
||||
4. **Run the seeders**:
|
||||
|
||||
to run the seeders, run:
|
||||
|
||||
```sh
|
||||
cargo run --bin seeder
|
||||
```
|
||||
|
||||
## How to Run
|
||||
|
||||
### Development
|
||||
|
||||
To run the services in development mode:
|
||||
|
||||
1. **Start the database and other dependencies** using Docker Compose:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Run using cargo run**. For example, to run the Core Service:
|
||||
|
||||
```sh
|
||||
cargo run --bin api
|
||||
```
|
||||
|
||||
3. **Run using cargo watch**. For example, to run the Core Service:
|
||||
|
||||
```sh
|
||||
cargo watch -x "run --bin api"
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
For production deployment:
|
||||
|
||||
1. **Build the Docker image**:
|
||||
|
||||
```sh
|
||||
docker build -t imphnen-backend .
|
||||
```
|
||||
|
||||
2. **Run the Docker container**:
|
||||
|
||||
```sh
|
||||
docker run --name imphnen-backend -d --env-file .env -p 3000:3000 imphnen-backend
|
||||
```
|
||||
|
||||
Adjust the port and environment variables as needed.
|
||||
|
||||
## How to Run the Tests
|
||||
|
||||
1. **Run the tests**:
|
||||
|
||||
```sh
|
||||
cargo test -p tests
|
||||
```
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Fork the repository** and clone it locally.
|
||||
2. **Create a new branch** for your feature or fix:
|
||||
|
||||
```sh
|
||||
git checkout -b feat/your-feature-name
|
||||
```
|
||||
|
||||
3. **Make your changes**, commit them, and push to your forked repository.
|
||||
4. **Create a pull request** to the `develop` branch of this repository.
|
||||
|
||||
If you encounter any issues or have questions, feel free to create a new issue in the repository.
|
||||
|
||||
---
|
||||
|
||||
_Note: For detailed API documentation, please refer to our [API Docs](https://api.imphnen.dev/docs)._
|
||||
|
||||
@@ -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"
|
||||
+24
-14
@@ -1,15 +1,25 @@
|
||||
{pkgs ? import <nixpkgs> {}}: let
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
rustDeps = pkgs.callPackage ./Cargo.nix {};
|
||||
packageEntry = rustDeps.workspaceMembers.${manifest.name};
|
||||
deps = packageEntry.build.cargoDeps or null;
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
let
|
||||
swaggerUi = pkgs.fetchurl {
|
||||
url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip";
|
||||
hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw=";
|
||||
};
|
||||
in
|
||||
pkgs.rustPlatform.buildRustPackage {
|
||||
pname = manifest.name;
|
||||
version = manifest.version;
|
||||
cargoDeps = deps;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
nativeBuildInputs = [pkgs.openssl pkgs.pkg-config];
|
||||
buildInputs = [pkgs.openssl];
|
||||
}
|
||||
pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "imphnen-backend";
|
||||
version = (pkgs.lib.importTOML ./imphnen-backend/Cargo.toml).package.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
cargoBuildFlags = [
|
||||
"--package"
|
||||
"imphnen-backend"
|
||||
"--bin"
|
||||
"api"
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
preBuild = ''
|
||||
export SWAGGER_UI_DOWNLOAD_URL="file://${swaggerUi}"
|
||||
'';
|
||||
doCheck = false;
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script deploy ke VPS
|
||||
|
||||
set -e
|
||||
|
||||
REMOTE_USER="asephs"
|
||||
REMOTE_HOST="70.153.9.42"
|
||||
REMOTE_PATH="/home/asephs/imphnen-backend-service"
|
||||
|
||||
# Build project
|
||||
taskset -c 0,1 cargo build --release -j 2
|
||||
|
||||
# Rsync hasil build dan file yang diperlukan
|
||||
rsync -avz --delete \
|
||||
target/release/ \
|
||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/target/release/
|
||||
|
||||
# Sync file konfigurasi dan source code (jika perlu)
|
||||
rsync -avz --delete \
|
||||
imphnen-backend/ \
|
||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/imphnen-backend/
|
||||
|
||||
rsync -avz --delete \
|
||||
docker-compose.yml Dockerfile \
|
||||
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/
|
||||
|
||||
# Tambahkan file lain jika diperlukan
|
||||
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
|
||||
cd /home/asephs/imphnen-backend-service
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
|
||||
export PATH=$PATH:/root/.local/share/pnpm
|
||||
export PATH=$PATH:/home/asephs/.nvm/versions/node/v22.17.1/bin
|
||||
export PATH=$PATH:/home/asephs/.bun/bin/bun
|
||||
|
||||
if [ -f ~/.bashrc ]; then
|
||||
source ~/.bashrc
|
||||
fi
|
||||
pm2 restart 4 --update-env
|
||||
EOF
|
||||
|
||||
echo "Deploy selesai ke $REMOTE_HOST:$REMOTE_PATH"
|
||||
+46
-32
@@ -1,32 +1,46 @@
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
env_file: ".env"
|
||||
depends_on:
|
||||
- surrealdb
|
||||
|
||||
surrealdb:
|
||||
image: surrealdb/surrealdb:latest
|
||||
command: start --log trace --user root --pass root
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: minio
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
||||
volumes:
|
||||
minio_data:
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
env_file: ".env"
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: imphnen_postgres
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-imphnen}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-imphnen}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: minio
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
|
||||
+43
-42
@@ -1,42 +1,43 @@
|
||||
{pkgs, ...}: let
|
||||
baseImage = pkgs.ociTools.pullImage {
|
||||
imageName = "ubuntu";
|
||||
tag = "latest";
|
||||
};
|
||||
in
|
||||
pkgs.dockerTools.buildImage {
|
||||
name = "imphnen-cms-api";
|
||||
|
||||
fromImage = baseImage;
|
||||
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "imphnen-cms-api";
|
||||
paths = [
|
||||
(pkgs.stdenv.mkDerivation {
|
||||
name = "imphnen-cms-api";
|
||||
src = ./src;
|
||||
|
||||
buildInputs = [
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.openssl
|
||||
pkgs.pkg-config
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
cargo build --release
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/najm-course-api $out/bin/
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
config = {
|
||||
Cmd = ["/bin/imphnen-cms-api"];
|
||||
WorkingDir = "/bin";
|
||||
};
|
||||
}
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
baseImage = pkgs.ociTools.pullImage {
|
||||
imageName = "ubuntu";
|
||||
tag = "latest";
|
||||
};
|
||||
in
|
||||
pkgs.dockerTools.buildImage {
|
||||
name = "imphnen-backend-service";
|
||||
|
||||
fromImage = baseImage;
|
||||
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "imphnen-backend-service";
|
||||
paths = [
|
||||
(pkgs.stdenv.mkDerivation {
|
||||
name = "imphnen-backend-service";
|
||||
src = ./src;
|
||||
|
||||
buildInputs = [
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.openssl
|
||||
pkgs.pkg-config
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
cargo build --release
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/imphnen-backend-service $out/bin/
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
config = {
|
||||
Cmd = [ "/bin/imphnen-backend-service" ];
|
||||
WorkingDir = "/bin";
|
||||
};
|
||||
}
|
||||
|
||||
+9
-9
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 351 KiB |
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739020877,
|
||||
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
||||
"lastModified": 1775036866,
|
||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -5,28 +5,43 @@
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
}: let
|
||||
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
||||
pkgsFor = system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
}:
|
||||
let
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
];
|
||||
pkgsFor =
|
||||
system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./default.nix { };
|
||||
});
|
||||
|
||||
overlays.default = final: _prev: {
|
||||
imphnen-backend = final.callPackage ./default.nix { };
|
||||
};
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in {
|
||||
packages = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./default.nix {};
|
||||
});
|
||||
devShells = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./shell.nix {};
|
||||
});
|
||||
dockerImages = forAllSystems (system: {
|
||||
tryOutApi = (pkgsFor system).callPackage ./docker.nix {};
|
||||
});
|
||||
};
|
||||
|
||||
nixosModules.backend = ./nixos-module.nix;
|
||||
|
||||
devShells = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./shell.nix { };
|
||||
});
|
||||
|
||||
dockerImages = forAllSystems (system: {
|
||||
tryOutApi = (pkgsFor system).callPackage ./docker.nix { };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
+11
-10
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imphnen-backend"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
@@ -8,8 +8,8 @@ name = "api"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "clear_db_test"
|
||||
path = "src/bin/clear_db_test.rs"
|
||||
name = "create_schema"
|
||||
path = "src/bin/create_schema.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seeder"
|
||||
@@ -40,16 +40,22 @@ name = "seed_roles_permissions"
|
||||
path = "src/bin/seed_roles_permissions.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_users"
|
||||
path = "src/bin/seed_users.rs"
|
||||
name = "seed_test_data"
|
||||
path = "src/bin/seed_test_data.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "test_postgres"
|
||||
path = "src/bin/test_postgres.rs"
|
||||
|
||||
[dependencies]
|
||||
sea-orm.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-gateway.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-gacha.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -57,9 +63,7 @@ serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
axum-test.workspace = true
|
||||
surrealdb.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -70,6 +74,3 @@ env_logger.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace=true
|
||||
tokio-tungstenite.workspace = true
|
||||
url.workspace = true
|
||||
futures-util.workspace = true
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
use axum::Router;
|
||||
use imphnen_gateway::gateway_service;
|
||||
use imphnen_libs::axum_init;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||
let app = gateway_service(surrealdb_ws, surrealdb_mem).await;
|
||||
let mut router = Router::new();
|
||||
router = router.nest("/api/v1/auth", imphnen_iam::v1::auth::auth_router());
|
||||
app.merge(router)
|
||||
axum_init(|postgres_db| async {
|
||||
gateway_service(postgres_db).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,76 @@
|
||||
#![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, "events", common::events::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "testimonials", common::testimonials::Entity)
|
||||
.await?;
|
||||
drop_and_create_table(&db, builder, "audit_logs", common::audit_log::Entity)
|
||||
.await?;
|
||||
drop_and_create_table(&db, builder, "rate_limits", common::rate_limit::Entity)
|
||||
.await?;
|
||||
|
||||
drop_and_create_table(&db, builder, "gacha_credits", gacha::gacha_credits::Entity)
|
||||
.await?;
|
||||
drop_and_create_table(&db, builder, "gacha_items", gacha::gacha_items::Entity)
|
||||
.await?;
|
||||
drop_and_create_table(&db, builder, "gacha_rolls", gacha::gacha_rolls::Entity)
|
||||
.await?;
|
||||
drop_and_create_table(&db, builder, "gacha_claims", gacha::gacha_claims::Entity)
|
||||
.await?;
|
||||
|
||||
println!("✅ Schema creation completed.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn drop_and_create_table<E>(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
builder: DbBackend,
|
||||
name: &str,
|
||||
entity: E,
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_libs::jsonwebtoken::encode_access_token;
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: mk_token <email_or_sub>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let sub = args[1].clone();
|
||||
match encode_access_token(sub.clone(), sub.clone()) {
|
||||
Ok(token) => println!("{}", token),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to generate token: {:?}", e);
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||
use imphnen_utils::{get_iso_date};
|
||||
#![allow(clippy::all)]
|
||||
|
||||
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 surrealdb::engine::any;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::enviroment::ENV;
|
||||
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 postgres_config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let events = vec![
|
||||
(
|
||||
@@ -58,6 +58,96 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"2025-09-20T13: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 (
|
||||
@@ -67,29 +157,41 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
price,
|
||||
location,
|
||||
is_online,
|
||||
start_date,
|
||||
end_date,
|
||||
start_date_str,
|
||||
end_date_str,
|
||||
) in events
|
||||
{
|
||||
let uuid = Uuid::new_v4().to_string(); // Generate new UUID
|
||||
let event = EventsSchema {
|
||||
id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
detail_link: detail_link.into(),
|
||||
price,
|
||||
location,
|
||||
is_online,
|
||||
is_deleted: false,
|
||||
start_date: start_date.into(),
|
||||
end_date: end_date.into(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
|
||||
db.create::<Option<EventsSchema>>(("app_events", uuid.as_str())) // Use generated UUID
|
||||
.content(event)
|
||||
let existing = EventEntity::find()
|
||||
.filter(<EventEntity as EntityTrait>::Column::Name.eq(name))
|
||||
.one(db)
|
||||
.await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
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!(
|
||||
"✅ Inserted event: {} ({})",
|
||||
|
||||
@@ -1,52 +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 surrealdb::opt::auth::Root;
|
||||
use surrealdb::sql::Thing;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::enviroment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
db.query("DELETE type::thing('app_gacha_items', $id)")
|
||||
.bind(("id", "gacha_item_test_id"))
|
||||
.await?;
|
||||
db.query("DELETE type::thing('app_gacha_rolls', $id)")
|
||||
.bind(("id", "gacha_roll_test_id"))
|
||||
let check_item_sql =
|
||||
"SELECT id FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1' LIMIT 1";
|
||||
let item_result = pg_conn
|
||||
.query_one(sea_orm::Statement::from_string(
|
||||
db.get_database_backend(),
|
||||
check_item_sql,
|
||||
))
|
||||
.await?;
|
||||
let gacha_item_uuid = if let Some(ref row) = item_result {
|
||||
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 gacha_item_id = "gacha_item_test_id";
|
||||
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", gacha_item_id))
|
||||
.bind(("name", "Test Gacha Item"))
|
||||
.bind(("image_url", "https://example.com/gacha_item.png"))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("Gacha Item seeded successfully!");
|
||||
let new_uuid = Uuid::new_v4();
|
||||
let mut item_model: GachaItemActiveModel = Default::default();
|
||||
item_model.id = Set(new_uuid);
|
||||
item_model.item_code = Set("ITEM_TEST_1".to_string());
|
||||
item_model.name = Set("Test Gacha Item".to_string());
|
||||
item_model.description = Set("Test item for gacha".to_string());
|
||||
item_model.rarity = Set("common".to_string());
|
||||
item_model.type_ = Set("item".to_string());
|
||||
item_model.category = Set("test".to_string());
|
||||
item_model.value = Set(1);
|
||||
item_model.weight = Set(1.0);
|
||||
item_model.stock = Set(10);
|
||||
item_model.is_limited = Set(false);
|
||||
item_model.created_at = Set(chrono::Utc::now());
|
||||
item_model.updated_at = Set(chrono::Utc::now());
|
||||
item_model.insert(db).await?;
|
||||
println!("Gacha Item seeded successfully!");
|
||||
new_uuid
|
||||
};
|
||||
|
||||
let gacha_roll_id = "gacha_roll_test_id";
|
||||
db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", gacha_roll_id))
|
||||
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
|
||||
.bind(("quantity", 10))
|
||||
.bind(("weight", 1.0))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
let gacha_roll_id = Uuid::new_v4();
|
||||
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||
roll_model.id = Set(gacha_roll_id);
|
||||
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||
roll_model.item_id = Set(gacha_item_uuid);
|
||||
roll_model.weight = Set(1.0);
|
||||
roll_model.quantity = Set(10);
|
||||
roll_model.is_deleted = Set(false);
|
||||
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.insert(db).await?;
|
||||
println!("Gacha Roll seeded successfully!");
|
||||
|
||||
let gacha_roll_id = Uuid::new_v4();
|
||||
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||
roll_model.id = Set(gacha_roll_id);
|
||||
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||
roll_model.item_id = Set(gacha_item_uuid);
|
||||
roll_model.weight = Set(1.0);
|
||||
roll_model.quantity = Set(10);
|
||||
roll_model.is_deleted = Set(false);
|
||||
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.insert(db).await?;
|
||||
println!("✅ Gacha items and rolls seeded.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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 std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::enviroment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
db.query("DELETE type::thing('app_mentors', $id)")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.await?;
|
||||
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.await?;
|
||||
|
||||
use surrealdb::sql::Thing;
|
||||
db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, legal_name = $legal_name, domicile = $domicile, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, last_education = $last_education, linkedin_url = $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")),
|
||||
let _ = pg_conn
|
||||
.execute(sea_orm::Statement::from_string(
|
||||
db.get_database_backend(),
|
||||
"DELETE FROM app_mentors WHERE id = 'e6f78d23-83bf-5c2b-bcd4-001345678901'"
|
||||
.to_string(),
|
||||
))
|
||||
.await?;
|
||||
println!("User updated with mentor_id successfully!");
|
||||
.await
|
||||
.ok();
|
||||
let _ = pg_conn
|
||||
.execute(sea_orm::Statement::from_string(
|
||||
db.get_database_backend(),
|
||||
"DELETE FROM app_users WHERE email = 'mentor@example.com'".to_string(),
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
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!("✅ 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_utils::{get_iso_date};
|
||||
use serde_json::json;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::enviroment::ENV;
|
||||
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 config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
for permission in [
|
||||
PermissionsEnum::ReadListUsers,
|
||||
@@ -54,19 +52,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
PermissionsEnum::UpdateMentors,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
PermissionsEnum::Administrator,
|
||||
] {
|
||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||
.bind(("id", permission.id()))
|
||||
.bind((
|
||||
"data",
|
||||
json!({
|
||||
"name": permission.to_string(),
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date()
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
let parsed_id =
|
||||
Uuid::parse_str(&permission.id()).unwrap_or_else(|_| Uuid::new_v4());
|
||||
|
||||
let existing = PermissionEntity::find_by_id(parsed_id).one(db).await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {permission}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut perm_model: PermissionActiveModel = Default::default();
|
||||
perm_model.id = Set(parsed_id);
|
||||
perm_model.name = Set(permission.to_string());
|
||||
perm_model.is_deleted = Set(false);
|
||||
perm_model.created_at = Set(Utc::now());
|
||||
perm_model.updated_at = Set(Utc::now());
|
||||
perm_model.insert(db).await?;
|
||||
println!("✅ Inserted: {permission}");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
use imphnen_utils::{get_iso_date};
|
||||
use serde_json::json;
|
||||
use chrono::Utc;
|
||||
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 surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::enviroment::ENV;
|
||||
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 config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let roles = vec![
|
||||
(
|
||||
@@ -26,7 +21,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
(
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
"User",
|
||||
None,
|
||||
Some("2025-02-28T14:53:58.576688+00"),
|
||||
Some("2025-02-28T14:53:58.576688+00"),
|
||||
),
|
||||
(
|
||||
@@ -44,34 +39,39 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||
"Admin",
|
||||
None,
|
||||
Some("2025-02-22T15:38:39.868306+00"),
|
||||
Some("2025-02-22T15:38:39.868306+00"),
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
"Mentor",
|
||||
None,
|
||||
Some("2025-07-06T10:00:00.000000+00"),
|
||||
Some("2025-07-06T10:00:00.000000+00"),
|
||||
),
|
||||
];
|
||||
|
||||
for (id, name, _created_at, _updated_at) in roles {
|
||||
db.query("DELETE type::thing('app_roles', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
|
||||
.bind(("id", id))
|
||||
.bind((
|
||||
"data",
|
||||
json!({
|
||||
"name": name,
|
||||
"permissions": [],
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date(),
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
for (id, name, _created_at_str, _updated_at_str) in roles {
|
||||
let uuid = Uuid::parse_str(id).unwrap_or_else(|_| Uuid::new_v4());
|
||||
|
||||
let existing = RoleEntity::find_by_id(uuid).one(db).await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let role_model = RoleBuilder::new()
|
||||
.name(name.to_string())
|
||||
.description("System generated role".to_string())
|
||||
.permissions(vec![])
|
||||
.is_default(false)
|
||||
.build()?;
|
||||
let mut role_model = role_model;
|
||||
role_model.id = Set(uuid);
|
||||
role_model.is_system_role = Set(true);
|
||||
role_model.created_at = Set(Utc::now());
|
||||
role_model.updated_at = Set(Utc::now());
|
||||
|
||||
role_model.insert(db).await?;
|
||||
println!("✅ Inserted role: {name}");
|
||||
}
|
||||
println!("✅ All Roles seeded");
|
||||
|
||||
@@ -1,71 +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 surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::enviroment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
db.query("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 config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
println!(
|
||||
"✅ Index 'user_email_index' defined on table 'users' for column 'email'."
|
||||
);
|
||||
|
||||
let roles_permissions = vec![
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::CreateUsers,
|
||||
PermissionsEnum::DeleteUsers,
|
||||
PermissionsEnum::UpdateUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::CreateRoles,
|
||||
PermissionsEnum::DeleteRoles,
|
||||
PermissionsEnum::UpdateRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::CreatePermissions,
|
||||
PermissionsEnum::DeletePermissions,
|
||||
PermissionsEnum::UpdatePermissions,
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
PermissionsEnum::CreateGachaItems,
|
||||
PermissionsEnum::DeleteGachaItems,
|
||||
PermissionsEnum::UpdateGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailMentors,
|
||||
PermissionsEnum::RegisterMentors,
|
||||
PermissionsEnum::UpdateMentors,
|
||||
PermissionsEnum::VerifyMentors,
|
||||
PermissionsEnum::DeleteMentors,
|
||||
],
|
||||
vec![PermissionsEnum::Administrator],
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::UpdateOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
@@ -100,13 +61,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
(
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||
vec![
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadListMentors,
|
||||
PermissionsEnum::ReadDetailUsers,
|
||||
PermissionsEnum::ActivateUsers,
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadDetailRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadDetailPermissions,
|
||||
PermissionsEnum::ReadListGachaItems,
|
||||
PermissionsEnum::ReadDetailGachaItems,
|
||||
@@ -125,17 +86,22 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
];
|
||||
|
||||
for (role_id, permissions) in roles_permissions {
|
||||
let permission_refs: Vec<_> = permissions
|
||||
.iter()
|
||||
.map(|perm| make_thing("app_permissions", perm.id()))
|
||||
.collect();
|
||||
let role_uuid = Uuid::parse_str(role_id).unwrap_or_else(|_| Uuid::new_v4());
|
||||
let json_permissions = JsonValue::Array(
|
||||
permissions
|
||||
.iter()
|
||||
.map(|p| JsonValue::String(p.id()))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||
.bind(("role_id", role_id))
|
||||
.bind(("permissions", permission_refs))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
println!("✅ Permissions updated for role: {role_id}");
|
||||
if let Some(role_model) = RolesEntity::find_by_id(role_uuid).one(db).await? {
|
||||
let mut am: RoleActiveModel = role_model.into();
|
||||
am.permissions = Set(Some(json_permissions));
|
||||
am.update(db).await?;
|
||||
println!("✅ Permissions updated for role: {role_id}");
|
||||
} else {
|
||||
println!("⚠️ Role with id {role_id} not found, skipping permissions update");
|
||||
}
|
||||
}
|
||||
|
||||
println!("✅ All roles permissions updated!");
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
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]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
let mut event_model: EventsActiveModel = Default::default();
|
||||
event_model.id = Set(Uuid::parse_str(&uuid)?);
|
||||
event_model.name = Set("Test Event".to_string());
|
||||
event_model.description = Set("Test event description".to_string());
|
||||
event_model.detail_link = Set("https://example.com/event".to_string());
|
||||
event_model.price = Set(50.0);
|
||||
event_model.is_online = Set(true);
|
||||
event_model.start_date = Set(Utc::now());
|
||||
event_model.end_date = Set(Utc::now() + chrono::Duration::days(1));
|
||||
event_model.location = Set(None);
|
||||
event_model.is_deleted = Set(false);
|
||||
match event_model.insert(db).await {
|
||||
Ok(_) => println!("✅ Inserted test event"),
|
||||
Err(_) => {
|
||||
println!("⚠️ Test event already exists or could not be inserted, skipping")
|
||||
}
|
||||
};
|
||||
|
||||
let mut testimonial_model: TestimonialsActiveModel = Default::default();
|
||||
testimonial_model.id =
|
||||
Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
||||
testimonial_model.user_id =
|
||||
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
testimonial_model.role = Set("Student".to_string());
|
||||
testimonial_model.content = Set("This is a great platform!".to_string());
|
||||
testimonial_model.is_deleted = Set(false);
|
||||
match testimonial_model.insert(db).await {
|
||||
Ok(_) => println!("✅ Inserted test testimonial"),
|
||||
Err(_) => println!(
|
||||
"⚠️ Test testimonial already exists or could not be inserted, skipping"
|
||||
),
|
||||
};
|
||||
|
||||
let mentor_id = Uuid::new_v4();
|
||||
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||
mentor_model.id = Set(mentor_id);
|
||||
mentor_model.user_id =
|
||||
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
mentor_model.industries = Set(Some(json!(["Technology", "Education"])));
|
||||
mentor_model.expertise = Set(Some(json!(["Software Development"])));
|
||||
mentor_model.languages = Set(Some(json!(["English", "Indonesian"])));
|
||||
mentor_model.current_company = Set(Some("Tech Corp".to_string()));
|
||||
mentor_model.current_role = Set(Some("Senior Engineer".to_string()));
|
||||
mentor_model.years_of_experience = Set(Some(5));
|
||||
mentor_model.topics_of_interest = Set(Some(json!(["Rust", "Web Development"])));
|
||||
mentor_model.preferred_mentee_level = Set(Some("Beginner".to_string()));
|
||||
mentor_model.preferred_mentoring_formats = Set(Some(json!(["1:1", "Group"])));
|
||||
mentor_model.availability_commitment = Set(Some("Weekly".to_string()));
|
||||
mentor_model.mentoring_rate = Set(Some(100.0));
|
||||
mentor_model.status = Set(Some("active".to_string()));
|
||||
mentor_model.is_deleted = Set(false);
|
||||
mentor_model.created_at = Set(chrono::Utc::now());
|
||||
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||
mentor_model.insert(db).await?;
|
||||
println!("✅ Inserted test mentor via SeaORM");
|
||||
|
||||
println!("✅ All test data seeded successfully");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
use imphnen_iam::UsersSchema;
|
||||
use imphnen_utils::{get_iso_date, hash_password};
|
||||
#![allow(clippy::all)]
|
||||
|
||||
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 uuid::Uuid;
|
||||
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::enviroment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let postgres_config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let users = vec![
|
||||
(
|
||||
@@ -36,54 +35,130 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"User",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"665a3cfc-ea5f-4bcd-8769-4a6d8d1451d4",
|
||||
"testuser1@example.com",
|
||||
"Test User 1",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"3972c139-a450-416c-93b0-c42539dc780f",
|
||||
"testuser2@example.com",
|
||||
"Test User 2",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"b426c0a9-0efb-4e26-b078-4f18767255f3",
|
||||
"testuser3@example.com",
|
||||
"Test User 3",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"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 {
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
for (id, email, fullname, role_id_str) in users {
|
||||
let role_uuid = Some(
|
||||
Uuid::parse_str(role_id_str)
|
||||
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?,
|
||||
);
|
||||
|
||||
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("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(),
|
||||
let uid = Uuid::parse_str(id)?;
|
||||
|
||||
let names: Vec<&str> = fullname.split_whitespace().collect();
|
||||
let first_name = names.first().map(|s| s.to_string());
|
||||
let last_name = if names.len() > 1 {
|
||||
Some(names[1..].join(" "))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
db.create::<Option<UsersSchema>>(("app_users", id))
|
||||
.content(user)
|
||||
.await?;
|
||||
let password = "password";
|
||||
let hashed = hash_password(password).unwrap();
|
||||
|
||||
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");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
|
||||
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
||||
println!("🔧 Seeding: {bin}");
|
||||
let status = Command::new("cargo").args(["run", "--bin", bin]).status()?;
|
||||
|
||||
if !status.success() {
|
||||
Err(format!("❌ Failed to run seed: {bin}").into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
println!("🚀 Running all seeders...\n");
|
||||
|
||||
run_seed("seed_permissions")?;
|
||||
run_seed("seed_roles")?;
|
||||
run_seed("seed_roles_permissions")?;
|
||||
run_seed("seed_users")?;
|
||||
run_seed("seed_events")?;
|
||||
run_seed("seed_gacha_rolls")?;
|
||||
run_seed("seed_mentor_user")?;
|
||||
println!("\n✅ All seeding completed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
|
||||
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
||||
println!("🔧 Seeding: {bin}");
|
||||
#[cfg(target_os = "windows")]
|
||||
let status = Command::new(format!("./target/release/{}.exe", bin)).status()?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let status = Command::new(format!("./target/release/{}", bin)).status()?;
|
||||
|
||||
if !status.success() {
|
||||
Err(format!("❌ Failed to run seed: {bin}").into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
println!("🚀 Running all seeders...\n");
|
||||
|
||||
run_seed("seed_permissions")?;
|
||||
run_seed("seed_roles")?;
|
||||
run_seed("seed_roles_permissions")?;
|
||||
run_seed("seed_users")?;
|
||||
run_seed("seed_events")?;
|
||||
run_seed("seed_gacha_rolls")?;
|
||||
run_seed("seed_mentor_user")?;
|
||||
run_seed("seed_test_data")?;
|
||||
println!("\n✅ All seeding completed successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||
})
|
||||
.await;
|
||||
let _ =
|
||||
axum_init(|postgres_conn| async { gateway_service(postgres_conn).await }).await;
|
||||
}
|
||||
|
||||
+14
-3
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "imphnen-cms"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
@@ -14,9 +15,9 @@ serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
zod-rs.workspace = true
|
||||
zod-rs-util.workspace = true
|
||||
axum-test.workspace = true
|
||||
surrealdb.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -25,6 +26,16 @@ tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
paginator-rs.workspace = true
|
||||
paginator-utils.workspace = true
|
||||
paginator-sea-orm.workspace = true
|
||||
paginator-axum.workspace = true
|
||||
sqlx.workspace = true
|
||||
image.workspace = true
|
||||
qrcode.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
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,2 +1,9 @@
|
||||
pub mod v1;
|
||||
pub use v1::*;
|
||||
pub mod events;
|
||||
pub mod roadmap;
|
||||
pub mod testimonials;
|
||||
pub mod qr;
|
||||
|
||||
pub use events::{events_protected_routes, events_public_routes};
|
||||
pub use roadmap::{roadmap_protected_routes, roadmap_public_routes};
|
||||
pub use testimonials::{testimonials_protected_routes, testimonials_public_routes};
|
||||
pub use qr::qr_router;
|
||||
|
||||
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user