Compare commits
30
Commits
v0.2.0
..
9b5efeff87
+23
-24
@@ -1,26 +1,21 @@
|
|||||||
RUST_ENV=development
|
RUST_ENV=development
|
||||||
RUST_LOG=debug
|
RUST_LOG=debug
|
||||||
PORT=4099
|
PORT=4099
|
||||||
SURREALDB_URL=ws://localhost:8000/rpc
|
ACCESS_TOKEN_SECRET=your-access-token-secret-key-here
|
||||||
SURREALDB_USERNAME=root
|
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
||||||
SURREALDB_PASSWORD=root
|
SMTP_EMAIL=your-email@example.com
|
||||||
SURREALDB_NAMESPACE=test
|
SMTP_PASSWORD=your-smtp-password
|
||||||
SURREALDB_DBNAME=test
|
SMTP_NAME="Your App Name"
|
||||||
ACCESS_TOKEN_SECRET=your-access-token-secret-key-here
|
SMTP_HOST=smtp.gmail.com
|
||||||
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
REDISDB_URL=localhost
|
||||||
SMTP_EMAIL=your-email@example.com
|
FE_URL=http://localhost
|
||||||
SMTP_PASSWORD=your-smtp-password
|
MINIO_ENDPOINT=http://localhost:9000
|
||||||
SMTP_NAME="Your App Name"
|
MINIO_BUCKET_NAME=default_bucket
|
||||||
SMTP_HOST=smtp.gmail.com
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
REDISDB_URL=localhost
|
MINIO_SECRET_KEY=minioadmin
|
||||||
FE_URL=http://localhost
|
MINIO_SECURE=false
|
||||||
MINIO_ENDPOINT=http://localhost:9000
|
|
||||||
MINIO_BUCKET_NAME=default_bucket
|
GOOGLE_CLIENT_ID="your_google_client_id"
|
||||||
MINIO_ACCESS_KEY=minioadmin
|
|
||||||
MINIO_SECRET_KEY=minioadmin
|
|
||||||
MINIO_SECURE=false
|
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID="your_google_client_id"
|
|
||||||
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
||||||
POOL_SIZE=10
|
POOL_SIZE=10
|
||||||
CONNECT_TIMEOUT=30
|
CONNECT_TIMEOUT=30
|
||||||
@@ -31,4 +26,8 @@ IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000
|
|||||||
SSLMODE=require
|
SSLMODE=require
|
||||||
RETRY_ATTEMPTS=3
|
RETRY_ATTEMPTS=3
|
||||||
RETRY_DELAY=1
|
RETRY_DELAY=1
|
||||||
GOOGLE_REDIRECT_URL=http://localhost:8000/api/v1/auth/google/callback
|
GOOGLE_REDIRECT_URL=http://localhost:8000/api/v1/auth/google/callback
|
||||||
|
|
||||||
|
CDN_URL=https://cdn.asepharyana.tech
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://gacha.imphnen.dev,https://imphnen.dev,https://dimentorin.imphnen.dev
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
name: Deploy to Ancikri
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Build the project
|
|
||||||
run: cargo build --release
|
|
||||||
|
|
||||||
- name: Stop service on VPS before upload
|
|
||||||
uses: appleboy/ssh-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
|
||||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
|
||||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
|
||||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
echo "Stopping the service before uploading the binary"
|
|
||||||
sudo systemctl stop imphnen-backend-service
|
|
||||||
|
|
||||||
- name: Upload artifact to VPS
|
|
||||||
uses: appleboy/scp-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
|
||||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
|
||||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
|
||||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
|
||||||
source: ./target/release/*
|
|
||||||
target: /opt/imphnen-backend-service/imphnen-backend-service
|
|
||||||
rm: true
|
|
||||||
overwrite: true
|
|
||||||
|
|
||||||
- name: Deploy to server
|
|
||||||
uses: appleboy/ssh-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
|
||||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
|
||||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
|
||||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
|
||||||
script: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Restarting the service"
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
|
|
||||||
sudo systemctl restart imphnen-backend-service
|
|
||||||
|
|
||||||
echo "Deployment completed successfully"
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: Nix Build & Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['develop']
|
||||||
|
pull_request:
|
||||||
|
branches: ['develop']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup Cachix
|
||||||
|
uses: cachix/cachix-action@v15
|
||||||
|
with:
|
||||||
|
name: msdqn
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: nix build .#default -o result
|
||||||
|
|
||||||
|
- name: Push to Cachix
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||||
|
run: cachix push msdqn result
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/develop'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
env:
|
||||||
|
INFRA_DEPLOY_KEY: ${{ secrets.INFRA_DEPLOY_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${INFRA_DEPLOY_KEY}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
ssh-keyscan 167.235.70.37 >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Update infra flake.lock
|
||||||
|
run: |
|
||||||
|
export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes"
|
||||||
|
git clone git@github.com:IMPHNEN/imphnen-infrastructure.git /tmp/infra
|
||||||
|
cd /tmp/infra
|
||||||
|
nix flake update imphnen-backend
|
||||||
|
if git diff --quiet flake.lock; then
|
||||||
|
echo "flake.lock unchanged, skipping"
|
||||||
|
else
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "chore: update imphnen-backend-service to ${GITHUB_SHA::7}"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key \
|
||||||
|
-o ConnectTimeout=30 \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
-o ServerAliveInterval=30 \
|
||||||
|
-o ServerAliveCountMax=40 \
|
||||||
|
root@167.235.70.37 \
|
||||||
|
'nixos-rebuild switch --flake github:IMPHNEN/imphnen-infrastructure#hetzner --refresh 2>&1 | tail -50'
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: rm -f ~/.ssh/deploy_key
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
name: Rust
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["develop"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["develop"]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --verbose
|
|
||||||
Generated
+680
-17
File diff suppressed because it is too large
Load Diff
+22
-12
@@ -1,17 +1,20 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"imphnen-entities", # Most basic - core data structures
|
"imphnen-entities",
|
||||||
"imphnen-macros", # Macros
|
"imphnen-macros",
|
||||||
"imphnen-libs", # Depends on entities
|
"imphnen-libs",
|
||||||
"imphnen-utils", # Depends on libs and entities
|
"imphnen-storage",
|
||||||
"imphnen-middleware",# Utility for permissions
|
"imphnen-email",
|
||||||
"imphnen-iam", # Core auth service, depends on libs, utils, entities
|
"imphnen-utils",
|
||||||
"imphnen-cms", # Content management, depends on core services
|
"imphnen-middleware",
|
||||||
"imphnen-gacha", # Game mechanics, depends on core services
|
"imphnen-iam",
|
||||||
"imphnen-dimentorin",# Learning platform, depends on core services
|
"imphnen-cms",
|
||||||
"imphnen-gateway", # API gateway, depends on all services
|
"imphnen-gacha",
|
||||||
"imphnen-backend", # Main application, depends on all services
|
"imphnen-dimentorin",
|
||||||
|
"imphnen-hackathon",
|
||||||
|
"imphnen-gateway",
|
||||||
|
"imphnen-backend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ tokio = { version = "1.47.1", features = ["full"] }
|
|||||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||||
jsonwebtoken = "9.3.1"
|
jsonwebtoken = "9.3.1"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
utoipa = { version = "5.4.0", features = ["axum_extras", "uuid", "chrono"] }
|
||||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||||
thiserror = "2.0.14"
|
thiserror = "2.0.14"
|
||||||
@@ -63,6 +66,7 @@ hyper = "1.6.0"
|
|||||||
hyper-util = "0.1.16"
|
hyper-util = "0.1.16"
|
||||||
minio = "0.3.0"
|
minio = "0.3.0"
|
||||||
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros", "with-chrono", "uuid"] }
|
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"
|
num_cpus = "1.16.0"
|
||||||
|
|
||||||
|
|
||||||
@@ -86,6 +90,12 @@ imphnen-entities = { path = "./imphnen-entities" }
|
|||||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||||
imphnen-macros = { path = "./imphnen-macros" }
|
imphnen-macros = { path = "./imphnen-macros" }
|
||||||
|
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
||||||
|
imphnen-storage = { path = "./imphnen-storage" }
|
||||||
|
imphnen-email = { path = "./imphnen-email" }
|
||||||
|
bcrypt = "0.15"
|
||||||
|
image = { version = "0.25", features = ["png", "jpeg"] }
|
||||||
|
qrcode = { version = "0.14", default-features = false, features = ["image"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
|
|||||||
+55
-47
@@ -1,47 +1,55 @@
|
|||||||
FROM rust:1.86-alpine AS builder
|
FROM rust:1.86-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
curl \
|
curl \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
openssl-dev \
|
openssl-dev \
|
||||||
openssl-libs-static \
|
openssl-libs-static \
|
||||||
pkgconfig
|
pkgconfig
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
RUN mkdir -p imphnen-backend/src imphnen-cms/src imphnen-dimentorin/src \
|
RUN mkdir -p \
|
||||||
imphnen-entities/src imphnen-gacha/src imphnen-gateway/src \
|
imphnen-backend/src \
|
||||||
imphnen-iam/src imphnen-libs/src imphnen-middleware/src \
|
imphnen-cms/src \
|
||||||
imphnen-utils/src tests/src && \
|
imphnen-dimentorin/src \
|
||||||
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
imphnen-email/src \
|
||||||
find . -name "src" -type d -exec sh -c 'echo "// dummy" > "$1/lib.rs"' _ {} \;
|
imphnen-entities/src \
|
||||||
|
imphnen-gacha/src \
|
||||||
RUN echo '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
imphnen-gateway/src \
|
||||||
|
imphnen-hackathon/src \
|
||||||
|
imphnen-iam/src \
|
||||||
RUN echo -e '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
imphnen-libs/src \
|
||||||
|
imphnen-macros/src \
|
||||||
|
imphnen-middleware/src \
|
||||||
COPY imphnen-backend ./imphnen-backend
|
imphnen-storage/src \
|
||||||
COPY imphnen-cms ./imphnen-cms
|
imphnen-utils/src && \
|
||||||
COPY imphnen-dimentorin ./imphnen-dimentorin
|
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
||||||
COPY imphnen-entities ./imphnen-entities
|
find . -name "src" -type d -exec sh -c 'touch "$1/lib.rs"' _ {} \;
|
||||||
COPY imphnen-gacha ./imphnen-gacha
|
|
||||||
COPY imphnen-gateway ./imphnen-gateway
|
COPY imphnen-backend ./imphnen-backend
|
||||||
COPY imphnen-iam ./imphnen-iam
|
COPY imphnen-cms ./imphnen-cms
|
||||||
COPY imphnen-libs ./imphnen-libs
|
COPY imphnen-dimentorin ./imphnen-dimentorin
|
||||||
COPY imphnen-middleware ./imphnen-middleware
|
COPY imphnen-email ./imphnen-email
|
||||||
COPY imphnen-utils ./imphnen-utils
|
COPY imphnen-entities ./imphnen-entities
|
||||||
COPY tests ./tests
|
COPY imphnen-gacha ./imphnen-gacha
|
||||||
|
COPY imphnen-gateway ./imphnen-gateway
|
||||||
RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-units=1 -C strip=symbols" \
|
COPY imphnen-hackathon ./imphnen-hackathon
|
||||||
cargo build -p imphnen-backend --release && \
|
COPY imphnen-iam ./imphnen-iam
|
||||||
strip target/release/api && \
|
COPY imphnen-libs ./imphnen-libs
|
||||||
upx --best --lzma target/release/api 2>/dev/null || true
|
COPY imphnen-macros ./imphnen-macros
|
||||||
|
COPY imphnen-middleware ./imphnen-middleware
|
||||||
FROM scratch AS runner
|
COPY imphnen-storage ./imphnen-storage
|
||||||
COPY --from=builder /app/target/release/api /api
|
COPY imphnen-utils ./imphnen-utils
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
ENTRYPOINT ["/api"]
|
RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-units=1 -C strip=symbols" \
|
||||||
|
cargo build -p imphnen-backend --release && \
|
||||||
|
strip target/release/api && \
|
||||||
|
upx --best --lzma target/release/api 2>/dev/null || true
|
||||||
|
|
||||||
|
FROM scratch AS runner
|
||||||
|
COPY --from=builder /app/target/release/api /api
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
ENTRYPOINT ["/api"]
|
||||||
|
|||||||
+25
-15
@@ -1,15 +1,25 @@
|
|||||||
{pkgs ? import <nixpkgs> {}}: let
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
let
|
||||||
rustDeps = pkgs.callPackage ./Cargo.nix {};
|
swaggerUi = pkgs.fetchurl {
|
||||||
packageEntry = rustDeps.workspaceMembers.${manifest.name};
|
url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip";
|
||||||
deps = packageEntry.build.cargoDeps or null;
|
hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw=";
|
||||||
in
|
};
|
||||||
pkgs.rustPlatform.buildRustPackage {
|
in
|
||||||
pname = manifest.name;
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
version = manifest.version;
|
pname = "imphnen-backend";
|
||||||
cargoDeps = deps;
|
version = (pkgs.lib.importTOML ./imphnen-backend/Cargo.toml).package.version;
|
||||||
src = pkgs.lib.cleanSource ./.;
|
src = pkgs.lib.cleanSource ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
nativeBuildInputs = [pkgs.openssl pkgs.pkg-config];
|
cargoBuildFlags = [
|
||||||
buildInputs = [pkgs.openssl];
|
"--package"
|
||||||
}
|
"imphnen-backend"
|
||||||
|
"--bin"
|
||||||
|
"api"
|
||||||
|
];
|
||||||
|
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||||
|
buildInputs = [ pkgs.openssl ];
|
||||||
|
preBuild = ''
|
||||||
|
export SWAGGER_UI_DOWNLOAD_URL="file://${swaggerUi}"
|
||||||
|
'';
|
||||||
|
doCheck = false;
|
||||||
|
}
|
||||||
|
|||||||
Generated
+27
-27
@@ -1,27 +1,27 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739020877,
|
"lastModified": 1775036866,
|
||||||
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
"version": 7
|
"version": 7
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,47 @@
|
|||||||
{
|
{
|
||||||
description = "IMPHNEN Backend Service Nix Flake";
|
description = "IMPHNEN Backend Service Nix Flake";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
supportedSystems = [
|
supportedSystems = [
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
"x86_64-darwin"
|
"x86_64-darwin"
|
||||||
"aarch64-darwin"
|
"aarch64-darwin"
|
||||||
"aarch64-linux"
|
"aarch64-linux"
|
||||||
];
|
];
|
||||||
pkgsFor =
|
pkgsFor =
|
||||||
system:
|
system:
|
||||||
import nixpkgs {
|
import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config = {
|
config.allowUnfree = true;
|
||||||
allowUnfree = true;
|
};
|
||||||
};
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
};
|
in
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
{
|
||||||
in
|
packages = forAllSystems (system: {
|
||||||
{
|
default = (pkgsFor system).callPackage ./default.nix { };
|
||||||
packages = forAllSystems (system: {
|
});
|
||||||
default = (pkgsFor system).callPackage ./default.nix { };
|
|
||||||
});
|
overlays.default = final: _prev: {
|
||||||
devShells = forAllSystems (system: {
|
imphnen-backend = final.callPackage ./default.nix { };
|
||||||
default = (pkgsFor system).callPackage ./shell.nix { };
|
};
|
||||||
});
|
|
||||||
dockerImages = forAllSystems (system: {
|
nixosModules.backend = ./nixos-module.nix;
|
||||||
tryOutApi = (pkgsFor system).callPackage ./docker.nix { };
|
|
||||||
});
|
devShells = forAllSystems (system: {
|
||||||
};
|
default = (pkgsFor system).callPackage ./shell.nix { };
|
||||||
}
|
});
|
||||||
|
|
||||||
|
dockerImages = forAllSystems (system: {
|
||||||
|
tryOutApi = (pkgsFor system).callPackage ./docker.nix { };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "imphnen-backend"
|
name = "imphnen-backend"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
// API entry point using PostgreSQL (SurrealDB migration complete)
|
use imphnen_gateway::gateway_service;
|
||||||
// This file has been updated to use SeaORM with PostgreSQL instead of SurrealDB
|
use imphnen_libs::axum_init;
|
||||||
use imphnen_gateway::gateway_service;
|
|
||||||
use imphnen_libs::axum_init;
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
#[tokio::main]
|
axum_init(|postgres_db| async {
|
||||||
async fn main() {
|
gateway_service(postgres_db).await
|
||||||
axum_init(|postgres_db| async {
|
})
|
||||||
// Gateway service now uses PostgreSQL exclusively (SeaORM)
|
.await;
|
||||||
// SurrealDB dependencies have been completely removed
|
}
|
||||||
gateway_service(postgres_db).await
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,93 +1,91 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use sea_orm::{Statement, ConnectionTrait};
|
use sea_orm::{ConnectionTrait, Statement};
|
||||||
use std::error::Error;
|
use std::env;
|
||||||
use std::env;
|
use std::error::Error;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
// New default behavior: execute by default; use --dry-run to preview only.
|
let dry_run = args
|
||||||
let dry_run = args.iter().any(|s| s == "--dry-run" || s == "--no-exec" || s == "--dry");
|
.iter()
|
||||||
let force = args.iter().any(|s| s == "--force" || s == "-f");
|
.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");
|
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");
|
||||||
// List of tables to truncate (order doesn't matter with CASCADE)
|
|
||||||
let tables = vec![
|
let tables = vec![
|
||||||
"gacha_claims",
|
"gacha_claims",
|
||||||
"gacha_rolls",
|
"gacha_rolls",
|
||||||
"gacha_items",
|
"gacha_items",
|
||||||
"gacha_credits",
|
"gacha_credits",
|
||||||
"audit_logs",
|
"audit_logs",
|
||||||
"rate_limits",
|
"rate_limits",
|
||||||
"testimonials",
|
"testimonials",
|
||||||
"events",
|
"events",
|
||||||
"app_mentors",
|
"app_mentors",
|
||||||
"app_sessions",
|
"app_sessions",
|
||||||
"app_roles_permissions",
|
"app_roles_permissions",
|
||||||
"app_permissions",
|
"app_permissions",
|
||||||
"app_roles",
|
"app_roles",
|
||||||
"app_users",
|
"app_users",
|
||||||
];
|
];
|
||||||
|
|
||||||
let postgres_config = PostgresConfig::from_env()?;
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
let db = &pg_conn.conn;
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
// Filter tables that actually exist in the database
|
let mut existing_tables: Vec<&str> = vec![];
|
||||||
let mut existing_tables: Vec<&str> = vec![];
|
for t in tables.iter() {
|
||||||
for t in tables.iter() {
|
let check_sql = format!(
|
||||||
let check_sql = format!(
|
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{}') as exists;",
|
||||||
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{}') as exists;",
|
t
|
||||||
t
|
);
|
||||||
);
|
let stmt = Statement::from_string(db.get_database_backend(), check_sql);
|
||||||
let stmt = Statement::from_string(db.get_database_backend(), check_sql);
|
if let Ok(Some(row)) = pg_conn.query_one(stmt).await {
|
||||||
if let Ok(Some(row)) = pg_conn.query_one(stmt).await {
|
let exists_val: Option<bool> = row.try_get("", "exists").ok();
|
||||||
let exists_val: Option<bool> = row.try_get("", "exists").ok();
|
if exists_val.unwrap_or(false) {
|
||||||
if exists_val.unwrap_or(false) {
|
existing_tables.push(t);
|
||||||
existing_tables.push(t);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if existing_tables.is_empty() {
|
||||||
if existing_tables.is_empty() {
|
println!("No configured tables found to clear - nothing to do.");
|
||||||
println!("No configured tables found to clear - nothing to do.");
|
return Ok(());
|
||||||
return Ok(());
|
}
|
||||||
}
|
|
||||||
|
let truncate_sql = format!(
|
||||||
let truncate_sql = format!(
|
"TRUNCATE TABLE {} RESTART IDENTITY CASCADE;",
|
||||||
"TRUNCATE TABLE {} RESTART IDENTITY CASCADE;",
|
existing_tables.join(", ")
|
||||||
existing_tables.join(", ")
|
);
|
||||||
);
|
|
||||||
|
println!("The script will run the following SQL (on the DB configured by env vars):\n\n{}", truncate_sql);
|
||||||
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();
|
||||||
// Prevent accidental execution in production without explicit force flag
|
if env_name == "production" && !force {
|
||||||
let env_name = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string());
|
println!("Security: RUST_ENV=production; the script will NOT run without --force. Use --force to override.");
|
||||||
if env_name == "production" && !force {
|
return Ok(());
|
||||||
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).");
|
||||||
if dry_run {
|
return Ok(());
|
||||||
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");
|
||||||
|
|
||||||
println!("Executing truncate...\n");
|
let postgres_config = PostgresConfig::from_env()?;
|
||||||
|
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||||
let postgres_config = PostgresConfig::from_env()?;
|
let db = &pg_conn.conn;
|
||||||
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 {
|
||||||
let stmt = Statement::from_string(db.get_database_backend(), truncate_sql);
|
Ok(_) => println!("✅ Successfully cleared DB tables"),
|
||||||
match pg_conn.execute(stmt).await {
|
Err(e) => println!("❌ Failed to clear DB tables: {}", e),
|
||||||
Ok(_) => println!("✅ Successfully cleared DB tables"),
|
}
|
||||||
Err(e) => println!("❌ Failed to clear DB tables: {}", e),
|
|
||||||
}
|
Ok(())
|
||||||
|
}
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,65 +1,78 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use sea_orm::{ConnectionTrait, Database, Schema, DbBackend, EntityTrait};
|
|
||||||
use imphnen_libs::postgres::PostgresConfig;
|
|
||||||
use imphnen_entities::seaorm::{auth, common, gacha};
|
use imphnen_entities::seaorm::{auth, common, gacha};
|
||||||
use sea_orm::sea_query::Table;
|
use imphnen_libs::postgres::PostgresConfig;
|
||||||
|
use sea_orm::sea_query::Table;
|
||||||
|
use sea_orm::{ConnectionTrait, Database, DbBackend, EntityTrait, Schema};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("🛠️ Creating database schema...");
|
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...");
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let db = Database::connect(&config.database_url).await?;
|
||||||
|
let builder = db.get_database_backend();
|
||||||
|
|
||||||
// Dropping and recreating tables to ensure schema is up-to-date
|
println!(" Database connected. Creating/updating tables...");
|
||||||
// This is safer for development/testing environments to prevent schema drift.
|
|
||||||
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.");
|
drop_and_create_table(&db, builder, "app_roles", auth::roles::Entity).await?;
|
||||||
Ok(())
|
drop_and_create_table(&db, builder, "app_permissions", auth::permissions::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_users", auth::users::Entity).await?;
|
||||||
|
drop_and_create_table(
|
||||||
|
&db,
|
||||||
|
builder,
|
||||||
|
"app_roles_permissions",
|
||||||
|
auth::roles_permissions::Entity,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_mentors", auth::mentors::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_sessions", auth::sessions::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "app_articles", common::articles::Entity)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
drop_and_create_table(&db, builder, "events", common::events::Entity).await?;
|
||||||
|
drop_and_create_table(&db, builder, "testimonials", common::testimonials::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "audit_logs", common::audit_log::Entity)
|
||||||
|
.await?;
|
||||||
|
drop_and_create_table(&db, builder, "rate_limits", common::rate_limit::Entity)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
drop_and_create_table(&db, builder, "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>(
|
async fn drop_and_create_table<E>(
|
||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
builder: DbBackend,
|
builder: DbBackend,
|
||||||
name: &str,
|
name: &str,
|
||||||
entity: E,
|
entity: E,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> // Return Result
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
where
|
where
|
||||||
E: EntityTrait,
|
E: EntityTrait,
|
||||||
{
|
{
|
||||||
let schema = Schema::new(builder);
|
let schema = Schema::new(builder);
|
||||||
|
|
||||||
// Drop table if it exists
|
let drop_stmt = Table::drop().table(entity).if_exists().cascade().to_owned();
|
||||||
let drop_stmt = Table::drop().table(entity).if_exists().cascade().to_owned(); // Added .cascade()
|
db.execute(builder.build(&drop_stmt)).await?;
|
||||||
db.execute(builder.build(&drop_stmt)).await?; // Propagate error
|
println!(" Dropped table if exists: {}", name);
|
||||||
println!(" Dropped table if exists: {}", name);
|
|
||||||
|
|
||||||
// Create table
|
let mut create_stmt = schema.create_table_from_entity(entity);
|
||||||
let mut create_stmt = schema.create_table_from_entity(entity);
|
create_stmt.if_not_exists();
|
||||||
create_stmt.if_not_exists();
|
|
||||||
|
|
||||||
db.execute(builder.build(&create_stmt)).await?; // Propagate error
|
db.execute(builder.build(&create_stmt)).await?;
|
||||||
println!(" ✅ Created table: {}", name);
|
println!(" ✅ Created table: {}", name);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use imphnen_libs::jsonwebtoken::encode_access_token;
|
use imphnen_libs::jsonwebtoken::encode_access_token;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
eprintln!("Usage: mk_token <email_or_sub>");
|
eprintln!("Usage: mk_token <email_or_sub>");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
let sub = args[1].clone();
|
let sub = args[1].clone();
|
||||||
// Use sub as both sub and user_id
|
match encode_access_token(sub.clone(), sub.clone()) {
|
||||||
match encode_access_token(sub.clone(), sub.clone()) {
|
Ok(token) => println!("{}", token),
|
||||||
Ok(token) => println!("{}", token),
|
Err(e) => {
|
||||||
Err(e) => {
|
eprintln!("Failed to generate token: {:?}", e);
|
||||||
eprintln!("Failed to generate token: {:?}", e);
|
std::process::exit(2);
|
||||||
std::process::exit(2);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#![allow(clippy::all)]
|
||||||
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::common::articles::{
|
||||||
|
ActiveModel as ArticleActiveModel, Entity as ArticlesEntity,
|
||||||
|
};
|
||||||
|
use imphnen_libs::postgres::PostgresConfig;
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue, Database};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = PostgresConfig::from_env()?;
|
||||||
|
let db = Database::connect(&config.database_url).await?;
|
||||||
|
|
||||||
|
let articles = vec![
|
||||||
|
("Cara Memulai Karier di UI/UX Design", "cara-memulai-karier-ui-ux-design", "UI/UX & Design", "Panduan lengkap untuk masuk ke dunia UI/UX design, dari skill yang dibutuhkan hingga portofolio.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit. UI/UX design adalah bidang yang menjanjikan. Artikel ini membahas langkah awal memulai karier sebagai UI/UX designer, tools yang wajib dikuasai seperti Figma, serta cara membangun portofolio yang menarik bagi perekrut."),
|
||||||
|
("Belajar Rust: Panduan Pemula 2026", "belajar-rust-panduan-pemula-2026", "Software/Web Dev", "Bahasa pemrograman Rust sedang naik daun. Pelajari konsep ownership dan borrow checker.", "Rust adalah bahasa pemrograman yang fokus pada performa dan keamanan memori. Dalam artikel ini kita membahas ownership, borrowing, dan cara setup environment Rust di Linux dan Windows, serta contoh project sederhana."),
|
||||||
|
("Mengenal Machine Learning untuk Data Analyst", "mengenal-machine-learning-data-analyst", "Data & AI", "Peran Data Analyst berevolusi dengan hadirnya machine learning. Simak panduannya.", "Machine learning membuka peluang besar bagi data analyst. Artikel ini menjelaskan perbedaan data analysis dan machine learning, serta roadmap belajar dari Python, pandas, sampai scikit-learn."),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (title, slug, category, excerpt, content) in articles {
|
||||||
|
let am = ArticleActiveModel {
|
||||||
|
id: ActiveValue::Set(Uuid::new_v4()),
|
||||||
|
title: ActiveValue::Set(title.to_string()),
|
||||||
|
slug: ActiveValue::Set(slug.to_string()),
|
||||||
|
category: ActiveValue::Set(category.to_string()),
|
||||||
|
excerpt: ActiveValue::Set(excerpt.to_string()),
|
||||||
|
content: ActiveValue::Set(content.to_string()),
|
||||||
|
cover_url: ActiveValue::Set(None),
|
||||||
|
author_name: ActiveValue::Set(Some("IMPHNEN Editorial".to_string())),
|
||||||
|
is_published: ActiveValue::Set(true),
|
||||||
|
created_at: ActiveValue::Set(Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(Utc::now()),
|
||||||
|
};
|
||||||
|
am.insert(&db).await?;
|
||||||
|
println!("✅ Inserted article: {}", slug);
|
||||||
|
}
|
||||||
|
println!("🟢 All articles seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use std::error::Error;
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::common::events::{
|
||||||
|
ActiveModel as EventsActiveModel, Entity as EventEntity,
|
||||||
|
};
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use imphnen_entities::seaorm::common::events::{ActiveModel as EventsActiveModel, Entity as EventEntity};
|
use sea_orm::{
|
||||||
use sea_orm::{ActiveValue::Set, ActiveModelTrait, EntityTrait, ColumnTrait, QueryFilter};
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter,
|
||||||
|
};
|
||||||
|
use std::error::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use chrono::Utc; // Removed NaiveDateTime as it was unused
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -54,7 +58,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
"2025-09-20T13:00:00Z",
|
"2025-09-20T13:00:00Z",
|
||||||
"2025-09-22T15:00:00Z",
|
"2025-09-22T15:00:00Z",
|
||||||
),
|
),
|
||||||
// Additional Events
|
|
||||||
(
|
(
|
||||||
"Rust Programming Bootcamp",
|
"Rust Programming Bootcamp",
|
||||||
"Intensive 3-day bootcamp to master Rust fundamentals and advanced concepts.",
|
"Intensive 3-day bootcamp to master Rust fundamentals and advanced concepts.",
|
||||||
@@ -154,18 +157,20 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
price,
|
price,
|
||||||
location,
|
location,
|
||||||
is_online,
|
is_online,
|
||||||
start_date_str, // Renamed to avoid conflict
|
start_date_str,
|
||||||
end_date_str, // Renamed to avoid conflict
|
end_date_str,
|
||||||
) in events
|
) in events
|
||||||
{
|
{
|
||||||
// Check if event already exists by name
|
let existing = EventEntity::find()
|
||||||
let existing = EventEntity::find().filter(<EventEntity as EntityTrait>::Column::Name.eq(name)).one(db).await?;
|
.filter(<EventEntity as EntityTrait>::Column::Name.eq(name))
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
if existing.is_some() {
|
if existing.is_some() {
|
||||||
println!("ℹ️ Skipping (already exists): {name}");
|
println!("ℹ️ Skipping (already exists): {name}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uuid = Uuid::new_v4(); // Generate a Uuid
|
let uuid = Uuid::new_v4();
|
||||||
let mut event_model: EventsActiveModel = Default::default();
|
let mut event_model: EventsActiveModel = Default::default();
|
||||||
event_model.id = Set(uuid);
|
event_model.id = Set(uuid);
|
||||||
event_model.name = Set(name.to_string());
|
event_model.name = Set(name.to_string());
|
||||||
@@ -174,12 +179,17 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
event_model.price = Set(price);
|
event_model.price = Set(price);
|
||||||
event_model.is_online = Set(is_online);
|
event_model.is_online = Set(is_online);
|
||||||
event_model.location = Set(location.clone());
|
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.start_date = Set(
|
||||||
event_model.end_date = Set(chrono::DateTime::parse_from_rfc3339(end_date_str)?.with_timezone(&chrono::Utc));
|
chrono::DateTime::parse_from_rfc3339(start_date_str)?
|
||||||
event_model.is_deleted = Set(false); // Explicitly set is_deleted
|
.with_timezone(&chrono::Utc),
|
||||||
event_model.created_at = Set(Utc::now()); // Explicitly set created_at
|
);
|
||||||
event_model.updated_at = Set(Utc::now()); // Explicitly set updated_at
|
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?;
|
event_model.insert(db).await?;
|
||||||
|
|
||||||
@@ -192,4 +202,4 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
println!("✅ All Events seeded");
|
println!("✅ All Events seeded");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
|
||||||
use imphnen_entities::seaorm::gacha::gacha_items::ActiveModel as GachaItemActiveModel;
|
use imphnen_entities::seaorm::gacha::gacha_items::ActiveModel as GachaItemActiveModel;
|
||||||
use imphnen_entities::seaorm::gacha::gacha_rolls::ActiveModel as GachaRollActiveModel;
|
use imphnen_entities::seaorm::gacha::gacha_rolls::ActiveModel as GachaRollActiveModel;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use sea_orm::ActiveModelTrait;
|
use sea_orm::ActiveModelTrait;
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use uuid::Uuid;
|
|
||||||
use sea_orm::ConnectionTrait;
|
use sea_orm::ConnectionTrait;
|
||||||
|
use std::error::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -15,18 +15,25 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let pg_conn = PostgresConnection::new(config).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = &pg_conn.conn;
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
// Check if gacha item already exists
|
let check_item_sql =
|
||||||
let check_item_sql = "SELECT id FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1' LIMIT 1";
|
"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 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 {
|
let gacha_item_uuid = if let Some(ref row) = item_result {
|
||||||
// Item exists, get its ID
|
|
||||||
row.try_get("", "id")?
|
row.try_get("", "id")?
|
||||||
} else {
|
} else {
|
||||||
// Item doesn't exist, create it
|
let _ = pg_conn
|
||||||
// Note: We can't easily delete by a fixed ID since it's a UUID, but the insert will fail if there's a conflict
|
.execute(sea_orm::Statement::from_string(
|
||||||
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();
|
db.get_database_backend(),
|
||||||
|
"DELETE FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1'".to_string(),
|
||||||
// Create gacha item via SeaORM
|
))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
let new_uuid = Uuid::new_v4();
|
let new_uuid = Uuid::new_v4();
|
||||||
let mut item_model: GachaItemActiveModel = Default::default();
|
let mut item_model: GachaItemActiveModel = Default::default();
|
||||||
item_model.id = Set(new_uuid);
|
item_model.id = Set(new_uuid);
|
||||||
@@ -47,7 +54,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
new_uuid
|
new_uuid
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always try to insert the roll, relying on the database constraints to prevent duplicates if needed
|
|
||||||
let gacha_roll_id = Uuid::new_v4();
|
let gacha_roll_id = Uuid::new_v4();
|
||||||
let mut roll_model: GachaRollActiveModel = Default::default();
|
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||||
roll_model.id = Set(gacha_roll_id);
|
roll_model.id = Set(gacha_roll_id);
|
||||||
@@ -61,18 +67,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
roll_model.insert(db).await?;
|
roll_model.insert(db).await?;
|
||||||
println!("Gacha Roll seeded successfully!");
|
println!("Gacha Roll seeded successfully!");
|
||||||
let gacha_roll_id = Uuid::new_v4();
|
let gacha_roll_id = Uuid::new_v4();
|
||||||
let mut roll_model: GachaRollActiveModel = Default::default();
|
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||||
roll_model.id = Set(gacha_roll_id);
|
roll_model.id = Set(gacha_roll_id);
|
||||||
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
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.gacha_id = Set(Uuid::new_v4().to_string());
|
||||||
roll_model.item_id = Set(gacha_item_uuid);
|
roll_model.item_id = Set(gacha_item_uuid);
|
||||||
roll_model.weight = Set(1.0);
|
roll_model.weight = Set(1.0);
|
||||||
roll_model.quantity = Set(10);
|
roll_model.quantity = Set(10);
|
||||||
roll_model.is_deleted = Set(false);
|
roll_model.is_deleted = Set(false);
|
||||||
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||||
roll_model.insert(db).await?;
|
roll_model.insert(db).await?;
|
||||||
println!("✅ Gacha items and rolls seeded.");
|
println!("✅ Gacha items and rolls seeded.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
#![allow(clippy::all)]
|
#![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::hash_password;
|
||||||
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, EntityTrait,
|
||||||
|
QueryFilter,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
|
||||||
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
|
||||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
|
||||||
use imphnen_entities::seaorm::auth::roles::{Entity as RoleEntity, Column as RoleColumn};
|
|
||||||
use sea_orm::{ActiveModelTrait, ConnectionTrait, ActiveValue::Set, EntityTrait, QueryFilter, ColumnTrait};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -16,17 +21,28 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let pg_conn = PostgresConnection::new(config).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = &pg_conn.conn;
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
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.ok();
|
let _ = pg_conn
|
||||||
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();
|
.execute(sea_orm::Statement::from_string(
|
||||||
|
db.get_database_backend(),
|
||||||
|
"DELETE FROM app_mentors WHERE id = 'e6f78d23-83bf-5c2b-bcd4-001345678901'"
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
.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();
|
||||||
|
|
||||||
// Find Mentor role
|
|
||||||
let role = RoleEntity::find()
|
let role = RoleEntity::find()
|
||||||
.filter(RoleColumn::Name.eq("Mentor"))
|
.filter(RoleColumn::Name.eq("Mentor"))
|
||||||
.one(db)
|
.one(db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or("Role 'Mentor' not found")?;
|
.ok_or("Role 'Mentor' not found")?;
|
||||||
|
|
||||||
// Insert user with Mentor role
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let mut user_model: UsersActiveModel = Default::default();
|
let mut user_model: UsersActiveModel = Default::default();
|
||||||
user_model.id = Set(user_id);
|
user_model.id = Set(user_id);
|
||||||
@@ -43,21 +59,23 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
user_model.updated_at = Set(chrono::Utc::now());
|
user_model.updated_at = Set(chrono::Utc::now());
|
||||||
user_model.insert(db).await?;
|
user_model.insert(db).await?;
|
||||||
|
|
||||||
// Insert mentor
|
|
||||||
let mentor_id = Uuid::new_v4();
|
let mentor_id = Uuid::new_v4();
|
||||||
let mut mentor_model: MentorsActiveModel = Default::default();
|
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||||
mentor_model.id = Set(mentor_id);
|
mentor_model.id = Set(mentor_id);
|
||||||
mentor_model.user_id = Set(user_id);
|
mentor_model.user_id = Set(user_id);
|
||||||
mentor_model.industries = Set(Some(json!( ["Software", "Education"] )));
|
mentor_model.industries = Set(Some(json!(["Software", "Education"])));
|
||||||
mentor_model.expertise = Set(Some(json!( ["Rust", "Microservices"] )));
|
mentor_model.expertise = Set(Some(json!(["Rust", "Microservices"])));
|
||||||
mentor_model.languages = Set(Some(json!( ["Indonesian", "English"] )));
|
mentor_model.languages = Set(Some(json!(["Indonesian", "English"])));
|
||||||
mentor_model.current_company = Set(Some("PT Contoh".to_string()));
|
mentor_model.current_company = Set(Some("PT Contoh".to_string()));
|
||||||
mentor_model.current_role = Set(Some("Senior Backend Engineer".to_string()));
|
mentor_model.current_role = Set(Some("Senior Backend Engineer".to_string()));
|
||||||
mentor_model.years_of_experience = Set(Some(5));
|
mentor_model.years_of_experience = Set(Some(5));
|
||||||
mentor_model.topics_of_interest = Set(Some(json!( ["Rust Programming", "Backend Development"] )));
|
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_mentee_level = Set(Some("beginner".to_string()));
|
||||||
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["online", "offline"] )));
|
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.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.mentoring_rate = Set(Some(100000.0));
|
||||||
mentor_model.status = Set(Some("verified".to_string()));
|
mentor_model.status = Set(Some("verified".to_string()));
|
||||||
mentor_model.is_deleted = Set(false);
|
mentor_model.is_deleted = Set(false);
|
||||||
|
|||||||
@@ -1,81 +1,79 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use imphnen_iam::PermissionsEnum;
|
use chrono::Utc;
|
||||||
use std::error::Error;
|
use imphnen_entities::seaorm::auth::permissions::ActiveModel as PermissionActiveModel;
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
use imphnen_entities::seaorm::auth::permissions::Entity as PermissionEntity;
|
||||||
use imphnen_entities::seaorm::auth::permissions::ActiveModel as PermissionActiveModel;
|
use imphnen_iam::PermissionsEnum;
|
||||||
use imphnen_entities::seaorm::auth::permissions::Entity as PermissionEntity;
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveModelTrait;
|
||||||
use sea_orm::{ActiveModelTrait};
|
use sea_orm::ActiveValue::Set;
|
||||||
use uuid::Uuid;
|
use std::error::Error;
|
||||||
use chrono::Utc;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let config = PostgresConfig::from_env()?;
|
let config = PostgresConfig::from_env()?;
|
||||||
let pg_conn = PostgresConnection::new(config).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = &pg_conn.conn;
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
for permission in [
|
for permission in [
|
||||||
PermissionsEnum::ReadListUsers,
|
PermissionsEnum::ReadListUsers,
|
||||||
PermissionsEnum::ReadDetailUsers,
|
PermissionsEnum::ReadDetailUsers,
|
||||||
PermissionsEnum::CreateUsers,
|
PermissionsEnum::CreateUsers,
|
||||||
PermissionsEnum::DeleteUsers,
|
PermissionsEnum::DeleteUsers,
|
||||||
PermissionsEnum::UpdateUsers,
|
PermissionsEnum::UpdateUsers,
|
||||||
PermissionsEnum::ActivateUsers,
|
PermissionsEnum::ActivateUsers,
|
||||||
PermissionsEnum::ReadListRoles,
|
PermissionsEnum::ReadListRoles,
|
||||||
PermissionsEnum::ReadDetailRoles,
|
PermissionsEnum::ReadDetailRoles,
|
||||||
PermissionsEnum::CreateRoles,
|
PermissionsEnum::CreateRoles,
|
||||||
PermissionsEnum::DeleteRoles,
|
PermissionsEnum::DeleteRoles,
|
||||||
PermissionsEnum::UpdateRoles,
|
PermissionsEnum::UpdateRoles,
|
||||||
PermissionsEnum::ReadListPermissions,
|
PermissionsEnum::ReadListPermissions,
|
||||||
PermissionsEnum::ReadDetailPermissions,
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
PermissionsEnum::CreatePermissions,
|
PermissionsEnum::CreatePermissions,
|
||||||
PermissionsEnum::DeletePermissions,
|
PermissionsEnum::DeletePermissions,
|
||||||
PermissionsEnum::UpdatePermissions,
|
PermissionsEnum::UpdatePermissions,
|
||||||
PermissionsEnum::CreateGachaClaims,
|
PermissionsEnum::CreateGachaClaims,
|
||||||
PermissionsEnum::ReadDetailGachaClaims,
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
PermissionsEnum::ReadListGachaItems,
|
PermissionsEnum::ReadListGachaItems,
|
||||||
PermissionsEnum::ReadDetailGachaItems,
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
PermissionsEnum::CreateGachaItems,
|
PermissionsEnum::CreateGachaItems,
|
||||||
PermissionsEnum::DeleteGachaItems,
|
PermissionsEnum::DeleteGachaItems,
|
||||||
PermissionsEnum::UpdateGachaItems,
|
PermissionsEnum::UpdateGachaItems,
|
||||||
PermissionsEnum::ReadDetailGachaRolls,
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
PermissionsEnum::CreateGachaRolls,
|
PermissionsEnum::CreateGachaRolls,
|
||||||
PermissionsEnum::ExecuteGachaRolls,
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
PermissionsEnum::ReadListMentors,
|
PermissionsEnum::ReadListMentors,
|
||||||
PermissionsEnum::ReadDetailMentors,
|
PermissionsEnum::ReadDetailMentors,
|
||||||
PermissionsEnum::RegisterMentors,
|
PermissionsEnum::RegisterMentors,
|
||||||
PermissionsEnum::ReadOwnMentorProfile,
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
PermissionsEnum::UpdateOwnMentorProfile,
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
PermissionsEnum::ReadOwnMentorStatus,
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
PermissionsEnum::UpdateMentors,
|
PermissionsEnum::UpdateMentors,
|
||||||
PermissionsEnum::VerifyMentors,
|
PermissionsEnum::VerifyMentors,
|
||||||
PermissionsEnum::DeleteMentors,
|
PermissionsEnum::DeleteMentors,
|
||||||
PermissionsEnum::Administrator,
|
PermissionsEnum::Administrator,
|
||||||
] {
|
] {
|
||||||
// permission.id() returns a string, try parse to uuid
|
let parsed_id =
|
||||||
let parsed_id = Uuid::parse_str(&permission.id()).unwrap_or_else(|_| Uuid::new_v4());
|
Uuid::parse_str(&permission.id()).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
|
|
||||||
// Check if permission already exists
|
let existing = PermissionEntity::find_by_id(parsed_id).one(db).await?;
|
||||||
let existing = PermissionEntity::find_by_id(parsed_id).one(db).await?;
|
if existing.is_some() {
|
||||||
if existing.is_some() {
|
println!("ℹ️ Skipping (already exists): {permission}");
|
||||||
println!("ℹ️ Skipping (already exists): {permission}");
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
let mut perm_model: PermissionActiveModel = Default::default();
|
||||||
// Insert permission using active model
|
perm_model.id = Set(parsed_id);
|
||||||
let mut perm_model: PermissionActiveModel = Default::default();
|
perm_model.name = Set(permission.to_string());
|
||||||
perm_model.id = Set(parsed_id);
|
perm_model.is_deleted = Set(false);
|
||||||
perm_model.name = Set(permission.to_string());
|
perm_model.created_at = Set(Utc::now());
|
||||||
perm_model.is_deleted = Set(false);
|
perm_model.updated_at = Set(Utc::now());
|
||||||
perm_model.created_at = Set(Utc::now());
|
perm_model.insert(db).await?;
|
||||||
perm_model.updated_at = Set(Utc::now());
|
println!("✅ Inserted: {permission}");
|
||||||
perm_model.insert(db).await?;
|
}
|
||||||
println!("✅ Inserted: {permission}");
|
|
||||||
}
|
println!("✅ All Permissions seeded");
|
||||||
|
|
||||||
println!("✅ All Permissions seeded");
|
Ok(())
|
||||||
|
}
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::error::Error;
|
use chrono::Utc;
|
||||||
|
use imphnen_entities::seaorm::auth::roles::{Entity as RoleEntity, RoleBuilder};
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use imphnen_entities::seaorm::auth::roles::{RoleBuilder, Entity as RoleEntity};
|
|
||||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait};
|
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait};
|
||||||
|
use std::error::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use chrono::Utc; // Added chrono
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -50,19 +50,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (id, name, _created_at_str, _updated_at_str) in roles { // Renamed to avoid conflict
|
for (id, name, _created_at_str, _updated_at_str) in roles {
|
||||||
let uuid = Uuid::parse_str(id).unwrap_or_else(|_| Uuid::new_v4());
|
let uuid = Uuid::parse_str(id).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
|
|
||||||
// Check if role already exists
|
|
||||||
let existing = RoleEntity::find_by_id(uuid).one(db).await?;
|
let existing = RoleEntity::find_by_id(uuid).one(db).await?;
|
||||||
if existing.is_some() {
|
if existing.is_some() {
|
||||||
println!("ℹ️ Skipping (already exists): {name}");
|
println!("ℹ️ Skipping (already exists): {name}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete existing by id to avoid duplicates (original logic, replaced by existence check)
|
|
||||||
// let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), format!("DELETE FROM app_roles WHERE id = '{}'", uuid))).await.ok();
|
|
||||||
|
|
||||||
let role_model = RoleBuilder::new()
|
let role_model = RoleBuilder::new()
|
||||||
.name(name.to_string())
|
.name(name.to_string())
|
||||||
.description("System generated role".to_string())
|
.description("System generated role".to_string())
|
||||||
@@ -71,13 +67,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
.build()?;
|
.build()?;
|
||||||
let mut role_model = role_model;
|
let mut role_model = role_model;
|
||||||
role_model.id = Set(uuid);
|
role_model.id = Set(uuid);
|
||||||
role_model.is_system_role = Set(true); // Set the missing field
|
role_model.is_system_role = Set(true);
|
||||||
role_model.created_at = Set(Utc::now()); // Set created_at
|
role_model.created_at = Set(Utc::now());
|
||||||
role_model.updated_at = Set(Utc::now()); // Set updated_at
|
role_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
role_model.insert(db).await?;
|
role_model.insert(db).await?;
|
||||||
println!("✅ Inserted role: {name}");
|
println!("✅ Inserted role: {name}");
|
||||||
}
|
}
|
||||||
println!("✅ All Roles seeded");
|
println!("✅ All Roles seeded");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +1,109 @@
|
|||||||
use imphnen_iam::PermissionsEnum;
|
use imphnen_entities::seaorm::auth::roles::ActiveModel as RoleActiveModel;
|
||||||
use std::error::Error;
|
use imphnen_entities::seaorm::auth::roles::Entity as RolesEntity;
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
use imphnen_iam::PermissionsEnum;
|
||||||
use imphnen_entities::seaorm::auth::roles::Entity as RolesEntity;
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use imphnen_entities::seaorm::auth::roles::ActiveModel as RoleActiveModel;
|
use sea_orm::ActiveModelTrait;
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::EntityTrait;
|
use sea_orm::EntityTrait;
|
||||||
use sea_orm::ActiveModelTrait;
|
use serde_json::Value as JsonValue;
|
||||||
use uuid::Uuid;
|
use std::error::Error;
|
||||||
use serde_json::Value as JsonValue;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let config = PostgresConfig::from_env()?;
|
let config = PostgresConfig::from_env()?;
|
||||||
let pg_conn = PostgresConnection::new(config).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = &pg_conn.conn;
|
let db = &pg_conn.conn;
|
||||||
// Ensure indexes are present if needed (placeholders) - we don't modify schema here
|
println!(
|
||||||
|
"✅ Index 'user_email_index' defined on table 'users' for column 'email'."
|
||||||
println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
|
);
|
||||||
|
|
||||||
let roles_permissions = vec![
|
let roles_permissions = vec![
|
||||||
(
|
(
|
||||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
vec![
|
vec![PermissionsEnum::Administrator],
|
||||||
// Only Administrator permission - grants access to everything
|
),
|
||||||
PermissionsEnum::Administrator,
|
(
|
||||||
],
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
),
|
vec![
|
||||||
(
|
PermissionsEnum::ReadListUsers,
|
||||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
vec![
|
PermissionsEnum::UpdateOwnMentorProfile,
|
||||||
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
PermissionsEnum::ReadOwnMentorProfile,
|
PermissionsEnum::ReadListMentors,
|
||||||
PermissionsEnum::UpdateOwnMentorProfile,
|
PermissionsEnum::ReadDetailMentors,
|
||||||
PermissionsEnum::ReadOwnMentorStatus,
|
PermissionsEnum::ReadListGachaItems,
|
||||||
PermissionsEnum::ReadListMentors,
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
PermissionsEnum::ReadDetailMentors,
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
PermissionsEnum::ReadListGachaItems,
|
PermissionsEnum::CreateGachaRolls,
|
||||||
PermissionsEnum::ReadDetailGachaItems,
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
PermissionsEnum::ReadDetailGachaRolls,
|
],
|
||||||
PermissionsEnum::CreateGachaRolls,
|
),
|
||||||
PermissionsEnum::ExecuteGachaRolls,
|
(
|
||||||
],
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
vec![
|
||||||
(
|
PermissionsEnum::ReadListGachaItems,
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
vec![
|
PermissionsEnum::ReadListUsers,
|
||||||
PermissionsEnum::ReadListGachaItems,
|
PermissionsEnum::ReadDetailUsers,
|
||||||
PermissionsEnum::ReadDetailGachaItems,
|
PermissionsEnum::CreateGachaClaims,
|
||||||
PermissionsEnum::ReadListUsers,
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
PermissionsEnum::ReadDetailUsers,
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
PermissionsEnum::CreateGachaClaims,
|
PermissionsEnum::CreateGachaRolls,
|
||||||
PermissionsEnum::ReadDetailGachaClaims,
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
PermissionsEnum::ReadDetailGachaRolls,
|
PermissionsEnum::RegisterMentors,
|
||||||
PermissionsEnum::CreateGachaRolls,
|
PermissionsEnum::ReadListMentors,
|
||||||
PermissionsEnum::ExecuteGachaRolls,
|
PermissionsEnum::ReadDetailMentors,
|
||||||
PermissionsEnum::RegisterMentors,
|
PermissionsEnum::ReadOwnMentorProfile,
|
||||||
PermissionsEnum::ReadListMentors,
|
PermissionsEnum::ReadOwnMentorStatus,
|
||||||
PermissionsEnum::ReadDetailMentors,
|
],
|
||||||
PermissionsEnum::ReadOwnMentorProfile,
|
),
|
||||||
PermissionsEnum::ReadOwnMentorStatus,
|
(
|
||||||
],
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
),
|
vec![
|
||||||
(
|
PermissionsEnum::ReadListRoles,
|
||||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
PermissionsEnum::ReadListPermissions,
|
||||||
vec![
|
PermissionsEnum::ReadListUsers,
|
||||||
// Staff should be able to list roles and permissions in tests
|
PermissionsEnum::ReadListMentors,
|
||||||
PermissionsEnum::ReadListRoles,
|
PermissionsEnum::ReadDetailUsers,
|
||||||
PermissionsEnum::ReadListPermissions,
|
PermissionsEnum::ActivateUsers,
|
||||||
PermissionsEnum::ReadListUsers,
|
PermissionsEnum::ReadDetailRoles,
|
||||||
PermissionsEnum::ReadListMentors,
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
PermissionsEnum::ReadDetailUsers,
|
PermissionsEnum::ReadListGachaItems,
|
||||||
PermissionsEnum::ActivateUsers,
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
PermissionsEnum::ReadDetailRoles,
|
PermissionsEnum::ReadListMentors,
|
||||||
PermissionsEnum::ReadDetailPermissions,
|
PermissionsEnum::ReadDetailMentors,
|
||||||
PermissionsEnum::ReadListGachaItems,
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
PermissionsEnum::ReadDetailGachaItems,
|
PermissionsEnum::CreateGachaRolls,
|
||||||
PermissionsEnum::ReadListMentors,
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
PermissionsEnum::ReadDetailMentors,
|
],
|
||||||
PermissionsEnum::ReadDetailGachaRolls,
|
),
|
||||||
PermissionsEnum::CreateGachaRolls,
|
(
|
||||||
PermissionsEnum::ExecuteGachaRolls,
|
"60f1aeb7-dad2-4e06-bcb5-be1ba510c906",
|
||||||
],
|
vec![PermissionsEnum::ActivateUsers],
|
||||||
),
|
),
|
||||||
(
|
("6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0", vec![]),
|
||||||
"60f1aeb7-dad2-4e06-bcb5-be1ba510c906",
|
];
|
||||||
vec![PermissionsEnum::ActivateUsers],
|
|
||||||
),
|
for (role_id, permissions) in roles_permissions {
|
||||||
("6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0", vec![]),
|
let role_uuid = Uuid::parse_str(role_id).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
];
|
let json_permissions = JsonValue::Array(
|
||||||
|
permissions
|
||||||
for (role_id, permissions) in roles_permissions {
|
.iter()
|
||||||
let role_uuid = Uuid::parse_str(role_id).unwrap_or_else(|_| Uuid::new_v4());
|
.map(|p| JsonValue::String(p.id()))
|
||||||
// Map permissions enum to JSON array of permission ids
|
.collect(),
|
||||||
let json_permissions = JsonValue::Array(
|
);
|
||||||
permissions.iter().map(|p| JsonValue::String(p.id())).collect()
|
|
||||||
);
|
if let Some(role_model) = RolesEntity::find_by_id(role_uuid).one(db).await? {
|
||||||
|
let mut am: RoleActiveModel = role_model.into();
|
||||||
// Find role and update permissions
|
am.permissions = Set(Some(json_permissions));
|
||||||
if let Some(role_model) = RolesEntity::find_by_id(role_uuid).one(db).await? {
|
am.update(db).await?;
|
||||||
let mut am: RoleActiveModel = role_model.into();
|
println!("✅ Permissions updated for role: {role_id}");
|
||||||
am.permissions = Set(Some(json_permissions));
|
} else {
|
||||||
am.update(db).await?;
|
println!("⚠️ Role with id {role_id} not found, skipping permissions update");
|
||||||
println!("✅ Permissions updated for role: {role_id}");
|
}
|
||||||
} else {
|
}
|
||||||
println!("⚠️ Role with id {role_id} not found, skipping permissions update");
|
|
||||||
}
|
println!("✅ All roles permissions updated!");
|
||||||
}
|
Ok(())
|
||||||
|
}
|
||||||
println!("✅ All roles permissions updated!");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,77 +1,79 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use std::error::Error;
|
use chrono::Utc;
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||||
use imphnen_entities::seaorm::common::events::ActiveModel as EventsActiveModel;
|
use imphnen_entities::seaorm::common::events::ActiveModel as EventsActiveModel;
|
||||||
use imphnen_entities::seaorm::common::testimonials::ActiveModel as TestimonialsActiveModel;
|
use imphnen_entities::seaorm::common::testimonials::ActiveModel as TestimonialsActiveModel;
|
||||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use sea_orm::ActiveValue::Set;
|
|
||||||
use sea_orm::ActiveModelTrait;
|
use sea_orm::ActiveModelTrait;
|
||||||
use uuid::Uuid;
|
use sea_orm::ActiveValue::Set;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use chrono::Utc;
|
use std::error::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let config = PostgresConfig::from_env()?;
|
let config = PostgresConfig::from_env()?;
|
||||||
let pg_conn = PostgresConnection::new(config).await?;
|
let pg_conn = PostgresConnection::new(config).await?;
|
||||||
let db = &pg_conn.conn;
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
// Seed Events - handle existing data
|
let uuid = Uuid::new_v4().to_string();
|
||||||
let uuid = Uuid::new_v4().to_string();
|
let mut event_model: EventsActiveModel = Default::default();
|
||||||
let mut event_model: EventsActiveModel = Default::default();
|
event_model.id = Set(Uuid::parse_str(&uuid)?);
|
||||||
event_model.id = Set(Uuid::parse_str(&uuid)?);
|
event_model.name = Set("Test Event".to_string());
|
||||||
event_model.name = Set("Test Event".to_string());
|
event_model.description = Set("Test event description".to_string());
|
||||||
event_model.description = Set("Test event description".to_string());
|
event_model.detail_link = Set("https://example.com/event".to_string());
|
||||||
event_model.detail_link = Set("https://example.com/event".to_string());
|
event_model.price = Set(50.0);
|
||||||
event_model.price = Set(50.0);
|
event_model.is_online = Set(true);
|
||||||
event_model.is_online = Set(true);
|
event_model.start_date = Set(Utc::now());
|
||||||
event_model.start_date = Set(Utc::now());
|
event_model.end_date = Set(Utc::now() + chrono::Duration::days(1));
|
||||||
event_model.end_date = Set(Utc::now() + chrono::Duration::days(1));
|
event_model.location = Set(None);
|
||||||
event_model.location = Set(None);
|
event_model.is_deleted = Set(false);
|
||||||
event_model.is_deleted = Set(false);
|
match event_model.insert(db).await {
|
||||||
match event_model.insert(db).await {
|
Ok(_) => println!("✅ Inserted test event"),
|
||||||
Ok(_) => println!("✅ Inserted test event"),
|
Err(_) => {
|
||||||
Err(_) => println!("⚠️ Test event already exists or could not be inserted, skipping"),
|
println!("⚠️ Test event already exists or could not be inserted, skipping")
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Seed Testimonials - handle existing data
|
let mut testimonial_model: TestimonialsActiveModel = Default::default();
|
||||||
let mut testimonial_model: TestimonialsActiveModel = Default::default();
|
testimonial_model.id =
|
||||||
testimonial_model.id = Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
||||||
testimonial_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
testimonial_model.user_id =
|
||||||
testimonial_model.role = Set("Student".to_string());
|
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
testimonial_model.content = Set("This is a great platform!".to_string());
|
testimonial_model.role = Set("Student".to_string());
|
||||||
testimonial_model.is_deleted = Set(false);
|
testimonial_model.content = Set("This is a great platform!".to_string());
|
||||||
match testimonial_model.insert(db).await {
|
testimonial_model.is_deleted = Set(false);
|
||||||
Ok(_) => println!("✅ Inserted test testimonial"),
|
match testimonial_model.insert(db).await {
|
||||||
Err(_) => println!("⚠️ Test testimonial already exists or could not be inserted, skipping"),
|
Ok(_) => println!("✅ Inserted test testimonial"),
|
||||||
};
|
Err(_) => println!(
|
||||||
|
"⚠️ Test testimonial already exists or could not be inserted, skipping"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
// Seed Mentor - handle existing data
|
let mentor_id = Uuid::new_v4();
|
||||||
let mentor_id = Uuid::new_v4();
|
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||||
let mut mentor_model: MentorsActiveModel = Default::default();
|
mentor_model.id = Set(mentor_id);
|
||||||
mentor_model.id = Set(mentor_id);
|
mentor_model.user_id =
|
||||||
// Use the admin user ID instead of a random one
|
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||||
mentor_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
mentor_model.industries = Set(Some(json!(["Technology", "Education"])));
|
||||||
mentor_model.industries = Set(Some(json!( ["Technology", "Education"] )));
|
mentor_model.expertise = Set(Some(json!(["Software Development"])));
|
||||||
mentor_model.expertise = Set(Some(json!( ["Software Development"] )));
|
mentor_model.languages = Set(Some(json!(["English", "Indonesian"])));
|
||||||
mentor_model.languages = Set(Some(json!( ["English", "Indonesian"] )));
|
mentor_model.current_company = Set(Some("Tech Corp".to_string()));
|
||||||
mentor_model.current_company = Set(Some("Tech Corp".to_string()));
|
mentor_model.current_role = Set(Some("Senior Engineer".to_string()));
|
||||||
mentor_model.current_role = Set(Some("Senior Engineer".to_string()));
|
mentor_model.years_of_experience = Set(Some(5));
|
||||||
mentor_model.years_of_experience = Set(Some(5));
|
mentor_model.topics_of_interest = Set(Some(json!(["Rust", "Web Development"])));
|
||||||
mentor_model.topics_of_interest = Set(Some(json!( ["Rust", "Web Development"] )));
|
mentor_model.preferred_mentee_level = Set(Some("Beginner".to_string()));
|
||||||
mentor_model.preferred_mentee_level = Set(Some("Beginner".to_string()));
|
mentor_model.preferred_mentoring_formats = Set(Some(json!(["1:1", "Group"])));
|
||||||
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["1:1", "Group"] )));
|
mentor_model.availability_commitment = Set(Some("Weekly".to_string()));
|
||||||
mentor_model.availability_commitment = Set(Some("Weekly".to_string()));
|
mentor_model.mentoring_rate = Set(Some(100.0));
|
||||||
mentor_model.mentoring_rate = Set(Some(100.0));
|
mentor_model.status = Set(Some("active".to_string()));
|
||||||
mentor_model.status = Set(Some("active".to_string()));
|
mentor_model.is_deleted = Set(false);
|
||||||
mentor_model.is_deleted = Set(false);
|
mentor_model.created_at = Set(chrono::Utc::now());
|
||||||
mentor_model.created_at = Set(chrono::Utc::now());
|
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||||
mentor_model.updated_at = Set(chrono::Utc::now());
|
mentor_model.insert(db).await?;
|
||||||
// Create mentor record via SeaORM active model
|
println!("✅ Inserted test mentor via SeaORM");
|
||||||
mentor_model.insert(db).await?;
|
|
||||||
println!("✅ Inserted test mentor via SeaORM");
|
|
||||||
|
|
||||||
println!("✅ All test data seeded successfully");
|
println!("✅ All test data seeded successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#![allow(clippy::all)]
|
#![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::hash_password;
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||||
use imphnen_entities::seaorm::auth::users::Entity as UserEntity; // Added for dynamic role lookup
|
|
||||||
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
use chrono::Utc;
|
||||||
|
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, IntoActiveModel};
|
||||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, IntoActiveModel};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use chrono::Utc;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -17,145 +17,148 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let db = &pg_conn.conn;
|
let db = &pg_conn.conn;
|
||||||
|
|
||||||
let users = vec![
|
let users = vec![
|
||||||
(
|
(
|
||||||
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2",
|
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2",
|
||||||
"admin@example.com",
|
"admin@example.com",
|
||||||
"Admin",
|
"Admin",
|
||||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"a4d23fb5-9e31-423c-9842-fbd6e75a5298",
|
"a4d23fb5-9e31-423c-9842-fbd6e75a5298",
|
||||||
"staff@example.com",
|
"staff@example.com",
|
||||||
"Staff",
|
"Staff",
|
||||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"d5e89c12-72af-4b1a-abc3-ff1234567890",
|
"d5e89c12-72af-4b1a-abc3-ff1234567890",
|
||||||
"user@example.com",
|
"user@example.com",
|
||||||
"User",
|
"User",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"665a3cfc-ea5f-4bcd-8769-4a6d8d1451d4",
|
"665a3cfc-ea5f-4bcd-8769-4a6d8d1451d4",
|
||||||
"testuser1@example.com",
|
"testuser1@example.com",
|
||||||
"Test User 1",
|
"Test User 1",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059", // Fixed UUID
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"3972c139-a450-416c-93b0-c42539dc780f",
|
"3972c139-a450-416c-93b0-c42539dc780f",
|
||||||
"testuser2@example.com",
|
"testuser2@example.com",
|
||||||
"Test User 2",
|
"Test User 2",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"b426c0a9-0efb-4e26-b078-4f18767255f3",
|
"b426c0a9-0efb-4e26-b078-4f18767255f3",
|
||||||
"testuser3@example.com",
|
"testuser3@example.com",
|
||||||
"Test User 3",
|
"Test User 3",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059", // Fixed UUID
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
),
|
),
|
||||||
// Additional Users for Volume and Variety
|
(
|
||||||
(
|
"11111111-1111-1111-1111-111111111111",
|
||||||
"11111111-1111-1111-1111-111111111111",
|
"user4@example.com",
|
||||||
"user4@example.com",
|
"User Four",
|
||||||
"User Four",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
),
|
||||||
),
|
(
|
||||||
(
|
"22222222-2222-2222-2222-222222222222",
|
||||||
"22222222-2222-2222-2222-222222222222",
|
"user5@example.com",
|
||||||
"user5@example.com",
|
"User Five",
|
||||||
"User Five",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
),
|
||||||
),
|
(
|
||||||
(
|
"33333333-3333-3333-3333-333333333333",
|
||||||
"33333333-3333-3333-3333-333333333333",
|
"mentor2@example.com",
|
||||||
"mentor2@example.com",
|
"Mentor Two",
|
||||||
"Mentor Two",
|
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a", // Mentor Role
|
),
|
||||||
),
|
(
|
||||||
(
|
"44444444-4444-4444-4444-444444444444",
|
||||||
"44444444-4444-4444-4444-444444444444",
|
"staff2@example.com",
|
||||||
"staff2@example.com",
|
"Staff Two",
|
||||||
"Staff Two",
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86", // Staff Role
|
),
|
||||||
),
|
(
|
||||||
(
|
"55555555-5555-5555-5555-555555555555",
|
||||||
"55555555-5555-5555-5555-555555555555",
|
"user6@example.com",
|
||||||
"user6@example.com",
|
"User Six",
|
||||||
"User Six",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
),
|
||||||
),
|
(
|
||||||
(
|
"66666666-6666-6666-6666-666666666666",
|
||||||
"66666666-6666-6666-6666-666666666666",
|
"user7@example.com",
|
||||||
"user7@example.com",
|
"User Seven",
|
||||||
"User Seven",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
),
|
||||||
),
|
(
|
||||||
(
|
"77777777-7777-7777-7777-777777777777",
|
||||||
"77777777-7777-7777-7777-777777777777",
|
"user8@example.com",
|
||||||
"user8@example.com",
|
"User Eight",
|
||||||
"User Eight",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
),
|
||||||
),
|
(
|
||||||
(
|
"88888888-8888-8888-8888-888888888888",
|
||||||
"88888888-8888-8888-8888-888888888888",
|
"user9@example.com",
|
||||||
"user9@example.com",
|
"User Nine",
|
||||||
"User Nine",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
),
|
||||||
),
|
(
|
||||||
(
|
"99999999-9999-9999-9999-999999999999",
|
||||||
"99999999-9999-9999-9999-999999999999",
|
"user10@example.com",
|
||||||
"user10@example.com",
|
"User Ten",
|
||||||
"User Ten",
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
),
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (id, email, fullname, role_id_str) in users { // role_id_str directly contains UUID
|
for (id, email, fullname, role_id_str) in users {
|
||||||
let role_uuid = Some(Uuid::parse_str(role_id_str)
|
let role_uuid = Some(
|
||||||
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?);
|
Uuid::parse_str(role_id_str)
|
||||||
|
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?,
|
||||||
// Build SeaORM ActiveModel for users
|
);
|
||||||
let uid = Uuid::parse_str(id)?; // Should always be valid UUID strings from test data
|
|
||||||
|
|
||||||
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 };
|
|
||||||
|
|
||||||
let password = "password";
|
|
||||||
let hashed = hash_password(password).unwrap();
|
|
||||||
|
|
||||||
// Explicit Upsert Logic
|
let uid = Uuid::parse_str(id)?;
|
||||||
let existing_user = UserEntity::find_by_id(uid).one(db).await?;
|
|
||||||
let is_update = existing_user.is_some();
|
let names: Vec<&str> = fullname.split_whitespace().collect();
|
||||||
|
let first_name = names.first().map(|s| s.to_string());
|
||||||
let mut user_model: UsersActiveModel = if let Some(existing) = existing_user {
|
let last_name = if names.len() > 1 {
|
||||||
println!("🔄 Updating user: {fullname} ({email})");
|
Some(names[1..].join(" "))
|
||||||
existing.into_active_model()
|
} else {
|
||||||
} else {
|
None
|
||||||
println!("✅ Inserting user: {fullname} ({email})");
|
};
|
||||||
let mut active: UsersActiveModel = Default::default();
|
|
||||||
active.id = Set(uid);
|
let password = "password";
|
||||||
active.created_at = Set(Utc::now());
|
let hashed = hash_password(password).unwrap();
|
||||||
active
|
|
||||||
};
|
let existing_user = UserEntity::find_by_id(uid).one(db).await?;
|
||||||
|
let is_update = existing_user.is_some();
|
||||||
user_model.email = Set(email.to_string());
|
|
||||||
user_model.password_hash = Set(hashed);
|
let mut user_model: UsersActiveModel = if let Some(existing) = existing_user {
|
||||||
user_model.username = Set(email.to_string());
|
println!("🔄 Updating user: {fullname} ({email})");
|
||||||
user_model.first_name = Set(first_name);
|
existing.into_active_model()
|
||||||
user_model.last_name = Set(last_name);
|
} else {
|
||||||
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
println!("✅ Inserting user: {fullname} ({email})");
|
||||||
user_model.is_verified = Set(true);
|
let mut active: UsersActiveModel = Default::default();
|
||||||
user_model.is_active = Set(true);
|
active.id = Set(uid);
|
||||||
user_model.role_id = Set(role_uuid);
|
active.created_at = Set(Utc::now());
|
||||||
user_model.updated_at = Set(Utc::now());
|
active
|
||||||
|
};
|
||||||
if is_update {
|
|
||||||
user_model.update(db).await?;
|
user_model.email = Set(email.to_string());
|
||||||
} else {
|
user_model.password_hash = Set(hashed);
|
||||||
user_model.insert(db).await?;
|
user_model.username = Set(email.to_string());
|
||||||
}
|
user_model.first_name = Set(first_name);
|
||||||
}
|
user_model.last_name = Set(last_name);
|
||||||
|
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
||||||
|
user_model.is_verified = Set(true);
|
||||||
|
user_model.is_active = Set(true);
|
||||||
|
user_model.role_id = Set(role_uuid);
|
||||||
|
user_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
|
if is_update {
|
||||||
|
user_model.update(db).await?;
|
||||||
|
} else {
|
||||||
|
user_model.insert(db).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
println!("✅ All Users seeded");
|
println!("✅ All Users seeded");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,368 +1,395 @@
|
|||||||
//! PostgreSQL Connection Test Program
|
use chrono::Utc;
|
||||||
//! This program tests the PostgreSQL integration with SeaORM
|
use imphnen_entities::seaorm::auth::roles::{
|
||||||
|
Entity as RolesEntity, Model as RoleModel,
|
||||||
use std::sync::Arc;
|
};
|
||||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection, PostgresError};
|
use imphnen_entities::seaorm::auth::users::{
|
||||||
use imphnen_entities::seaorm::auth::users::{Entity as UsersEntity, Model as UserModel};
|
Entity as UsersEntity, Model as UserModel,
|
||||||
use imphnen_entities::seaorm::auth::roles::{Entity as RolesEntity, Model as RoleModel};
|
};
|
||||||
use sea_orm::{EntityTrait, ActiveModelTrait, Set, TransactionTrait, DbErr, PaginatorTrait};
|
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection, PostgresError};
|
||||||
use uuid::Uuid;
|
use sea_orm::{
|
||||||
use chrono::Utc;
|
ActiveModelTrait, DbErr, EntityTrait, PaginatorTrait, Set, TransactionTrait,
|
||||||
|
};
|
||||||
#[tokio::main]
|
use std::sync::Arc;
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
use uuid::Uuid;
|
||||||
println!("🚀 Starting PostgreSQL Connection Test");
|
|
||||||
println!("=====================================");
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Load configuration from environment
|
println!("🚀 Starting PostgreSQL Connection Test");
|
||||||
let config = PostgresConfig::from_env()?;
|
println!("=====================================");
|
||||||
println!("✅ Configuration loaded successfully");
|
|
||||||
println!(" Database URL: {}", config.database_url.replace("postgres://", "postgres://****:****@"));
|
let config = PostgresConfig::from_env()?;
|
||||||
println!(" Pool size: {}", config.pool_size);
|
println!("✅ Configuration loaded successfully");
|
||||||
println!(" Connect timeout: {}s", config.connect_timeout);
|
println!(
|
||||||
println!(" Retry attempts: {}", config.retry_attempts);
|
" Database URL: {}",
|
||||||
|
config
|
||||||
// Test connection
|
.database_url
|
||||||
println!("\n🔌 Testing PostgreSQL connection...");
|
.replace("postgres://", "postgres://****:****@")
|
||||||
match test_connection(config).await {
|
);
|
||||||
Ok(()) => {
|
println!(" Pool size: {}", config.pool_size);
|
||||||
println!("✅ All PostgreSQL tests passed successfully!");
|
println!(" Connect timeout: {}s", config.connect_timeout);
|
||||||
Ok(())
|
println!(" Retry attempts: {}", config.retry_attempts);
|
||||||
}
|
|
||||||
Err(e) => {
|
println!("\n🔌 Testing PostgreSQL connection...");
|
||||||
println!("❌ PostgreSQL test failed: {}", e);
|
match test_connection(config).await {
|
||||||
Err(e.into())
|
Ok(()) => {
|
||||||
}
|
println!("✅ All PostgreSQL tests passed successfully!");
|
||||||
}
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
async fn test_connection(config: PostgresConfig) -> Result<(), PostgresError> {
|
println!("❌ PostgreSQL test failed: {}", e);
|
||||||
// Create connection
|
Err(e.into())
|
||||||
println!(" Creating PostgreSQL connection...");
|
}
|
||||||
let postgres_conn = PostgresConnection::new(config).await?;
|
}
|
||||||
let connection = Arc::new(postgres_conn);
|
}
|
||||||
println!(" ✅ Connection established successfully");
|
|
||||||
|
async fn test_connection(config: PostgresConfig) -> Result<(), PostgresError> {
|
||||||
// Test basic connectivity
|
println!(" Creating PostgreSQL connection...");
|
||||||
println!(" Testing basic connectivity...");
|
let postgres_conn = PostgresConnection::new(config).await?;
|
||||||
test_basic_connectivity(&connection).await?;
|
let connection = Arc::new(postgres_conn);
|
||||||
println!(" ✅ Basic connectivity test passed");
|
println!(" ✅ Connection established successfully");
|
||||||
|
|
||||||
// Test table existence
|
println!(" Testing basic connectivity...");
|
||||||
println!(" Testing table existence...");
|
test_basic_connectivity(&connection).await?;
|
||||||
test_table_existence(&connection).await?;
|
println!(" ✅ Basic connectivity test passed");
|
||||||
println!(" ✅ Table existence test passed");
|
|
||||||
|
println!(" Testing table existence...");
|
||||||
// Test CRUD operations
|
test_table_existence(&connection).await?;
|
||||||
println!(" Testing CRUD operations...");
|
println!(" ✅ Table existence test passed");
|
||||||
test_crud_operations(&connection).await?;
|
|
||||||
println!(" ✅ CRUD operations test passed");
|
println!(" Testing CRUD operations...");
|
||||||
|
test_crud_operations(&connection).await?;
|
||||||
// Test transaction support
|
println!(" ✅ CRUD operations test passed");
|
||||||
println!(" Testing transaction support...");
|
|
||||||
test_transactions(&connection).await?;
|
println!(" Testing transaction support...");
|
||||||
println!(" ✅ Transaction support test passed");
|
test_transactions(&connection).await?;
|
||||||
|
println!(" ✅ Transaction support test passed");
|
||||||
// Test error handling
|
|
||||||
println!(" Testing error handling...");
|
println!(" Testing error handling...");
|
||||||
test_error_handling(&connection).await?;
|
test_error_handling(&connection).await?;
|
||||||
println!(" ✅ Error handling test passed");
|
println!(" ✅ Error handling test passed");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_basic_connectivity(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
async fn test_basic_connectivity(
|
||||||
// Execute a simple query
|
connection: &Arc<PostgresConnection>,
|
||||||
let statement = sea_orm::Statement::from_string(
|
) -> Result<(), PostgresError> {
|
||||||
connection.get_database_backend(),
|
let statement = sea_orm::Statement::from_string(
|
||||||
"SELECT 1 as test_value, current_timestamp as current_time".to_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 result = connection.query_one(statement).await?.ok_or_else(|| {
|
||||||
|
PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
// Verify we got expected results
|
"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) {
|
let test_value: Option<i32> = result.try_get("", "test_value").ok();
|
||||||
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
let current_time: Option<String> = result.try_get("", "current_time").ok();
|
||||||
format!("Expected test_value=1, got {:?}", test_value)
|
|
||||||
)));
|
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()
|
|
||||||
)));
|
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(())
|
}
|
||||||
}
|
|
||||||
|
println!(
|
||||||
async fn test_table_existence(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
" 📝 Query result: test_value={:?}, current_time={:?}",
|
||||||
// Test if our tables exist
|
test_value, current_time
|
||||||
use sea_orm::EntityTrait;
|
);
|
||||||
|
Ok(())
|
||||||
println!(" 📋 Checking users table...");
|
}
|
||||||
let user_count = UsersEntity::find()
|
|
||||||
.count(&connection.conn)
|
async fn test_table_existence(
|
||||||
.await
|
connection: &Arc<PostgresConnection>,
|
||||||
.map_err(PostgresError::ConnectionError)?;
|
) -> Result<(), PostgresError> {
|
||||||
println!(" 📊 Users table accessible, current count: {}", user_count);
|
use sea_orm::EntityTrait;
|
||||||
|
|
||||||
println!(" 📋 Checking roles table...");
|
println!(" 📋 Checking users table...");
|
||||||
let role_count = RolesEntity::find()
|
let user_count = UsersEntity::find()
|
||||||
.count(&connection.conn)
|
.count(&connection.conn)
|
||||||
.await
|
.await
|
||||||
.map_err(PostgresError::ConnectionError)?;
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
println!(" 📊 Roles table accessible, current count: {}", role_count);
|
println!(
|
||||||
|
" 📊 Users table accessible, current count: {}",
|
||||||
Ok(())
|
user_count
|
||||||
}
|
);
|
||||||
|
|
||||||
async fn test_crud_operations(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
println!(" 📋 Checking roles table...");
|
||||||
use sea_orm::{ActiveModelTrait, Set};
|
let role_count = RolesEntity::find()
|
||||||
|
.count(&connection.conn)
|
||||||
// Create test user
|
.await
|
||||||
println!(" ➕ Creating test user...");
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
let test_user_id = Uuid::new_v4();
|
println!(
|
||||||
let now = Utc::now();
|
" 📊 Roles table accessible, current count: {}",
|
||||||
|
role_count
|
||||||
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
);
|
||||||
id: Set(test_user_id),
|
|
||||||
email: Set(format!("test_user_{}@example.com", test_user_id)),
|
Ok(())
|
||||||
password_hash: Set("test_password_hash".to_string()),
|
}
|
||||||
username: Set(format!("testuser_{}", test_user_id)),
|
|
||||||
first_name: Set(Some("Test".to_string())),
|
async fn test_crud_operations(
|
||||||
last_name: Set(Some("User".to_string())),
|
connection: &Arc<PostgresConnection>,
|
||||||
avatar_url: Set(None),
|
) -> Result<(), PostgresError> {
|
||||||
is_verified: Set(false),
|
use sea_orm::{ActiveModelTrait, Set};
|
||||||
is_active: Set(true),
|
|
||||||
metadata: Set(None),
|
println!(" ➕ Creating test user...");
|
||||||
role_id: Set(None),
|
let test_user_id = Uuid::new_v4();
|
||||||
created_at: Set(now),
|
let now = Utc::now();
|
||||||
updated_at: Set(now),
|
|
||||||
deleted_at: Set(None),
|
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||||
};
|
id: Set(test_user_id),
|
||||||
|
email: Set(format!("test_user_{}@example.com", test_user_id)),
|
||||||
let created_user = user_model.insert(&connection.conn)
|
password_hash: Set("test_password_hash".to_string()),
|
||||||
.await
|
username: Set(format!("testuser_{}", test_user_id)),
|
||||||
.map_err(PostgresError::ConnectionError)?;
|
first_name: Set(Some("Test".to_string())),
|
||||||
|
last_name: Set(Some("User".to_string())),
|
||||||
println!(" ✅ Created user with ID: {}", created_user.id);
|
avatar_url: Set(None),
|
||||||
|
is_verified: Set(false),
|
||||||
// Read user
|
is_active: Set(true),
|
||||||
println!(" 🔍 Reading test user...");
|
metadata: Set(None),
|
||||||
let found_user = UsersEntity::find_by_id(test_user_id)
|
role_id: Set(None),
|
||||||
.one(&connection.conn)
|
created_at: Set(now),
|
||||||
.await
|
updated_at: Set(now),
|
||||||
.map_err(PostgresError::ConnectionError)?
|
deleted_at: Set(None),
|
||||||
.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);
|
let created_user = user_model
|
||||||
|
.insert(&connection.conn)
|
||||||
// Update user
|
.await
|
||||||
println!(" ✏️ Updating test user...");
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
let mut update_model: imphnen_entities::seaorm::auth::users::ActiveModel = found_user.into();
|
|
||||||
update_model.first_name = Set(Some("Updated".to_string()));
|
println!(" ✅ Created user with ID: {}", created_user.id);
|
||||||
update_model.updated_at = Set(Utc::now());
|
|
||||||
|
println!(" 🔍 Reading test user...");
|
||||||
let updated_user = update_model.update(&connection.conn)
|
let found_user = UsersEntity::find_by_id(test_user_id)
|
||||||
.await
|
.one(&connection.conn)
|
||||||
.map_err(PostgresError::ConnectionError)?;
|
.await
|
||||||
|
.map_err(PostgresError::ConnectionError)?
|
||||||
println!(" ✅ Updated user first name to: {:?}", updated_user.first_name);
|
.ok_or_else(|| {
|
||||||
|
PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||||
// Delete user
|
"User not found after creation".to_string(),
|
||||||
println!(" 🗑️ Deleting test user...");
|
))
|
||||||
UsersEntity::delete_by_id(updated_user.id)
|
})?;
|
||||||
.exec(&connection.conn)
|
|
||||||
.await
|
println!(
|
||||||
.map_err(PostgresError::ConnectionError)?;
|
" ✅ Found user: {} ({})",
|
||||||
|
found_user.username, found_user.email
|
||||||
println!(" ✅ Test user deleted successfully");
|
);
|
||||||
|
|
||||||
Ok(())
|
println!(" ✏️ Updating test user...");
|
||||||
}
|
let mut update_model: imphnen_entities::seaorm::auth::users::ActiveModel =
|
||||||
|
found_user.into();
|
||||||
async fn test_transactions(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
update_model.first_name = Set(Some("Updated".to_string()));
|
||||||
println!(" 💰 Testing transaction support...");
|
update_model.updated_at = Set(Utc::now());
|
||||||
|
|
||||||
// Test transaction with rollback
|
let updated_user = update_model
|
||||||
let transaction_result = connection.conn.transaction(|txn| {
|
.update(&connection.conn)
|
||||||
Box::pin(async move {
|
.await
|
||||||
// Create a test user within transaction
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
let test_user_id = Uuid::new_v4();
|
|
||||||
let now = Utc::now();
|
println!(
|
||||||
|
" ✅ Updated user first name to: {:?}",
|
||||||
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
updated_user.first_name
|
||||||
id: Set(test_user_id),
|
);
|
||||||
email: Set(format!("transaction_test_{}@example.com", test_user_id)),
|
|
||||||
password_hash: Set("transaction_password_hash".to_string()),
|
println!(" 🗑️ Deleting test user...");
|
||||||
username: Set(format!("transaction_user_{}", test_user_id)),
|
UsersEntity::delete_by_id(updated_user.id)
|
||||||
first_name: Set(Some("Transaction".to_string())),
|
.exec(&connection.conn)
|
||||||
last_name: Set(Some("Test".to_string())),
|
.await
|
||||||
avatar_url: Set(None),
|
.map_err(PostgresError::ConnectionError)?;
|
||||||
is_verified: Set(false),
|
|
||||||
is_active: Set(true),
|
println!(" ✅ Test user deleted successfully");
|
||||||
metadata: Set(None),
|
|
||||||
role_id: Set(None),
|
Ok(())
|
||||||
created_at: Set(now),
|
}
|
||||||
updated_at: Set(now),
|
|
||||||
deleted_at: Set(None),
|
async fn test_transactions(
|
||||||
};
|
connection: &Arc<PostgresConnection>,
|
||||||
|
) -> Result<(), PostgresError> {
|
||||||
let _created_user = user_model.insert(txn)
|
println!(" 💰 Testing transaction support...");
|
||||||
.await?;
|
|
||||||
|
let transaction_result = connection
|
||||||
// Simulate an error to trigger rollback (return a sea_orm::DbErr so the TransactionError matches)
|
.conn
|
||||||
Err::<(), DbErr>(DbErr::Custom("Simulated transaction failure".to_string()))
|
.transaction(|txn| {
|
||||||
})
|
Box::pin(async move {
|
||||||
}).await;
|
let test_user_id = Uuid::new_v4();
|
||||||
|
let now = Utc::now();
|
||||||
// Transaction should fail and rollback
|
|
||||||
match transaction_result {
|
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||||
Err(e) => {
|
id: Set(test_user_id),
|
||||||
let e_text = format!("{:?}", e);
|
email: Set(format!("transaction_test_{}@example.com", test_user_id)),
|
||||||
if e_text.contains("Simulated transaction failure") {
|
password_hash: Set("transaction_password_hash".to_string()),
|
||||||
println!(" ✅ Transaction failed as expected, rollback successful");
|
username: Set(format!("transaction_user_{}", test_user_id)),
|
||||||
} else {
|
first_name: Set(Some("Transaction".to_string())),
|
||||||
return Err(PostgresError::OperationFailed(format!("Unexpected transaction result: {}", e_text)));
|
last_name: Set(Some("Test".to_string())),
|
||||||
}
|
avatar_url: Set(None),
|
||||||
}
|
is_verified: Set(false),
|
||||||
Ok(_) => {
|
is_active: Set(true),
|
||||||
return Err(PostgresError::OperationFailed("Unexpected transaction result: transaction unexpectedly succeeded".to_string()));
|
metadata: Set(None),
|
||||||
}
|
role_id: Set(None),
|
||||||
}
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
// Verify user was not created (due to rollback)
|
deleted_at: Set(None),
|
||||||
let user_exists = UsersEntity::find_by_id(Uuid::nil()) // Use nil UUID as we don't know the actual ID
|
};
|
||||||
.one(&connection.conn)
|
|
||||||
.await
|
let _created_user = user_model.insert(txn).await?;
|
||||||
.map_err(PostgresError::ConnectionError)?
|
|
||||||
.is_some();
|
Err::<(), DbErr>(DbErr::Custom("Simulated transaction failure".to_string()))
|
||||||
|
})
|
||||||
if user_exists {
|
})
|
||||||
println!(" ⚠️ User found despite rollback - this might indicate an issue");
|
.await;
|
||||||
} else {
|
|
||||||
println!(" ✅ Transaction rollback verified - user not found");
|
match transaction_result {
|
||||||
}
|
Err(e) => {
|
||||||
|
let e_text = format!("{:?}", e);
|
||||||
Ok(())
|
if e_text.contains("Simulated transaction failure") {
|
||||||
}
|
println!(" ✅ Transaction failed as expected, rollback successful");
|
||||||
|
} else {
|
||||||
async fn test_error_handling(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
return Err(PostgresError::OperationFailed(format!(
|
||||||
println!(" ⚠️ Testing error handling...");
|
"Unexpected transaction result: {}",
|
||||||
|
e_text
|
||||||
// Test invalid UUID
|
)));
|
||||||
println!(" 🔍 Testing invalid UUID handling...");
|
}
|
||||||
let invalid_uuid = Uuid::nil(); // This should exist or be handled gracefully
|
}
|
||||||
|
Ok(_) => {
|
||||||
match UsersEntity::find_by_id(invalid_uuid)
|
return Err(PostgresError::OperationFailed(
|
||||||
.one(&connection.conn)
|
"Unexpected transaction result: transaction unexpectedly succeeded"
|
||||||
.await
|
.to_string(),
|
||||||
.map_err(PostgresError::ConnectionError)?
|
));
|
||||||
{
|
}
|
||||||
Some(_) => println!(" ✅ Found user with nil UUID (expected in some cases)"),
|
}
|
||||||
None => println!(" ✅ No user found with nil UUID (expected)"),
|
|
||||||
}
|
let user_exists = UsersEntity::find_by_id(Uuid::nil())
|
||||||
|
.one(&connection.conn)
|
||||||
// Test invalid query
|
.await
|
||||||
println!(" 🔍 Testing invalid query handling...");
|
.map_err(PostgresError::ConnectionError)?
|
||||||
let invalid_statement = sea_orm::Statement::from_string(
|
.is_some();
|
||||||
connection.get_database_backend(),
|
|
||||||
"SELECT * FROM non_existent_table".to_string()
|
if user_exists {
|
||||||
);
|
println!(" ⚠️ User found despite rollback - this might indicate an issue");
|
||||||
|
} else {
|
||||||
match connection.execute(invalid_statement).await {
|
println!(" ✅ Transaction rollback verified - user not found");
|
||||||
Err(_) => println!(" ✅ Invalid query properly handled with error"),
|
}
|
||||||
Ok(_) => println!(" ⚠️ Invalid query unexpectedly succeeded"),
|
|
||||||
}
|
Ok(())
|
||||||
|
}
|
||||||
Ok(())
|
|
||||||
}
|
async fn test_error_handling(
|
||||||
|
connection: &Arc<PostgresConnection>,
|
||||||
/// Additional utility functions for comprehensive testing
|
) -> Result<(), PostgresError> {
|
||||||
pub mod test_utils {
|
println!(" ⚠️ Testing error handling...");
|
||||||
use super::*;
|
|
||||||
|
println!(" 🔍 Testing invalid UUID handling...");
|
||||||
/// Create a test PostgreSQL configuration
|
let invalid_uuid = Uuid::nil();
|
||||||
pub fn create_test_config() -> PostgresConfig {
|
|
||||||
PostgresConfig {
|
match UsersEntity::find_by_id(invalid_uuid)
|
||||||
database_url: "postgres://postgres:postgres@localhost:5432/imphnen_test".to_string(),
|
.one(&connection.conn)
|
||||||
pool_size: 5,
|
.await
|
||||||
connect_timeout: 10,
|
.map_err(PostgresError::ConnectionError)?
|
||||||
idle_timeout: 30,
|
{
|
||||||
max_lifetime: Some(600),
|
Some(_) => {
|
||||||
retry_attempts: 2,
|
println!(" ✅ Found user with nil UUID (expected in some cases)")
|
||||||
retry_delay: 1,
|
}
|
||||||
}
|
None => println!(" ✅ No user found with nil UUID (expected)"),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a test user model
|
println!(" 🔍 Testing invalid query handling...");
|
||||||
pub fn create_test_user_model() -> UserModel {
|
let invalid_statement = sea_orm::Statement::from_string(
|
||||||
UserModel {
|
connection.get_database_backend(),
|
||||||
id: Uuid::new_v4(),
|
"SELECT * FROM non_existent_table".to_string(),
|
||||||
email: format!("test_{}@example.com", Uuid::new_v4()),
|
);
|
||||||
password_hash: "test_password_hash".to_string(),
|
|
||||||
username: format!("testuser_{}", Uuid::new_v4()),
|
match connection.execute(invalid_statement).await {
|
||||||
first_name: Some("Test".to_string()),
|
Err(_) => println!(" ✅ Invalid query properly handled with error"),
|
||||||
last_name: Some("User".to_string()),
|
Ok(_) => println!(" ⚠️ Invalid query unexpectedly succeeded"),
|
||||||
avatar_url: None,
|
}
|
||||||
is_verified: false,
|
|
||||||
is_active: true,
|
Ok(())
|
||||||
metadata: None,
|
}
|
||||||
role_id: None,
|
|
||||||
created_at: Utc::now(),
|
pub mod test_utils {
|
||||||
updated_at: Utc::now(),
|
use super::*;
|
||||||
deleted_at: None,
|
|
||||||
}
|
pub fn create_test_config() -> PostgresConfig {
|
||||||
}
|
PostgresConfig {
|
||||||
|
database_url: "postgres://postgres:postgres@localhost:5432/imphnen_test"
|
||||||
/// Create a test role model
|
.to_string(),
|
||||||
pub fn create_test_role_model() -> RoleModel {
|
pool_size: 5,
|
||||||
RoleModel {
|
connect_timeout: 10,
|
||||||
id: Uuid::new_v4(),
|
idle_timeout: 30,
|
||||||
name: format!("test_role_{}", Uuid::new_v4()),
|
max_lifetime: Some(600),
|
||||||
description: "Test role description".to_string(),
|
retry_attempts: 2,
|
||||||
permissions: Some(serde_json::json!(["test.permission"])),
|
retry_delay: 1,
|
||||||
is_system_role: false,
|
}
|
||||||
is_default: false,
|
}
|
||||||
created_at: Utc::now(),
|
|
||||||
updated_at: Utc::now(),
|
pub fn create_test_user_model() -> UserModel {
|
||||||
deleted_at: None,
|
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()),
|
||||||
#[cfg(test)]
|
first_name: Some("Test".to_string()),
|
||||||
mod tests {
|
last_name: Some("User".to_string()),
|
||||||
use super::*;
|
avatar_url: None,
|
||||||
|
is_verified: false,
|
||||||
#[test]
|
is_active: true,
|
||||||
fn test_create_test_config() {
|
metadata: None,
|
||||||
let config = test_utils::create_test_config();
|
role_id: None,
|
||||||
assert_eq!(config.pool_size, 5);
|
created_at: Utc::now(),
|
||||||
assert_eq!(config.connect_timeout, 10);
|
updated_at: Utc::now(),
|
||||||
assert!(config.database_url.contains("imphnen_test"));
|
deleted_at: None,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#[test]
|
|
||||||
fn test_create_test_user_model() {
|
pub fn create_test_role_model() -> RoleModel {
|
||||||
let user = test_utils::create_test_user_model();
|
RoleModel {
|
||||||
assert!(!user.email.is_empty());
|
id: Uuid::new_v4(),
|
||||||
assert!(!user.username.is_empty());
|
name: format!("test_role_{}", Uuid::new_v4()),
|
||||||
assert!(user.is_active);
|
description: "Test role description".to_string(),
|
||||||
// is_admin field removed; instead, check role-based permission or is_active
|
permissions: Some(serde_json::json!(["test.permission"])),
|
||||||
}
|
is_system_role: false,
|
||||||
|
is_default: false,
|
||||||
#[test]
|
created_at: Utc::now(),
|
||||||
fn test_create_test_role_model() {
|
updated_at: Utc::now(),
|
||||||
let role = test_utils::create_test_role_model();
|
deleted_at: None,
|
||||||
assert!(!role.name.is_empty());
|
}
|
||||||
assert!(role.permissions.is_some());
|
}
|
||||||
assert!(!role.is_system_role);
|
}
|
||||||
}
|
|
||||||
}
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-13
@@ -1,13 +1,10 @@
|
|||||||
use imphnen_gateway::gateway_service;
|
use imphnen_gateway::gateway_service;
|
||||||
use imphnen_libs::axum_init;
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let _ = axum_init(|postgres_conn| async {
|
let _ =
|
||||||
// PostgreSQL is now the primary database - SurrealDB has been completely removed
|
axum_init(|postgres_conn| async { gateway_service(postgres_conn).await }).await;
|
||||||
gateway_service(postgres_conn).await
|
}
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "imphnen-cms"
|
name = "imphnen-cms"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -33,6 +33,9 @@ paginator-rs.workspace = true
|
|||||||
paginator-utils.workspace = true
|
paginator-utils.workspace = true
|
||||||
paginator-sea-orm.workspace = true
|
paginator-sea-orm.workspace = true
|
||||||
paginator-axum.workspace = true
|
paginator-axum.workspace = true
|
||||||
|
sqlx.workspace = true
|
||||||
|
image.workspace = true
|
||||||
|
qrcode.workspace = true
|
||||||
|
|
||||||
[package.metadata.validator.regex]
|
[package.metadata.validator.regex]
|
||||||
VALID_URL_REGEX = "^https?://"
|
VALID_URL_REGEX = "^https?://"
|
||||||
|
|||||||
@@ -1,40 +1,43 @@
|
|||||||
use std::sync::Arc;
|
use crate::events::domain::{EventEntity, EventRepository, EventService};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::PaginationParams;
|
use paginator_rs::PaginationParams;
|
||||||
use paginator_utils::PaginatorResponse;
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use crate::events::domain::{EventEntity, EventRepository, EventService};
|
|
||||||
|
|
||||||
pub struct EventServiceImpl {
|
pub struct EventServiceImpl {
|
||||||
repo: Arc<dyn EventRepository>,
|
repo: Arc<dyn EventRepository>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventServiceImpl {
|
impl EventServiceImpl {
|
||||||
pub fn new(repo: Arc<dyn EventRepository>) -> Self {
|
pub fn new(repo: Arc<dyn EventRepository>) -> Self {
|
||||||
Self { repo }
|
Self { repo }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl EventService for EventServiceImpl {
|
impl EventService for EventServiceImpl {
|
||||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
async fn list(
|
||||||
self.repo.find_all(params).await
|
&self,
|
||||||
}
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||||
|
self.repo.find_all(params).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||||
self.repo.find_by_id(id).await
|
self.repo.find_by_id(id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
self.repo.create(entity).await
|
self.repo.create(entity).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
self.repo.update(entity).await
|
self.repo.update(entity).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
self.repo.delete(id).await
|
self.repo.delete(id).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct EventEntity {
|
pub struct EventEntity {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub detail_link: String,
|
pub detail_link: String,
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
pub is_online: bool,
|
pub is_online: bool,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
pub start_date: DateTime<Utc>,
|
pub start_date: DateTime<Utc>,
|
||||||
pub end_date: DateTime<Utc>,
|
pub end_date: DateTime<Utc>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
use super::event::EventEntity;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::PaginationParams;
|
use paginator_rs::PaginationParams;
|
||||||
use paginator_utils::PaginatorResponse;
|
use paginator_utils::PaginatorResponse;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use super::event::EventEntity;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait EventRepository: Send + Sync {
|
pub trait EventRepository: Send + Sync {
|
||||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
async fn find_all(
|
||||||
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
&self,
|
||||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
params: PaginationParams,
|
||||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), 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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
use super::event::EventEntity;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::PaginationParams;
|
use paginator_rs::PaginationParams;
|
||||||
use paginator_utils::PaginatorResponse;
|
use paginator_utils::PaginatorResponse;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use super::event::EventEntity;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait EventService: Send + Sync {
|
pub trait EventService: Send + Sync {
|
||||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
async fn list(
|
||||||
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
&self,
|
||||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
params: PaginationParams,
|
||||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), 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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,131 +1,131 @@
|
|||||||
|
use crate::events::domain::event::EventEntity;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use imphnen_libs::ZodValidate;
|
use imphnen_libs::ZodValidate;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::events::domain::event::EventEntity;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct EventsCreateRequestDto {
|
pub struct EventsCreateRequestDto {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub detail_link: String,
|
pub detail_link: String,
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
pub end_date: DateTime<Utc>,
|
pub end_date: DateTime<Utc>,
|
||||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
pub start_date: DateTime<Utc>,
|
pub start_date: DateTime<Utc>,
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
pub is_online: bool,
|
pub is_online: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ZodValidate for EventsCreateRequestDto {
|
impl ZodValidate for EventsCreateRequestDto {
|
||||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EventsCreateRequestDto> for EventEntity {
|
impl From<EventsCreateRequestDto> for EventEntity {
|
||||||
fn from(dto: EventsCreateRequestDto) -> Self {
|
fn from(dto: EventsCreateRequestDto) -> Self {
|
||||||
EventEntity {
|
EventEntity {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
detail_link: dto.detail_link,
|
detail_link: dto.detail_link,
|
||||||
price: dto.price,
|
price: dto.price,
|
||||||
is_online: dto.is_online,
|
is_online: dto.is_online,
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
location: dto.location,
|
location: dto.location,
|
||||||
start_date: dto.start_date,
|
start_date: dto.start_date,
|
||||||
end_date: dto.end_date,
|
end_date: dto.end_date,
|
||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct EventsUpdateRequestDto {
|
pub struct EventsUpdateRequestDto {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
pub end_date: DateTime<Utc>,
|
pub end_date: DateTime<Utc>,
|
||||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
pub start_date: DateTime<Utc>,
|
pub start_date: DateTime<Utc>,
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
pub is_online: bool,
|
pub is_online: bool,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub detail_link: String,
|
pub detail_link: String,
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ZodValidate for EventsUpdateRequestDto {
|
impl ZodValidate for EventsUpdateRequestDto {
|
||||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct EventsListItemDto {
|
pub struct EventsListItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub detail_link: String,
|
pub detail_link: String,
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
pub is_online: bool,
|
pub is_online: bool,
|
||||||
pub start_date: String,
|
pub start_date: String,
|
||||||
pub end_date: String,
|
pub end_date: String,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EventEntity> for EventsListItemDto {
|
impl From<EventEntity> for EventsListItemDto {
|
||||||
fn from(e: EventEntity) -> Self {
|
fn from(e: EventEntity) -> Self {
|
||||||
EventsListItemDto {
|
EventsListItemDto {
|
||||||
id: e.id.to_string(),
|
id: e.id.to_string(),
|
||||||
name: e.name,
|
name: e.name,
|
||||||
description: e.description,
|
description: e.description,
|
||||||
detail_link: e.detail_link,
|
detail_link: e.detail_link,
|
||||||
price: e.price,
|
price: e.price,
|
||||||
is_online: e.is_online,
|
is_online: e.is_online,
|
||||||
start_date: e.start_date.to_rfc3339(),
|
start_date: e.start_date.to_rfc3339(),
|
||||||
end_date: e.end_date.to_rfc3339(),
|
end_date: e.end_date.to_rfc3339(),
|
||||||
created_at: e.created_at.to_rfc3339(),
|
created_at: e.created_at.to_rfc3339(),
|
||||||
location: e.location,
|
location: e.location,
|
||||||
is_deleted: e.is_deleted,
|
is_deleted: e.is_deleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct EventsDetailItemDto {
|
pub struct EventsDetailItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub detail_link: String,
|
pub detail_link: String,
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
pub is_online: bool,
|
pub is_online: bool,
|
||||||
pub start_date: String,
|
pub start_date: String,
|
||||||
pub end_date: String,
|
pub end_date: String,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EventEntity> for EventsDetailItemDto {
|
impl From<EventEntity> for EventsDetailItemDto {
|
||||||
fn from(e: EventEntity) -> Self {
|
fn from(e: EventEntity) -> Self {
|
||||||
EventsDetailItemDto {
|
EventsDetailItemDto {
|
||||||
id: e.id.to_string(),
|
id: e.id.to_string(),
|
||||||
name: e.name,
|
name: e.name,
|
||||||
description: e.description,
|
description: e.description,
|
||||||
detail_link: e.detail_link,
|
detail_link: e.detail_link,
|
||||||
price: e.price,
|
price: e.price,
|
||||||
is_online: e.is_online,
|
is_online: e.is_online,
|
||||||
start_date: e.start_date.to_rfc3339(),
|
start_date: e.start_date.to_rfc3339(),
|
||||||
end_date: e.end_date.to_rfc3339(),
|
end_date: e.end_date.to_rfc3339(),
|
||||||
created_at: e.created_at.to_rfc3339(),
|
created_at: e.created_at.to_rfc3339(),
|
||||||
updated_at: e.updated_at.to_rfc3339(),
|
updated_at: e.updated_at.to_rfc3339(),
|
||||||
location: e.location,
|
location: e.location,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
use std::sync::Arc;
|
use super::dto::{
|
||||||
use axum::{Extension, extract::Path, http::HeaderMap, response::{IntoResponse, Response}};
|
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||||
use paginator_axum::PaginationQuery;
|
EventsUpdateRequestDto,
|
||||||
use paginator_utils::PaginatorResponse;
|
};
|
||||||
use uuid::Uuid;
|
use crate::events::domain::EventService;
|
||||||
use imphnen_libs::{AppState, ValidatedJson};
|
use axum::{
|
||||||
use imphnen_utils::{ApiSuccess, ApiPaginated, ApiMessage};
|
Extension,
|
||||||
|
extract::Path,
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
use imphnen_entities::ResponseSuccessDto;
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
use imphnen_utils::AppError;
|
use imphnen_utils::AppError;
|
||||||
use super::dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsUpdateRequestDto};
|
use imphnen_utils::{ApiMessage, ApiPaginated, ApiSuccess};
|
||||||
use crate::events::domain::EventService;
|
use paginator_axum::PaginationQuery;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/v1/cms/landing/events",
|
path = "/v1/landing/cms/events",
|
||||||
params(
|
params(
|
||||||
("page" = Option<i64>, Query, description = "Page number"),
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
@@ -27,24 +35,29 @@ use crate::events::domain::EventService;
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn get_event_list(
|
pub async fn get_event_list(
|
||||||
Extension(service): Extension<Arc<dyn EventService>>,
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
PaginationQuery(params): PaginationQuery,
|
PaginationQuery(params): PaginationQuery,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match service.list(params).await {
|
match service.list(params).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
let mapped = PaginatorResponse {
|
let mapped = PaginatorResponse {
|
||||||
data: result.data.into_iter().map(EventsListItemDto::from).collect::<Vec<_>>(),
|
data: result
|
||||||
meta: result.meta,
|
.data
|
||||||
};
|
.into_iter()
|
||||||
ApiPaginated(mapped).into_response()
|
.map(EventsListItemDto::from)
|
||||||
}
|
.collect::<Vec<_>>(),
|
||||||
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string()).into_response(),
|
meta: result.meta,
|
||||||
}
|
};
|
||||||
|
ApiPaginated(mapped).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string())
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/v1/cms/landing/events/detail/{id}",
|
path = "/v1/landing/cms/events/detail/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Event ID")
|
("id" = String, Path, description = "Event ID")
|
||||||
),
|
),
|
||||||
@@ -54,23 +67,30 @@ pub async fn get_event_list(
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn get_event_by_id(
|
pub async fn get_event_by_id(
|
||||||
Extension(service): Extension<Arc<dyn EventService>>,
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let uuid = match Uuid::parse_str(&id) {
|
let uuid = match Uuid::parse_str(&id) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => return ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, format!("Invalid UUID: {e}")).into_response(),
|
Err(e) => {
|
||||||
};
|
return ApiMessage::new(
|
||||||
match service.get(uuid).await {
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
Ok(event) => ApiSuccess(EventsDetailItemDto::from(event)).into_response(),
|
format!("Invalid UUID: {e}"),
|
||||||
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
)
|
||||||
}
|
.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(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
security(("Bearer" = [])),
|
security(("Bearer" = [])),
|
||||||
path = "/v1/cms/landing/events/create",
|
path = "/v1/landing/cms/events/create",
|
||||||
request_body = EventsCreateRequestDto,
|
request_body = EventsCreateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "[ADMIN] Create new event")
|
(status = 201, description = "[ADMIN] Create new event")
|
||||||
@@ -78,22 +98,22 @@ pub async fn get_event_by_id(
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn post_create_event(
|
pub async fn post_create_event(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(service): Extension<Arc<dyn EventService>>,
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
let entity = payload.into();
|
let entity = payload.into();
|
||||||
service.create(entity).await?;
|
service.create(entity).await?;
|
||||||
Ok(ApiMessage::created("Event created"))
|
Ok(ApiMessage::created("Event created"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
patch,
|
patch,
|
||||||
security(("Bearer" = [])),
|
security(("Bearer" = [])),
|
||||||
path = "/v1/cms/landing/events/update/{id}",
|
path = "/v1/landing/cms/events/update/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Event ID")
|
("id" = String, Path, description = "Event ID")
|
||||||
),
|
),
|
||||||
@@ -104,39 +124,39 @@ pub async fn post_create_event(
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn patch_update_event(
|
pub async fn patch_update_event(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(service): Extension<Arc<dyn EventService>>,
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
let uuid = Uuid::parse_str(&id)
|
let uuid = Uuid::parse_str(&id)
|
||||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
let existing = service.get(uuid).await?;
|
let existing = service.get(uuid).await?;
|
||||||
let entity = crate::events::domain::EventEntity {
|
let entity = crate::events::domain::EventEntity {
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
detail_link: payload.detail_link,
|
detail_link: payload.detail_link,
|
||||||
price: payload.price,
|
price: payload.price,
|
||||||
is_online: payload.is_online,
|
is_online: payload.is_online,
|
||||||
location: payload.location,
|
location: payload.location,
|
||||||
start_date: payload.start_date,
|
start_date: payload.start_date,
|
||||||
end_date: payload.end_date,
|
end_date: payload.end_date,
|
||||||
is_deleted: existing.is_deleted,
|
is_deleted: existing.is_deleted,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
service.update(entity).await?;
|
service.update(entity).await?;
|
||||||
Ok(ApiMessage::ok("Event updated"))
|
Ok(ApiMessage::ok("Event updated"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
security(("Bearer" = [])),
|
security(("Bearer" = [])),
|
||||||
path = "/v1/cms/landing/events/delete/{id}",
|
path = "/v1/landing/cms/events/delete/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Event ID")
|
("id" = String, Path, description = "Event ID")
|
||||||
),
|
),
|
||||||
@@ -146,15 +166,15 @@ pub async fn patch_update_event(
|
|||||||
tag = "Events"
|
tag = "Events"
|
||||||
)]
|
)]
|
||||||
pub async fn delete_event(
|
pub async fn delete_event(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(service): Extension<Arc<dyn EventService>>,
|
Extension(service): Extension<Arc<dyn EventService>>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
let uuid = Uuid::parse_str(&id)
|
let uuid = Uuid::parse_str(&id)
|
||||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
service.delete(uuid).await?;
|
service.delete(uuid).await?;
|
||||||
Ok(ApiMessage::ok("Event deleted"))
|
Ok(ApiMessage::ok("Event deleted"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ pub mod dto;
|
|||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
pub use routes::{events_public_routes, events_protected_routes};
|
pub use routes::{events_protected_routes, events_public_routes};
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
use std::sync::Arc;
|
use super::handlers::{
|
||||||
use axum::{Router, routing::{delete, get, patch, post}, Extension};
|
delete_event, get_event_by_id, get_event_list, patch_update_event,
|
||||||
use sea_orm::DatabaseConnection;
|
post_create_event,
|
||||||
|
};
|
||||||
use crate::events::application::EventServiceImpl;
|
use crate::events::application::EventServiceImpl;
|
||||||
use crate::events::domain::EventService;
|
use crate::events::domain::EventService;
|
||||||
use crate::events::infrastructure::persistence::PostgresEventRepository;
|
use crate::events::infrastructure::persistence::PostgresEventRepository;
|
||||||
use super::handlers::{
|
use axum::{
|
||||||
delete_event, get_event_by_id, get_event_list, patch_update_event, post_create_event,
|
Extension, Router,
|
||||||
|
routing::{delete, get, patch, post},
|
||||||
};
|
};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn build_service(db: DatabaseConnection) -> Arc<dyn EventService> {
|
fn build_service(db: DatabaseConnection) -> Arc<dyn EventService> {
|
||||||
let repo = Arc::new(PostgresEventRepository::new(db));
|
let repo = Arc::new(PostgresEventRepository::new(db));
|
||||||
Arc::new(EventServiceImpl::new(repo))
|
Arc::new(EventServiceImpl::new(repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn events_public_routes(db: DatabaseConnection) -> Router {
|
pub fn events_public_routes(db: DatabaseConnection) -> Router {
|
||||||
let service = build_service(db);
|
let service = build_service(db);
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/cms/landing/events", get(get_event_list))
|
.route("/events", get(get_event_list))
|
||||||
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
.route("/events/detail/{id}", get(get_event_by_id))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn events_protected_routes(db: DatabaseConnection) -> Router {
|
pub fn events_protected_routes(db: DatabaseConnection) -> Router {
|
||||||
let service = build_service(db);
|
let service = build_service(db);
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/cms/landing/events/create", post(post_create_event))
|
.route("/events/create", post(post_create_event))
|
||||||
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
.route("/events/update/{id}", patch(patch_update_event))
|
||||||
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
.route("/events/delete/{id}", delete(delete_event))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,149 +1,161 @@
|
|||||||
use std::sync::Arc;
|
use crate::events::domain::{event::EventEntity, repository::EventRepository};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sea_orm::prelude::*;
|
use imphnen_entities::seaorm::common::events::{
|
||||||
use sea_orm::{ActiveValue, Order, QueryOrder, PaginatorTrait};
|
ActiveModel as EventsActiveModel, Column as EventsColumn, Entity as EventsEntity,
|
||||||
|
Model as EventsModel,
|
||||||
|
};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::{PaginationParams, SortDirection};
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, Order, PaginatorTrait, QueryOrder};
|
||||||
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use imphnen_entities::seaorm::common::events::{
|
|
||||||
Entity as EventsEntity, Column as EventsColumn,
|
|
||||||
ActiveModel as EventsActiveModel, Model as EventsModel,
|
|
||||||
};
|
|
||||||
use crate::events::domain::{event::EventEntity, repository::EventRepository};
|
|
||||||
|
|
||||||
fn to_entity(model: EventsModel) -> EventEntity {
|
fn to_entity(model: EventsModel) -> EventEntity {
|
||||||
EventEntity {
|
EventEntity {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
description: model.description,
|
description: model.description,
|
||||||
detail_link: model.detail_link,
|
detail_link: model.detail_link,
|
||||||
price: model.price,
|
price: model.price,
|
||||||
is_online: model.is_online,
|
is_online: model.is_online,
|
||||||
is_deleted: model.is_deleted,
|
is_deleted: model.is_deleted,
|
||||||
location: model.location,
|
location: model.location,
|
||||||
start_date: model.start_date,
|
start_date: model.start_date,
|
||||||
end_date: model.end_date,
|
end_date: model.end_date,
|
||||||
created_at: model.created_at,
|
created_at: model.created_at,
|
||||||
updated_at: model.updated_at,
|
updated_at: model.updated_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PostgresEventRepository {
|
pub struct PostgresEventRepository {
|
||||||
db: Arc<DatabaseConnection>,
|
db: Arc<DatabaseConnection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostgresEventRepository {
|
impl PostgresEventRepository {
|
||||||
pub fn new(db: DatabaseConnection) -> Self {
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
Self { db: Arc::new(db) }
|
Self { db: Arc::new(db) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl EventRepository for PostgresEventRepository {
|
impl EventRepository for PostgresEventRepository {
|
||||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
async fn find_all(
|
||||||
let page = params.page.max(1);
|
&self,
|
||||||
let per_page = params.per_page.clamp(1, 100);
|
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()
|
let mut query = EventsEntity::find().filter(EventsColumn::IsDeleted.eq(false));
|
||||||
.filter(EventsColumn::IsDeleted.eq(false));
|
|
||||||
|
|
||||||
if let Some(ref search) = params.search {
|
if let Some(ref search) = params.search {
|
||||||
query = query.filter(EventsColumn::Name.contains(&search.query));
|
query = query.filter(EventsColumn::Name.contains(&search.query));
|
||||||
}
|
}
|
||||||
|
|
||||||
query = match params.sort_by.as_deref() {
|
query = match params.sort_by.as_deref() {
|
||||||
Some("name") => match params.sort_direction {
|
Some("name") => match params.sort_direction {
|
||||||
Some(SortDirection::Desc) => query.order_by(EventsColumn::Name, Order::Desc),
|
Some(SortDirection::Desc) => query.order_by(EventsColumn::Name, Order::Desc),
|
||||||
_ => query.order_by(EventsColumn::Name, Order::Asc),
|
_ => query.order_by(EventsColumn::Name, Order::Asc),
|
||||||
},
|
},
|
||||||
_ => match params.sort_direction {
|
_ => match params.sort_direction {
|
||||||
Some(SortDirection::Asc) => query.order_by(EventsColumn::CreatedAt, Order::Asc),
|
Some(SortDirection::Asc) => {
|
||||||
_ => query.order_by(EventsColumn::CreatedAt, Order::Desc),
|
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 paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
let total = paginator.num_items().await
|
let total = paginator
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.num_items()
|
||||||
let events = paginator.fetch_page((page - 1) as u64).await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.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 data = events.into_iter().map(to_entity).collect();
|
||||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
Ok(PaginatorResponse { data, meta })
|
Ok(PaginatorResponse { data, meta })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError> {
|
||||||
let event = EventsEntity::find_by_id(id)
|
let event = EventsEntity::find_by_id(id)
|
||||||
.filter(EventsColumn::IsDeleted.eq(false))
|
.filter(EventsColumn::IsDeleted.eq(false))
|
||||||
.one(self.db.as_ref())
|
.one(self.db.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?;
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?;
|
||||||
|
|
||||||
Ok(to_entity(event))
|
Ok(to_entity(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
async fn create(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
let active_model = EventsActiveModel {
|
let active_model = EventsActiveModel {
|
||||||
id: ActiveValue::Set(entity.id),
|
id: ActiveValue::Set(entity.id),
|
||||||
name: ActiveValue::Set(entity.name),
|
name: ActiveValue::Set(entity.name),
|
||||||
description: ActiveValue::Set(entity.description),
|
description: ActiveValue::Set(entity.description),
|
||||||
detail_link: ActiveValue::Set(entity.detail_link),
|
detail_link: ActiveValue::Set(entity.detail_link),
|
||||||
price: ActiveValue::Set(entity.price),
|
price: ActiveValue::Set(entity.price),
|
||||||
is_online: ActiveValue::Set(entity.is_online),
|
is_online: ActiveValue::Set(entity.is_online),
|
||||||
is_deleted: ActiveValue::Set(false),
|
is_deleted: ActiveValue::Set(false),
|
||||||
location: ActiveValue::Set(entity.location),
|
location: ActiveValue::Set(entity.location),
|
||||||
start_date: ActiveValue::Set(entity.start_date),
|
start_date: ActiveValue::Set(entity.start_date),
|
||||||
end_date: ActiveValue::Set(entity.end_date),
|
end_date: ActiveValue::Set(entity.end_date),
|
||||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
};
|
};
|
||||||
|
|
||||||
EventsEntity::insert(active_model)
|
EventsEntity::insert(active_model)
|
||||||
.exec(self.db.as_ref())
|
.exec(self.db.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
async fn update(&self, entity: EventEntity) -> Result<(), AppError> {
|
||||||
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(entity.id)
|
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(entity.id)
|
||||||
.one(self.db.as_ref())
|
.one(self.db.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
active_model.name = ActiveValue::Set(entity.name);
|
active_model.name = ActiveValue::Set(entity.name);
|
||||||
active_model.description = ActiveValue::Set(entity.description);
|
active_model.description = ActiveValue::Set(entity.description);
|
||||||
active_model.detail_link = ActiveValue::Set(entity.detail_link);
|
active_model.detail_link = ActiveValue::Set(entity.detail_link);
|
||||||
active_model.price = ActiveValue::Set(entity.price);
|
active_model.price = ActiveValue::Set(entity.price);
|
||||||
active_model.is_online = ActiveValue::Set(entity.is_online);
|
active_model.is_online = ActiveValue::Set(entity.is_online);
|
||||||
active_model.location = ActiveValue::Set(entity.location);
|
active_model.location = ActiveValue::Set(entity.location);
|
||||||
active_model.start_date = ActiveValue::Set(entity.start_date);
|
active_model.start_date = ActiveValue::Set(entity.start_date);
|
||||||
active_model.end_date = ActiveValue::Set(entity.end_date);
|
active_model.end_date = ActiveValue::Set(entity.end_date);
|
||||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
active_model.update(self.db.as_ref()).await
|
active_model
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.update(self.db.as_ref())
|
||||||
Ok(())
|
.await
|
||||||
}
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(id)
|
let mut active_model: EventsActiveModel = EventsEntity::find_by_id(id)
|
||||||
.one(self.db.as_ref())
|
.one(self.db.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
.ok_or_else(|| AppError::NotFoundError("Event not found".to_string()))?
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
active_model.is_deleted = ActiveValue::Set(true);
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
active_model.update(self.db.as_ref()).await
|
active_model
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.update(self.db.as_ref())
|
||||||
Ok(())
|
.await
|
||||||
}
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ pub mod application;
|
|||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod infrastructure;
|
pub mod infrastructure;
|
||||||
|
|
||||||
pub use infrastructure::http::{events_public_routes, events_protected_routes};
|
pub use infrastructure::http::{events_protected_routes, events_public_routes};
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub mod roadmap;
|
||||||
pub mod testimonials;
|
pub mod testimonials;
|
||||||
|
pub mod qr;
|
||||||
|
|
||||||
pub use events::{events_public_routes, events_protected_routes};
|
pub use events::{events_protected_routes, events_public_routes};
|
||||||
pub use testimonials::{testimonials_public_routes, testimonials_protected_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;
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
use crate::roadmap::domain::{roadmap::RoadmapEntity, repository::RoadmapRepository};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use imphnen_entities::seaorm::common::roadmap_items::{
|
||||||
|
ActiveModel as RoadmapActiveModel, Column as RoadmapColumn, Entity as RoadmapEntity_,
|
||||||
|
Model as RoadmapModel,
|
||||||
|
};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, Order, PaginatorTrait, QueryOrder};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn to_entity(model: RoadmapModel) -> RoadmapEntity {
|
||||||
|
RoadmapEntity {
|
||||||
|
id: model.id,
|
||||||
|
title: model.title,
|
||||||
|
description: model.description,
|
||||||
|
status: model.status,
|
||||||
|
votes: model.votes,
|
||||||
|
is_deleted: model.is_deleted,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresRoadmapRepository {
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresRoadmapRepository {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db: Arc::new(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RoadmapRepository for PostgresRoadmapRepository {
|
||||||
|
async fn find_all(
|
||||||
|
&self,
|
||||||
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<RoadmapEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
|
let mut query = RoadmapEntity_::find().filter(RoadmapColumn::IsDeleted.eq(false));
|
||||||
|
|
||||||
|
if let Some(ref search) = params.search {
|
||||||
|
query = query.filter(RoadmapColumn::Title.contains(&search.query));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = match params.sort_by.as_deref() {
|
||||||
|
Some("title") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Desc) => query.order_by(RoadmapColumn::Title, Order::Desc),
|
||||||
|
_ => query.order_by(RoadmapColumn::Title, Order::Asc),
|
||||||
|
},
|
||||||
|
Some("votes") => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => query.order_by(RoadmapColumn::Votes, Order::Asc),
|
||||||
|
_ => query.order_by(RoadmapColumn::Votes, Order::Desc),
|
||||||
|
},
|
||||||
|
_ => match params.sort_direction {
|
||||||
|
Some(SortDirection::Asc) => {
|
||||||
|
query.order_by(RoadmapColumn::CreatedAt, Order::Asc)
|
||||||
|
}
|
||||||
|
_ => query.order_by(RoadmapColumn::CreatedAt, Order::Desc),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
|
let total = paginator
|
||||||
|
.num_items()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let items = paginator
|
||||||
|
.fetch_page((page - 1) as u64)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let data = items.into_iter().map(to_entity).collect();
|
||||||
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
|
Ok(PaginatorResponse { data, meta })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<RoadmapEntity, AppError> {
|
||||||
|
let item = RoadmapEntity_::find_by_id(id)
|
||||||
|
.filter(RoadmapColumn::IsDeleted.eq(false))
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(to_entity(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, entity: RoadmapEntity) -> Result<(), AppError> {
|
||||||
|
let active_model = RoadmapActiveModel {
|
||||||
|
id: ActiveValue::Set(entity.id),
|
||||||
|
title: ActiveValue::Set(entity.title),
|
||||||
|
description: ActiveValue::Set(entity.description),
|
||||||
|
status: ActiveValue::Set(entity.status),
|
||||||
|
votes: ActiveValue::Set(0),
|
||||||
|
is_deleted: ActiveValue::Set(false),
|
||||||
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
RoadmapEntity_::insert(active_model)
|
||||||
|
.exec(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, entity: RoadmapEntity) -> Result<(), AppError> {
|
||||||
|
let mut active_model: RoadmapActiveModel = RoadmapEntity_::find_by_id(entity.id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.title = ActiveValue::Set(entity.title);
|
||||||
|
active_model.description = ActiveValue::Set(entity.description);
|
||||||
|
active_model.status = ActiveValue::Set(entity.status);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let mut active_model: RoadmapActiveModel = RoadmapEntity_::find_by_id(id)
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn increment_votes(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
|
let item = RoadmapEntity_::find_by_id(id)
|
||||||
|
.filter(RoadmapColumn::IsDeleted.eq(false))
|
||||||
|
.one(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
|
.ok_or_else(|| AppError::NotFoundError("Roadmap item not found".to_string()))?;
|
||||||
|
|
||||||
|
let new_votes = item.votes + 1;
|
||||||
|
let mut active_model: RoadmapActiveModel = item.into();
|
||||||
|
active_model.votes = ActiveValue::Set(new_votes);
|
||||||
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
active_model
|
||||||
|
.update(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod infrastructure;
|
||||||
|
|
||||||
|
pub use infrastructure::http::{roadmap_protected_routes, roadmap_public_routes};
|
||||||
@@ -1,40 +1,48 @@
|
|||||||
use std::sync::Arc;
|
use crate::testimonials::domain::{
|
||||||
|
TestimonialEntity, TestimonialRepository, TestimonialService,
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::PaginationParams;
|
use paginator_rs::PaginationParams;
|
||||||
use paginator_utils::PaginatorResponse;
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use crate::testimonials::domain::{TestimonialEntity, TestimonialRepository, TestimonialService};
|
|
||||||
|
|
||||||
pub struct TestimonialServiceImpl {
|
pub struct TestimonialServiceImpl {
|
||||||
repo: Arc<dyn TestimonialRepository>,
|
repo: Arc<dyn TestimonialRepository>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestimonialServiceImpl {
|
impl TestimonialServiceImpl {
|
||||||
pub fn new(repo: Arc<dyn TestimonialRepository>) -> Self {
|
pub fn new(repo: Arc<dyn TestimonialRepository>) -> Self {
|
||||||
Self { repo }
|
Self { repo }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TestimonialService for TestimonialServiceImpl {
|
impl TestimonialService for TestimonialServiceImpl {
|
||||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
async fn list(
|
||||||
self.repo.find_all(params).await
|
&self,
|
||||||
}
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
||||||
|
self.repo.find_all(params).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
||||||
self.repo.find_by_id(id).await
|
self.repo.find_by_id(id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError> {
|
async fn create(
|
||||||
self.repo.create(entity).await
|
&self,
|
||||||
}
|
entity: TestimonialEntity,
|
||||||
|
) -> Result<TestimonialEntity, AppError> {
|
||||||
|
self.repo.create(entity).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
||||||
self.repo.update(entity).await
|
self.repo.update(entity).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
self.repo.delete(id).await
|
self.repo.delete(id).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pub mod testimonial;
|
|
||||||
pub mod repository;
|
pub mod repository;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
pub mod testimonial;
|
||||||
|
|
||||||
pub use testimonial::TestimonialEntity;
|
|
||||||
pub use repository::TestimonialRepository;
|
pub use repository::TestimonialRepository;
|
||||||
pub use service::TestimonialService;
|
pub use service::TestimonialService;
|
||||||
|
pub use testimonial::TestimonialEntity;
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
|
use super::testimonial::TestimonialEntity;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::PaginationParams;
|
use paginator_rs::PaginationParams;
|
||||||
use paginator_utils::PaginatorResponse;
|
use paginator_utils::PaginatorResponse;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use super::testimonial::TestimonialEntity;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TestimonialRepository: Send + Sync {
|
pub trait TestimonialRepository: Send + Sync {
|
||||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
async fn find_all(
|
||||||
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
&self,
|
||||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError>;
|
params: PaginationParams,
|
||||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn create(
|
||||||
|
&self,
|
||||||
|
entity: TestimonialEntity,
|
||||||
|
) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
|
use super::testimonial::TestimonialEntity;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::PaginationParams;
|
use paginator_rs::PaginationParams;
|
||||||
use paginator_utils::PaginatorResponse;
|
use paginator_utils::PaginatorResponse;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use super::testimonial::TestimonialEntity;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TestimonialService: Send + Sync {
|
pub trait TestimonialService: Send + Sync {
|
||||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
async fn list(
|
||||||
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
&self,
|
||||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError>;
|
params: PaginationParams,
|
||||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
) -> Result<PaginatorResponse<TestimonialEntity>, AppError>;
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
async fn get(&self, id: Uuid) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn create(
|
||||||
|
&self,
|
||||||
|
entity: TestimonialEntity,
|
||||||
|
) -> Result<TestimonialEntity, AppError>;
|
||||||
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError>;
|
||||||
|
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TestimonialEntity {
|
pub struct TestimonialEntity {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
pub user_fullname: String,
|
pub user_fullname: String,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,83 @@
|
|||||||
|
use crate::testimonials::domain::testimonial::TestimonialEntity;
|
||||||
use imphnen_libs::ZodValidate;
|
use imphnen_libs::ZodValidate;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use zod_rs::prelude::*;
|
use zod_rs::prelude::*;
|
||||||
use crate::testimonials::domain::testimonial::TestimonialEntity;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
pub struct TestimonialsCreateRequestDto {
|
pub struct TestimonialsCreateRequestDto {
|
||||||
#[zod(min_length(1), max_length(100))]
|
#[zod(min_length(1), max_length(100))]
|
||||||
pub role: String,
|
pub role: String,
|
||||||
#[zod(min_length(1), max_length(1000))]
|
#[zod(min_length(1), max_length(1000))]
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ZodValidate for TestimonialsCreateRequestDto {
|
impl ZodValidate for TestimonialsCreateRequestDto {
|
||||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||||
pub struct TestimonialsUpdateRequestDto {
|
pub struct TestimonialsUpdateRequestDto {
|
||||||
#[zod(min_length(1), max_length(100))]
|
#[zod(min_length(1), max_length(100))]
|
||||||
pub role: String,
|
pub role: String,
|
||||||
#[zod(min_length(1), max_length(1000))]
|
#[zod(min_length(1), max_length(1000))]
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ZodValidate for TestimonialsUpdateRequestDto {
|
impl ZodValidate for TestimonialsUpdateRequestDto {
|
||||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct TestimonialsListItemDto {
|
pub struct TestimonialsListItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
pub user_fullname: String,
|
pub user_fullname: String,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TestimonialEntity> for TestimonialsListItemDto {
|
impl From<TestimonialEntity> for TestimonialsListItemDto {
|
||||||
fn from(e: TestimonialEntity) -> Self {
|
fn from(e: TestimonialEntity) -> Self {
|
||||||
TestimonialsListItemDto {
|
TestimonialsListItemDto {
|
||||||
id: e.id.to_string(),
|
id: e.id.to_string(),
|
||||||
user_id: e.user_id.to_string(),
|
user_id: e.user_id.to_string(),
|
||||||
user_fullname: e.user_fullname,
|
user_fullname: e.user_fullname,
|
||||||
role: e.role,
|
role: e.role,
|
||||||
content: e.content,
|
content: e.content,
|
||||||
created_at: e.created_at,
|
created_at: e.created_at,
|
||||||
is_deleted: e.is_deleted,
|
is_deleted: e.is_deleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct TestimonialsDetailItemDto {
|
pub struct TestimonialsDetailItemDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
pub user_fullname: String,
|
pub user_fullname: String,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TestimonialEntity> for TestimonialsDetailItemDto {
|
impl From<TestimonialEntity> for TestimonialsDetailItemDto {
|
||||||
fn from(e: TestimonialEntity) -> Self {
|
fn from(e: TestimonialEntity) -> Self {
|
||||||
TestimonialsDetailItemDto {
|
TestimonialsDetailItemDto {
|
||||||
id: e.id.to_string(),
|
id: e.id.to_string(),
|
||||||
user_id: e.user_id.to_string(),
|
user_id: e.user_id.to_string(),
|
||||||
user_fullname: e.user_fullname,
|
user_fullname: e.user_fullname,
|
||||||
role: e.role,
|
role: e.role,
|
||||||
content: e.content,
|
content: e.content,
|
||||||
created_at: e.created_at,
|
created_at: e.created_at,
|
||||||
updated_at: e.updated_at,
|
updated_at: e.updated_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use axum::{Extension, extract::Path, http::HeaderMap, http::StatusCode, response::{IntoResponse, Response}};
|
|
||||||
use paginator_axum::PaginationQuery;
|
|
||||||
use paginator_utils::PaginatorResponse;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use imphnen_libs::{AppState, ValidatedJson};
|
|
||||||
use imphnen_utils::{ApiSuccess, ApiCreated, ApiPaginated, ApiMessage, extract_email};
|
|
||||||
use imphnen_entities::ResponseSuccessDto;
|
|
||||||
use imphnen_iam::require_auth;
|
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use super::dto::{
|
use super::dto::{
|
||||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
TestimonialsCreateRequestDto, TestimonialsDetailItemDto, TestimonialsListItemDto,
|
||||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
TestimonialsUpdateRequestDto,
|
||||||
};
|
};
|
||||||
use crate::testimonials::domain::{TestimonialEntity, TestimonialService};
|
use crate::testimonials::domain::{TestimonialEntity, TestimonialService};
|
||||||
|
use axum::{
|
||||||
|
Extension,
|
||||||
|
extract::Path,
|
||||||
|
http::HeaderMap,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use imphnen_entities::ResponseSuccessDto;
|
||||||
|
use imphnen_iam::require_auth;
|
||||||
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
use imphnen_utils::{
|
||||||
|
ApiCreated, ApiMessage, ApiPaginated, ApiSuccess, extract_email,
|
||||||
|
};
|
||||||
|
use paginator_axum::PaginationQuery;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/v1/cms/landing/testimonials",
|
path = "/v1/landing/cms/testimonials",
|
||||||
params(
|
params(
|
||||||
("page" = Option<i64>, Query, description = "Page number"),
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
@@ -30,27 +38,31 @@ use crate::testimonials::domain::{TestimonialEntity, TestimonialService};
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn get_testimonial_list(
|
pub async fn get_testimonial_list(
|
||||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
PaginationQuery(params): PaginationQuery,
|
PaginationQuery(params): PaginationQuery,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match service.list(params).await {
|
match service.list(params).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
let mapped = PaginatorResponse {
|
let mapped = PaginatorResponse {
|
||||||
data: result.data.into_iter()
|
data: result
|
||||||
.filter(|e| !e.is_deleted)
|
.data
|
||||||
.map(TestimonialsListItemDto::from)
|
.into_iter()
|
||||||
.collect::<Vec<_>>(),
|
.filter(|e| !e.is_deleted)
|
||||||
meta: result.meta,
|
.map(TestimonialsListItemDto::from)
|
||||||
};
|
.collect::<Vec<_>>(),
|
||||||
ApiPaginated(mapped).into_response()
|
meta: result.meta,
|
||||||
}
|
};
|
||||||
Err(e) => ApiMessage::new(StatusCode::BAD_REQUEST, e.to_string()).into_response(),
|
ApiPaginated(mapped).into_response()
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ApiMessage::new(StatusCode::BAD_REQUEST, e.to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/v1/cms/landing/testimonials/detail/{id}",
|
path = "/v1/landing/cms/testimonials/detail/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Testimonial ID")
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
),
|
),
|
||||||
@@ -60,26 +72,31 @@ pub async fn get_testimonial_list(
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn get_testimonial_by_id(
|
pub async fn get_testimonial_by_id(
|
||||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let uuid = match Uuid::parse_str(&id) {
|
let uuid = match Uuid::parse_str(&id) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => return ApiMessage::new(StatusCode::BAD_REQUEST, format!("Invalid UUID: {e}")).into_response(),
|
Err(e) => {
|
||||||
};
|
return ApiMessage::new(StatusCode::BAD_REQUEST, format!("Invalid UUID: {e}"))
|
||||||
match service.get(uuid).await {
|
.into_response();
|
||||||
Ok(t) if !t.is_deleted => {
|
}
|
||||||
ApiSuccess(TestimonialsDetailItemDto::from(t)).into_response()
|
};
|
||||||
}
|
match service.get(uuid).await {
|
||||||
Ok(_) => ApiMessage::new(StatusCode::NOT_FOUND, "Testimonial not found").into_response(),
|
Ok(t) if !t.is_deleted => {
|
||||||
Err(e) => ApiMessage::new(StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
ApiSuccess(TestimonialsDetailItemDto::from(t)).into_response()
|
||||||
}
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
ApiMessage::new(StatusCode::NOT_FOUND, "Testimonial not found").into_response()
|
||||||
|
}
|
||||||
|
Err(e) => ApiMessage::new(StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
security(("Bearer" = [])),
|
security(("Bearer" = [])),
|
||||||
path = "/v1/cms/landing/testimonials/create",
|
path = "/v1/landing/cms/testimonials/create",
|
||||||
request_body = TestimonialsCreateRequestDto,
|
request_body = TestimonialsCreateRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "[USER] Create new testimonial", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
(status = 201, description = "[USER] Create new testimonial", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||||
@@ -87,38 +104,42 @@ pub async fn get_testimonial_by_id(
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn post_create_testimonial(
|
pub async fn post_create_testimonial(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
require_auth!(headers.clone(), state, {
|
require_auth!(headers.clone(), state, {
|
||||||
let email = extract_email(&headers)
|
let email = extract_email(&headers).ok_or_else(|| {
|
||||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
AppError::AuthenticationError("Token tidak valid".to_string())
|
||||||
let user_info = state.user_lookup_service.get_user_by_email(&email, &state).await
|
})?;
|
||||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
let user_info = state
|
||||||
let user = user_info.basic_info;
|
.user_lookup_service
|
||||||
let user_id = Uuid::parse_str(&user.id)
|
.get_user_by_email(&email, &state)
|
||||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {e}")))?;
|
.await
|
||||||
let entity = TestimonialEntity {
|
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||||
id: Uuid::new_v4(),
|
let user = user_info.basic_info;
|
||||||
user_id,
|
let user_id = Uuid::parse_str(&user.id)
|
||||||
user_fullname: user.fullname.clone(),
|
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {e}")))?;
|
||||||
role: payload.role,
|
let entity = TestimonialEntity {
|
||||||
content: payload.content,
|
id: Uuid::new_v4(),
|
||||||
is_deleted: false,
|
user_id,
|
||||||
created_at: chrono::Utc::now().to_rfc3339(),
|
user_fullname: user.fullname.clone(),
|
||||||
updated_at: chrono::Utc::now().to_rfc3339(),
|
role: payload.role,
|
||||||
};
|
content: payload.content,
|
||||||
let created = service.create(entity).await?;
|
is_deleted: false,
|
||||||
Ok(ApiCreated(TestimonialsDetailItemDto::from(created)))
|
created_at: chrono::Utc::now().to_rfc3339(),
|
||||||
})
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
let created = service.create(entity).await?;
|
||||||
|
Ok(ApiCreated(TestimonialsDetailItemDto::from(created)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
patch,
|
patch,
|
||||||
security(("Bearer" = [])),
|
security(("Bearer" = [])),
|
||||||
path = "/v1/cms/landing/testimonials/update/{id}",
|
path = "/v1/landing/cms/testimonials/update/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Testimonial ID")
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
),
|
),
|
||||||
@@ -129,35 +150,35 @@ pub async fn post_create_testimonial(
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn patch_update_testimonial(
|
pub async fn patch_update_testimonial(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
require_auth!(headers, state, {
|
require_auth!(headers, state, {
|
||||||
let uuid = Uuid::parse_str(&id)
|
let uuid = Uuid::parse_str(&id)
|
||||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
let existing = service.get(uuid).await?;
|
let existing = service.get(uuid).await?;
|
||||||
let entity = TestimonialEntity {
|
let entity = TestimonialEntity {
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
user_id: existing.user_id,
|
user_id: existing.user_id,
|
||||||
user_fullname: existing.user_fullname,
|
user_fullname: existing.user_fullname,
|
||||||
role: payload.role,
|
role: payload.role,
|
||||||
content: payload.content,
|
content: payload.content,
|
||||||
is_deleted: existing.is_deleted,
|
is_deleted: existing.is_deleted,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at: chrono::Utc::now().to_rfc3339(),
|
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||||
};
|
};
|
||||||
service.update(entity).await?;
|
service.update(entity).await?;
|
||||||
Ok(ApiMessage::ok("Testimonial updated"))
|
Ok(ApiMessage::ok("Testimonial updated"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
security(("Bearer" = [])),
|
security(("Bearer" = [])),
|
||||||
path = "/v1/cms/landing/testimonials/delete/{id}",
|
path = "/v1/landing/cms/testimonials/delete/{id}",
|
||||||
params(
|
params(
|
||||||
("id" = String, Path, description = "Testimonial ID")
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
),
|
),
|
||||||
@@ -167,15 +188,15 @@ pub async fn patch_update_testimonial(
|
|||||||
tag = "Testimonials"
|
tag = "Testimonials"
|
||||||
)]
|
)]
|
||||||
pub async fn delete_testimonial(
|
pub async fn delete_testimonial(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
Extension(service): Extension<Arc<dyn TestimonialService>>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
require_auth!(headers, state, {
|
require_auth!(headers, state, {
|
||||||
let uuid = Uuid::parse_str(&id)
|
let uuid = Uuid::parse_str(&id)
|
||||||
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
|
||||||
service.delete(uuid).await?;
|
service.delete(uuid).await?;
|
||||||
Ok(ApiMessage::ok("Testimonial deleted"))
|
Ok(ApiMessage::ok("Testimonial deleted"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ pub mod dto;
|
|||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
pub use routes::{testimonials_public_routes, testimonials_protected_routes};
|
pub use routes::{testimonials_protected_routes, testimonials_public_routes};
|
||||||
|
|||||||
@@ -1,32 +1,47 @@
|
|||||||
use std::sync::Arc;
|
use super::handlers::{
|
||||||
use axum::{Router, routing::{delete, get, patch, post}, Extension};
|
delete_testimonial, get_testimonial_by_id, get_testimonial_list,
|
||||||
use sea_orm::DatabaseConnection;
|
patch_update_testimonial, post_create_testimonial,
|
||||||
|
};
|
||||||
use crate::testimonials::application::TestimonialServiceImpl;
|
use crate::testimonials::application::TestimonialServiceImpl;
|
||||||
use crate::testimonials::domain::TestimonialService;
|
use crate::testimonials::domain::TestimonialService;
|
||||||
use crate::testimonials::infrastructure::persistence::PostgresTestimonialRepository;
|
use crate::testimonials::infrastructure::persistence::PostgresTestimonialRepository;
|
||||||
use super::handlers::{
|
use axum::{
|
||||||
delete_testimonial, get_testimonial_by_id, get_testimonial_list,
|
Extension, Router,
|
||||||
patch_update_testimonial, post_create_testimonial,
|
routing::{delete, get, patch, post},
|
||||||
};
|
};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn build_service(db: DatabaseConnection) -> Arc<dyn TestimonialService> {
|
fn build_service(db: DatabaseConnection) -> Arc<dyn TestimonialService> {
|
||||||
let repo = Arc::new(PostgresTestimonialRepository::new(db));
|
let repo = Arc::new(PostgresTestimonialRepository::new(db));
|
||||||
Arc::new(TestimonialServiceImpl::new(repo))
|
Arc::new(TestimonialServiceImpl::new(repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn testimonials_public_routes(db: DatabaseConnection) -> Router {
|
pub fn testimonials_public_routes(db: DatabaseConnection) -> Router {
|
||||||
let service = build_service(db);
|
let service = build_service(db);
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
.route("/testimonials", get(get_testimonial_list))
|
||||||
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
.route(
|
||||||
.layer(Extension(service))
|
"/testimonials/detail/{id}",
|
||||||
|
get(get_testimonial_by_id),
|
||||||
|
)
|
||||||
|
.layer(Extension(service))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn testimonials_protected_routes(db: DatabaseConnection) -> Router {
|
pub fn testimonials_protected_routes(db: DatabaseConnection) -> Router {
|
||||||
let service = build_service(db);
|
let service = build_service(db);
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
.route(
|
||||||
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
"/testimonials/create",
|
||||||
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
post(post_create_testimonial),
|
||||||
.layer(Extension(service))
|
)
|
||||||
|
.route(
|
||||||
|
"/testimonials/update/{id}",
|
||||||
|
patch(patch_update_testimonial),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/testimonials/delete/{id}",
|
||||||
|
delete(delete_testimonial),
|
||||||
|
)
|
||||||
|
.layer(Extension(service))
|
||||||
}
|
}
|
||||||
|
|||||||
+161
-129
@@ -1,159 +1,191 @@
|
|||||||
use std::sync::Arc;
|
use crate::testimonials::domain::{
|
||||||
|
repository::TestimonialRepository, testimonial::TestimonialEntity,
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sea_orm::prelude::*;
|
use imphnen_entities::seaorm::auth::users::Entity as UsersEntity;
|
||||||
use sea_orm::{ActiveValue, QueryOrder, PaginatorTrait};
|
use imphnen_entities::seaorm::common::testimonials::{
|
||||||
|
ActiveModel as TestimonialsActiveModel, Column as TestimonialsColumn,
|
||||||
|
Entity as TestimonialsEntity,
|
||||||
|
};
|
||||||
|
use imphnen_utils::AppError;
|
||||||
use paginator_rs::{PaginationParams, SortDirection};
|
use paginator_rs::{PaginationParams, SortDirection};
|
||||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::{ActiveValue, PaginatorTrait, QueryOrder};
|
||||||
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use imphnen_utils::AppError;
|
|
||||||
use imphnen_entities::seaorm::common::testimonials::{
|
|
||||||
Entity as TestimonialsEntity, Column as TestimonialsColumn, ActiveModel as TestimonialsActiveModel,
|
|
||||||
};
|
|
||||||
use imphnen_entities::seaorm::auth::users::Entity as UsersEntity;
|
|
||||||
use crate::testimonials::domain::{testimonial::TestimonialEntity, repository::TestimonialRepository};
|
|
||||||
|
|
||||||
pub struct PostgresTestimonialRepository {
|
pub struct PostgresTestimonialRepository {
|
||||||
db: Arc<DatabaseConnection>,
|
db: Arc<DatabaseConnection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostgresTestimonialRepository {
|
impl PostgresTestimonialRepository {
|
||||||
pub fn new(db: DatabaseConnection) -> Self {
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
Self { db: Arc::new(db) }
|
Self { db: Arc::new(db) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TestimonialRepository for PostgresTestimonialRepository {
|
impl TestimonialRepository for PostgresTestimonialRepository {
|
||||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
async fn find_all(
|
||||||
let page = params.page.max(1);
|
&self,
|
||||||
let per_page = params.per_page.clamp(1, 100);
|
params: PaginationParams,
|
||||||
|
) -> Result<PaginatorResponse<TestimonialEntity>, AppError> {
|
||||||
|
let page = params.page.max(1);
|
||||||
|
let per_page = params.per_page.clamp(1, 100);
|
||||||
|
|
||||||
let mut query = TestimonialsEntity::find()
|
let mut query = TestimonialsEntity::find()
|
||||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||||
.find_also_related(UsersEntity);
|
.find_also_related(UsersEntity);
|
||||||
|
|
||||||
query = match params.sort_by.as_deref() {
|
query = match params.sort_by.as_deref() {
|
||||||
Some("updated_at") => match params.sort_direction {
|
Some("updated_at") => match params.sort_direction {
|
||||||
Some(SortDirection::Asc) => query.order_by_asc(TestimonialsColumn::UpdatedAt),
|
Some(SortDirection::Asc) => {
|
||||||
_ => query.order_by_desc(TestimonialsColumn::UpdatedAt),
|
query.order_by_asc(TestimonialsColumn::UpdatedAt)
|
||||||
},
|
}
|
||||||
_ => match params.sort_direction {
|
_ => query.order_by_desc(TestimonialsColumn::UpdatedAt),
|
||||||
Some(SortDirection::Asc) => query.order_by_asc(TestimonialsColumn::CreatedAt),
|
},
|
||||||
_ => query.order_by_desc(TestimonialsColumn::CreatedAt),
|
_ => match params.sort_direction {
|
||||||
},
|
Some(SortDirection::Asc) => {
|
||||||
};
|
query.order_by_asc(TestimonialsColumn::CreatedAt)
|
||||||
|
}
|
||||||
|
_ => query.order_by_desc(TestimonialsColumn::CreatedAt),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||||
let total = paginator.num_items().await
|
let total = paginator
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.num_items()
|
||||||
let rows = paginator.fetch_page((page - 1) as u64).await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
let rows = paginator
|
||||||
|
.fetch_page((page - 1) as u64)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
let data: Vec<TestimonialEntity> = rows.into_iter()
|
let data: Vec<TestimonialEntity> = rows
|
||||||
.filter_map(|(t, u)| {
|
.into_iter()
|
||||||
u.map(|user| TestimonialEntity {
|
.filter_map(|(t, u)| {
|
||||||
id: t.id,
|
u.map(|user| TestimonialEntity {
|
||||||
user_id: t.user_id,
|
id: t.id,
|
||||||
user_fullname: format!(
|
user_id: t.user_id,
|
||||||
"{} {}",
|
user_fullname: format!(
|
||||||
user.first_name.as_deref().unwrap_or(""),
|
"{} {}",
|
||||||
user.last_name.as_deref().unwrap_or("")
|
user.first_name.as_deref().unwrap_or(""),
|
||||||
).trim().to_string(),
|
user.last_name.as_deref().unwrap_or("")
|
||||||
role: t.role,
|
)
|
||||||
content: t.content,
|
.trim()
|
||||||
is_deleted: t.is_deleted,
|
.to_string(),
|
||||||
created_at: t.created_at.to_rfc3339(),
|
role: t.role,
|
||||||
updated_at: t.updated_at.to_rfc3339(),
|
content: t.content,
|
||||||
})
|
is_deleted: t.is_deleted,
|
||||||
})
|
created_at: t.created_at.to_rfc3339(),
|
||||||
.collect();
|
updated_at: t.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||||
Ok(PaginatorResponse { data, meta })
|
Ok(PaginatorResponse { data, meta })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
async fn find_by_id(&self, id: Uuid) -> Result<TestimonialEntity, AppError> {
|
||||||
let (testimonial, user) = TestimonialsEntity::find_by_id(id)
|
let (testimonial, user) = TestimonialsEntity::find_by_id(id)
|
||||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||||
.find_also_related(UsersEntity)
|
.find_also_related(UsersEntity)
|
||||||
.one(self.db.as_ref())
|
.one(self.db.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?;
|
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?;
|
||||||
|
|
||||||
let user = user.ok_or_else(|| AppError::NotFoundError("User not found for testimonial".to_string()))?;
|
let user = user.ok_or_else(|| {
|
||||||
|
AppError::NotFoundError("User not found for testimonial".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(TestimonialEntity {
|
Ok(TestimonialEntity {
|
||||||
id: testimonial.id,
|
id: testimonial.id,
|
||||||
user_id: testimonial.user_id,
|
user_id: testimonial.user_id,
|
||||||
user_fullname: format!(
|
user_fullname: format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
user.first_name.as_deref().unwrap_or(""),
|
user.first_name.as_deref().unwrap_or(""),
|
||||||
user.last_name.as_deref().unwrap_or("")
|
user.last_name.as_deref().unwrap_or("")
|
||||||
).trim().to_string(),
|
)
|
||||||
role: testimonial.role,
|
.trim()
|
||||||
content: testimonial.content,
|
.to_string(),
|
||||||
is_deleted: testimonial.is_deleted,
|
role: testimonial.role,
|
||||||
created_at: testimonial.created_at.to_rfc3339(),
|
content: testimonial.content,
|
||||||
updated_at: testimonial.updated_at.to_rfc3339(),
|
is_deleted: testimonial.is_deleted,
|
||||||
})
|
created_at: testimonial.created_at.to_rfc3339(),
|
||||||
}
|
updated_at: testimonial.updated_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn create(&self, entity: TestimonialEntity) -> Result<TestimonialEntity, AppError> {
|
async fn create(
|
||||||
let active_model = TestimonialsActiveModel {
|
&self,
|
||||||
id: ActiveValue::Set(entity.id),
|
entity: TestimonialEntity,
|
||||||
user_id: ActiveValue::Set(entity.user_id),
|
) -> Result<TestimonialEntity, AppError> {
|
||||||
role: ActiveValue::Set(entity.role.clone()),
|
let active_model = TestimonialsActiveModel {
|
||||||
content: ActiveValue::Set(entity.content.clone()),
|
id: ActiveValue::Set(entity.id),
|
||||||
is_deleted: ActiveValue::Set(false),
|
user_id: ActiveValue::Set(entity.user_id),
|
||||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
role: ActiveValue::Set(entity.role.clone()),
|
||||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
content: ActiveValue::Set(entity.content.clone()),
|
||||||
};
|
is_deleted: ActiveValue::Set(false),
|
||||||
|
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
|
||||||
let inserted = active_model.insert(self.db.as_ref()).await
|
let inserted = active_model
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.insert(self.db.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
Ok(TestimonialEntity {
|
Ok(TestimonialEntity {
|
||||||
id: inserted.id,
|
id: inserted.id,
|
||||||
user_id: inserted.user_id,
|
user_id: inserted.user_id,
|
||||||
user_fullname: entity.user_fullname,
|
user_fullname: entity.user_fullname,
|
||||||
role: inserted.role,
|
role: inserted.role,
|
||||||
content: inserted.content,
|
content: inserted.content,
|
||||||
is_deleted: inserted.is_deleted,
|
is_deleted: inserted.is_deleted,
|
||||||
created_at: inserted.created_at.to_rfc3339(),
|
created_at: inserted.created_at.to_rfc3339(),
|
||||||
updated_at: inserted.updated_at.to_rfc3339(),
|
updated_at: inserted.updated_at.to_rfc3339(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
async fn update(&self, entity: TestimonialEntity) -> Result<(), AppError> {
|
||||||
let mut active_model: TestimonialsActiveModel = TestimonialsEntity::find_by_id(entity.id)
|
let mut active_model: TestimonialsActiveModel =
|
||||||
.one(self.db.as_ref())
|
TestimonialsEntity::find_by_id(entity.id)
|
||||||
.await
|
.one(self.db.as_ref())
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
.await
|
||||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
.into();
|
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
active_model.role = ActiveValue::Set(entity.role);
|
active_model.role = ActiveValue::Set(entity.role);
|
||||||
active_model.content = ActiveValue::Set(entity.content);
|
active_model.content = ActiveValue::Set(entity.content);
|
||||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
|
|
||||||
active_model.update(self.db.as_ref()).await
|
active_model
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.update(self.db.as_ref())
|
||||||
Ok(())
|
.await
|
||||||
}
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||||
let mut active_model: TestimonialsActiveModel = TestimonialsEntity::find_by_id(id)
|
let mut active_model: TestimonialsActiveModel =
|
||||||
.one(self.db.as_ref())
|
TestimonialsEntity::find_by_id(id)
|
||||||
.await
|
.one(self.db.as_ref())
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
.await
|
||||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||||
.into();
|
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||||
|
.into();
|
||||||
|
|
||||||
active_model.is_deleted = ActiveValue::Set(true);
|
active_model.is_deleted = ActiveValue::Set(true);
|
||||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||||
active_model.update(self.db.as_ref()).await
|
active_model
|
||||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
.update(self.db.as_ref())
|
||||||
Ok(())
|
.await
|
||||||
}
|
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,6 @@ pub mod application;
|
|||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod infrastructure;
|
pub mod infrastructure;
|
||||||
|
|
||||||
pub use infrastructure::http::{testimonials_public_routes, testimonials_protected_routes};
|
pub use infrastructure::http::{
|
||||||
|
testimonials_protected_routes, testimonials_public_routes,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "imphnen-dimentorin"
|
name = "imphnen-dimentorin"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::super::domain::article::ArticleEntity;
|
||||||
|
use super::super::domain::article_types::{ArticleDetail, ArticleListItem, CreateArticleCommand};
|
||||||
|
use super::super::domain::repository::ArticleRepository;
|
||||||
|
use super::super::domain::service::ArticleService;
|
||||||
|
use imphnen_utils::AppError;
|
||||||
|
|
||||||
|
pub struct ArticleServiceImpl {
|
||||||
|
repo: Arc<dyn ArticleRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArticleServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn ArticleRepository>) -> Self {
|
||||||
|
Self { repo }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ArticleService for ArticleServiceImpl {
|
||||||
|
async fn list(
|
||||||
|
&self,
|
||||||
|
page: u64,
|
||||||
|
per_page: u64,
|
||||||
|
category: Option<String>,
|
||||||
|
) -> Result<PaginatorResponse<ArticleListItem>, AppError> {
|
||||||
|
let result = self.repo.find_all_paginated(page, per_page, category).await?;
|
||||||
|
let items: Vec<ArticleListItem> = result
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(ArticleListItem::from)
|
||||||
|
.collect();
|
||||||
|
Ok(PaginatorResponse {
|
||||||
|
data: items,
|
||||||
|
meta: result.meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_by_id(&self, id: Uuid) -> Result<ArticleDetail, AppError> {
|
||||||
|
let entity = self.repo.find_by_id(id).await?;
|
||||||
|
Ok(ArticleDetail::from(entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_by_slug(&self, slug: &str) -> Result<ArticleDetail, AppError> {
|
||||||
|
let entity = self.repo.find_by_slug(slug).await?;
|
||||||
|
Ok(ArticleDetail::from(entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn categories(&self) -> Result<Vec<String>, AppError> {
|
||||||
|
self.repo.find_categories().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, cmd: CreateArticleCommand) -> Result<ArticleDetail, AppError> {
|
||||||
|
let entity = ArticleEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
title: cmd.title,
|
||||||
|
slug: cmd.slug,
|
||||||
|
category: cmd.category,
|
||||||
|
excerpt: cmd.excerpt,
|
||||||
|
content: cmd.content,
|
||||||
|
cover_url: cmd.cover_url,
|
||||||
|
author_name: cmd.author_name,
|
||||||
|
is_published: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
self.repo.create(entity.clone()).await?;
|
||||||
|
Ok(ArticleDetail::from(entity))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod article_service;
|
||||||
|
|
||||||
|
pub use article_service::ArticleServiceImpl;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ArticleEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub category: String,
|
||||||
|
pub excerpt: String,
|
||||||
|
pub content: String,
|
||||||
|
pub cover_url: Option<String>,
|
||||||
|
pub author_name: Option<String>,
|
||||||
|
pub is_published: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::article::ArticleEntity;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ArticleListItem {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub category: String,
|
||||||
|
pub excerpt: String,
|
||||||
|
pub cover_url: Option<String>,
|
||||||
|
pub author_name: Option<String>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ArticleDetail {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub category: String,
|
||||||
|
pub excerpt: String,
|
||||||
|
pub content: String,
|
||||||
|
pub cover_url: Option<String>,
|
||||||
|
pub author_name: Option<String>,
|
||||||
|
pub is_published: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CreateArticleCommand {
|
||||||
|
pub title: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub category: String,
|
||||||
|
pub excerpt: String,
|
||||||
|
pub content: String,
|
||||||
|
pub cover_url: Option<String>,
|
||||||
|
pub author_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ArticleEntity> for ArticleListItem {
|
||||||
|
fn from(e: ArticleEntity) -> Self {
|
||||||
|
Self {
|
||||||
|
id: e.id,
|
||||||
|
title: e.title,
|
||||||
|
slug: e.slug,
|
||||||
|
category: e.category,
|
||||||
|
excerpt: e.excerpt,
|
||||||
|
cover_url: e.cover_url,
|
||||||
|
author_name: e.author_name,
|
||||||
|
created_at: e.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ArticleEntity> for ArticleDetail {
|
||||||
|
fn from(e: ArticleEntity) -> Self {
|
||||||
|
Self {
|
||||||
|
id: e.id,
|
||||||
|
title: e.title,
|
||||||
|
slug: e.slug,
|
||||||
|
category: e.category,
|
||||||
|
excerpt: e.excerpt,
|
||||||
|
content: e.content,
|
||||||
|
cover_url: e.cover_url,
|
||||||
|
author_name: e.author_name,
|
||||||
|
is_published: e.is_published,
|
||||||
|
created_at: e.created_at,
|
||||||
|
updated_at: e.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
pub mod article;
|
||||||
|
pub mod article_types;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use repository::ArticleRepository;
|
||||||
|
pub use service::ArticleService;
|
||||||
|
pub use article::ArticleEntity;
|
||||||
|
pub use article_types::{ArticleDetail, ArticleListItem, CreateArticleCommand};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user