Compare commits
16
Commits
v0.2.0
...
e1bc336baa
+4
-5
@@ -1,11 +1,6 @@
|
||||
RUST_ENV=development
|
||||
RUST_LOG=debug
|
||||
PORT=4099
|
||||
SURREALDB_URL=ws://localhost:8000/rpc
|
||||
SURREALDB_USERNAME=root
|
||||
SURREALDB_PASSWORD=root
|
||||
SURREALDB_NAMESPACE=test
|
||||
SURREALDB_DBNAME=test
|
||||
ACCESS_TOKEN_SECRET=your-access-token-secret-key-here
|
||||
REFRESH_TOKEN_SECRET=your-refresh-token-secret-key-here
|
||||
SMTP_EMAIL=your-email@example.com
|
||||
@@ -32,3 +27,7 @@ SSLMODE=require
|
||||
RETRY_ATTEMPTS=3
|
||||
RETRY_DELAY=1
|
||||
GOOGLE_REDIRECT_URL=http://localhost:8000/api/v1/auth/google/callback
|
||||
|
||||
CDN_URL=https://cdn.asepharyana.tech
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://gacha.imphnen.dev,https://imphnen.dev,https://dimentorin.imphnen.dev
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Deploy to Ancikri
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Build the project
|
||||
run: cargo build --release
|
||||
|
||||
- name: Stop service on VPS before upload
|
||||
uses: appleboy/ssh-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||
script: |
|
||||
set -e
|
||||
echo "Stopping the service before uploading the binary"
|
||||
sudo systemctl stop imphnen-backend-service
|
||||
|
||||
- name: Upload artifact to VPS
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||
source: ./target/release/*
|
||||
target: /opt/imphnen-backend-service/imphnen-backend-service
|
||||
rm: true
|
||||
overwrite: true
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.VPS_ANCIKRI_IP }}
|
||||
username: ${{ secrets.VPS_ANCIKRI_USER }}
|
||||
key: ${{ secrets.VPS_ANCIKRI_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_ANCIKRI_PORT }}
|
||||
script: |
|
||||
set -e
|
||||
|
||||
echo "Restarting the service"
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
sudo systemctl restart imphnen-backend-service
|
||||
|
||||
echo "Deployment completed successfully"
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Nix Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['develop']
|
||||
pull_request:
|
||||
branches: ['develop']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Setup Cachix
|
||||
uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: msdqn
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
- name: Build
|
||||
run: nix build .#default -o result
|
||||
|
||||
- name: Push to Cachix
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||
run: cachix push msdqn result
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/develop'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Setup SSH
|
||||
env:
|
||||
INFRA_DEPLOY_KEY: ${{ secrets.INFRA_DEPLOY_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${INFRA_DEPLOY_KEY}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||
ssh-keyscan 167.235.70.37 >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Update infra flake.lock
|
||||
run: |
|
||||
export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes"
|
||||
git clone git@github.com:IMPHNEN/imphnen-infrastructure.git /tmp/infra
|
||||
cd /tmp/infra
|
||||
nix flake update imphnen-backend
|
||||
if git diff --quiet flake.lock; then
|
||||
echo "flake.lock unchanged, skipping"
|
||||
else
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add flake.lock
|
||||
git commit -m "chore: update imphnen-backend-service to ${GITHUB_SHA::7}"
|
||||
git push
|
||||
fi
|
||||
|
||||
- name: Deploy to server
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key \
|
||||
-o ConnectTimeout=30 \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ServerAliveInterval=30 \
|
||||
-o ServerAliveCountMax=40 \
|
||||
root@167.235.70.37 \
|
||||
'nixos-rebuild switch --flake github:IMPHNEN/imphnen-infrastructure#hetzner --refresh 2>&1 | tail -50'
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop"]
|
||||
pull_request:
|
||||
branches: ["develop"]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
Generated
+679
-17
File diff suppressed because it is too large
Load Diff
+22
-12
@@ -1,17 +1,20 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"imphnen-entities", # Most basic - core data structures
|
||||
"imphnen-macros", # Macros
|
||||
"imphnen-libs", # Depends on entities
|
||||
"imphnen-utils", # Depends on libs and entities
|
||||
"imphnen-middleware",# Utility for permissions
|
||||
"imphnen-iam", # Core auth service, depends on libs, utils, entities
|
||||
"imphnen-cms", # Content management, depends on core services
|
||||
"imphnen-gacha", # Game mechanics, depends on core services
|
||||
"imphnen-dimentorin",# Learning platform, depends on core services
|
||||
"imphnen-gateway", # API gateway, depends on all services
|
||||
"imphnen-backend", # Main application, depends on all services
|
||||
"imphnen-entities",
|
||||
"imphnen-macros",
|
||||
"imphnen-libs",
|
||||
"imphnen-storage",
|
||||
"imphnen-email",
|
||||
"imphnen-utils",
|
||||
"imphnen-middleware",
|
||||
"imphnen-iam",
|
||||
"imphnen-cms",
|
||||
"imphnen-gacha",
|
||||
"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"] }
|
||||
jsonwebtoken = "9.3.1"
|
||||
chrono = "0.4.41"
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras", "uuid", "chrono"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||
thiserror = "2.0.14"
|
||||
@@ -63,6 +66,7 @@ hyper = "1.6.0"
|
||||
hyper-util = "0.1.16"
|
||||
minio = "0.3.0"
|
||||
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros", "with-chrono", "uuid"] }
|
||||
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "uuid", "chrono", "json", "macros"] }
|
||||
num_cpus = "1.16.0"
|
||||
|
||||
|
||||
@@ -86,6 +90,12 @@ imphnen-entities = { path = "./imphnen-entities" }
|
||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||
imphnen-macros = { path = "./imphnen-macros" }
|
||||
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
||||
imphnen-storage = { path = "./imphnen-storage" }
|
||||
imphnen-email = { path = "./imphnen-email" }
|
||||
bcrypt = "0.15"
|
||||
image = { version = "0.25", features = ["png", "jpeg"] }
|
||||
qrcode = { version = "0.14", default-features = false, features = ["image"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
||||
+20
-12
@@ -11,30 +11,38 @@ WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
RUN mkdir -p imphnen-backend/src imphnen-cms/src imphnen-dimentorin/src \
|
||||
imphnen-entities/src imphnen-gacha/src imphnen-gateway/src \
|
||||
imphnen-iam/src imphnen-libs/src imphnen-middleware/src \
|
||||
imphnen-utils/src tests/src && \
|
||||
RUN mkdir -p \
|
||||
imphnen-backend/src \
|
||||
imphnen-cms/src \
|
||||
imphnen-dimentorin/src \
|
||||
imphnen-email/src \
|
||||
imphnen-entities/src \
|
||||
imphnen-gacha/src \
|
||||
imphnen-gateway/src \
|
||||
imphnen-hackathon/src \
|
||||
imphnen-iam/src \
|
||||
imphnen-libs/src \
|
||||
imphnen-macros/src \
|
||||
imphnen-middleware/src \
|
||||
imphnen-storage/src \
|
||||
imphnen-utils/src && \
|
||||
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
||||
find . -name "src" -type d -exec sh -c 'echo "// dummy" > "$1/lib.rs"' _ {} \;
|
||||
|
||||
RUN echo '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||
|
||||
|
||||
RUN echo -e '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||
|
||||
find . -name "src" -type d -exec sh -c 'touch "$1/lib.rs"' _ {} \;
|
||||
|
||||
COPY imphnen-backend ./imphnen-backend
|
||||
COPY imphnen-cms ./imphnen-cms
|
||||
COPY imphnen-dimentorin ./imphnen-dimentorin
|
||||
COPY imphnen-email ./imphnen-email
|
||||
COPY imphnen-entities ./imphnen-entities
|
||||
COPY imphnen-gacha ./imphnen-gacha
|
||||
COPY imphnen-gateway ./imphnen-gateway
|
||||
COPY imphnen-hackathon ./imphnen-hackathon
|
||||
COPY imphnen-iam ./imphnen-iam
|
||||
COPY imphnen-libs ./imphnen-libs
|
||||
COPY imphnen-macros ./imphnen-macros
|
||||
COPY imphnen-middleware ./imphnen-middleware
|
||||
COPY imphnen-storage ./imphnen-storage
|
||||
COPY imphnen-utils ./imphnen-utils
|
||||
COPY tests ./tests
|
||||
|
||||
RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-units=1 -C strip=symbols" \
|
||||
cargo build -p imphnen-backend --release && \
|
||||
|
||||
+22
-12
@@ -1,15 +1,25 @@
|
||||
{pkgs ? import <nixpkgs> {}}: let
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
rustDeps = pkgs.callPackage ./Cargo.nix {};
|
||||
packageEntry = rustDeps.workspaceMembers.${manifest.name};
|
||||
deps = packageEntry.build.cargoDeps or null;
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
let
|
||||
swaggerUi = pkgs.fetchurl {
|
||||
url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip";
|
||||
hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw=";
|
||||
};
|
||||
in
|
||||
pkgs.rustPlatform.buildRustPackage {
|
||||
pname = manifest.name;
|
||||
version = manifest.version;
|
||||
cargoDeps = deps;
|
||||
pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "imphnen-backend";
|
||||
version = (pkgs.lib.importTOML ./imphnen-backend/Cargo.toml).package.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
nativeBuildInputs = [pkgs.openssl pkgs.pkg-config];
|
||||
buildInputs = [pkgs.openssl];
|
||||
}
|
||||
cargoBuildFlags = [
|
||||
"--package"
|
||||
"imphnen-backend"
|
||||
"--bin"
|
||||
"api"
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
preBuild = ''
|
||||
export SWAGGER_UI_DOWNLOAD_URL="file://${swaggerUi}"
|
||||
'';
|
||||
doCheck = false;
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739020877,
|
||||
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
||||
"lastModified": 1775036866,
|
||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
};
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
@@ -31,9 +29,17 @@
|
||||
packages = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./default.nix { };
|
||||
});
|
||||
|
||||
overlays.default = final: _prev: {
|
||||
imphnen-backend = final.callPackage ./default.nix { };
|
||||
};
|
||||
|
||||
nixosModules.backend = ./nixos-module.nix;
|
||||
|
||||
devShells = forAllSystems (system: {
|
||||
default = (pkgsFor system).callPackage ./shell.nix { };
|
||||
});
|
||||
|
||||
dockerImages = forAllSystems (system: {
|
||||
tryOutApi = (pkgsFor system).callPackage ./docker.nix { };
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imphnen-backend"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
// API entry point using PostgreSQL (SurrealDB migration complete)
|
||||
// This file has been updated to use SeaORM with PostgreSQL instead of SurrealDB
|
||||
use imphnen_gateway::gateway_service;
|
||||
use imphnen_libs::axum_init;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
axum_init(|postgres_db| async {
|
||||
// Gateway service now uses PostgreSQL exclusively (SeaORM)
|
||||
// SurrealDB dependencies have been completely removed
|
||||
gateway_service(postgres_db).await
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::{Statement, ConnectionTrait};
|
||||
use std::error::Error;
|
||||
use sea_orm::{ConnectionTrait, Statement};
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
// New default behavior: execute by default; use --dry-run to preview only.
|
||||
let dry_run = args.iter().any(|s| s == "--dry-run" || s == "--no-exec" || s == "--dry");
|
||||
let dry_run = args
|
||||
.iter()
|
||||
.any(|s| s == "--dry-run" || s == "--no-exec" || s == "--dry");
|
||||
let force = args.iter().any(|s| s == "--force" || s == "-f");
|
||||
|
||||
println!("🔎 Clear DB script - WARNING: This will remove data from tables\n");
|
||||
println!("Note: script now runs by default (no --yes required). To preview without executing, use --dry-run.\n");
|
||||
|
||||
// List of tables to truncate (order doesn't matter with CASCADE)
|
||||
let tables = vec![
|
||||
"gacha_claims",
|
||||
"gacha_rolls",
|
||||
@@ -37,7 +37,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
// Filter tables that actually exist in the database
|
||||
let mut existing_tables: Vec<&str> = vec![];
|
||||
for t in tables.iter() {
|
||||
let check_sql = format!(
|
||||
@@ -65,8 +64,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
println!("The script will run the following SQL (on the DB configured by env vars):\n\n{}", truncate_sql);
|
||||
|
||||
// Prevent accidental execution in production without explicit force flag
|
||||
let env_name = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string());
|
||||
let env_name = imphnen_libs::ENV.rust_env.clone();
|
||||
if env_name == "production" && !force {
|
||||
println!("Security: RUST_ENV=production; the script will NOT run without --force. Use --force to override.");
|
||||
return Ok(());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![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_libs::postgres::PostgresConfig;
|
||||
use sea_orm::sea_query::Table;
|
||||
use sea_orm::{ConnectionTrait, Database, DbBackend, EntityTrait, Schema};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -15,24 +15,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
println!(" Database connected. Creating/updating tables...");
|
||||
|
||||
// Dropping and recreating tables to ensure schema is up-to-date
|
||||
// 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_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_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_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, "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?;
|
||||
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(())
|
||||
@@ -43,22 +56,20 @@ async fn drop_and_create_table<E>(
|
||||
builder: DbBackend,
|
||||
name: &str,
|
||||
entity: E,
|
||||
) -> Result<(), Box<dyn std::error::Error>> // Return Result
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
E: EntityTrait,
|
||||
{
|
||||
let schema = Schema::new(builder);
|
||||
|
||||
// Drop table if it exists
|
||||
let drop_stmt = Table::drop().table(entity).if_exists().cascade().to_owned(); // Added .cascade()
|
||||
db.execute(builder.build(&drop_stmt)).await?; // Propagate error
|
||||
let drop_stmt = Table::drop().table(entity).if_exists().cascade().to_owned();
|
||||
db.execute(builder.build(&drop_stmt)).await?;
|
||||
println!(" Dropped table if exists: {}", name);
|
||||
|
||||
// Create table
|
||||
let mut create_stmt = schema.create_table_from_entity(entity);
|
||||
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);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -10,7 +10,6 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
let sub = args[1].clone();
|
||||
// Use sub as both sub and user_id
|
||||
match encode_access_token(sub.clone(), sub.clone()) {
|
||||
Ok(token) => println!("{}", token),
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#![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_entities::seaorm::common::events::{ActiveModel as EventsActiveModel, Entity as EventEntity};
|
||||
use sea_orm::{ActiveValue::Set, ActiveModelTrait, EntityTrait, ColumnTrait, QueryFilter};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter,
|
||||
};
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc; // Removed NaiveDateTime as it was unused
|
||||
|
||||
#[tokio::main]
|
||||
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-22T15:00:00Z",
|
||||
),
|
||||
// Additional Events
|
||||
(
|
||||
"Rust Programming Bootcamp",
|
||||
"Intensive 3-day bootcamp to master Rust fundamentals and advanced concepts.",
|
||||
@@ -154,18 +157,20 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
price,
|
||||
location,
|
||||
is_online,
|
||||
start_date_str, // Renamed to avoid conflict
|
||||
end_date_str, // Renamed to avoid conflict
|
||||
start_date_str,
|
||||
end_date_str,
|
||||
) in events
|
||||
{
|
||||
// Check if event already exists by name
|
||||
let existing = EventEntity::find().filter(<EventEntity as EntityTrait>::Column::Name.eq(name)).one(db).await?;
|
||||
let existing = EventEntity::find()
|
||||
.filter(<EventEntity as EntityTrait>::Column::Name.eq(name))
|
||||
.one(db)
|
||||
.await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let uuid = Uuid::new_v4(); // Generate a Uuid
|
||||
let uuid = Uuid::new_v4();
|
||||
let mut event_model: EventsActiveModel = Default::default();
|
||||
event_model.id = Set(uuid);
|
||||
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.is_online = Set(is_online);
|
||||
event_model.location = Set(location.clone());
|
||||
event_model.start_date = Set(chrono::DateTime::parse_from_rfc3339(start_date_str)?.with_timezone(&chrono::Utc));
|
||||
event_model.end_date = Set(chrono::DateTime::parse_from_rfc3339(end_date_str)?.with_timezone(&chrono::Utc));
|
||||
event_model.is_deleted = Set(false); // Explicitly set is_deleted
|
||||
event_model.created_at = Set(Utc::now()); // Explicitly set created_at
|
||||
event_model.updated_at = Set(Utc::now()); // Explicitly set updated_at
|
||||
|
||||
event_model.start_date = Set(
|
||||
chrono::DateTime::parse_from_rfc3339(start_date_str)?
|
||||
.with_timezone(&chrono::Utc),
|
||||
);
|
||||
event_model.end_date = Set(
|
||||
chrono::DateTime::parse_from_rfc3339(end_date_str)?
|
||||
.with_timezone(&chrono::Utc),
|
||||
);
|
||||
event_model.is_deleted = Set(false);
|
||||
event_model.created_at = Set(Utc::now());
|
||||
event_model.updated_at = Set(Utc::now());
|
||||
|
||||
event_model.insert(db).await?;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#![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_rolls::ActiveModel as GachaRollActiveModel;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use uuid::Uuid;
|
||||
use sea_orm::ConnectionTrait;
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
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 db = &pg_conn.conn;
|
||||
|
||||
// Check if gacha item already exists
|
||||
let check_item_sql = "SELECT id FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1' LIMIT 1";
|
||||
let item_result = pg_conn.query_one(sea_orm::Statement::from_string(db.get_database_backend(), check_item_sql)).await?;
|
||||
let check_item_sql =
|
||||
"SELECT id FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1' LIMIT 1";
|
||||
let item_result = pg_conn
|
||||
.query_one(sea_orm::Statement::from_string(
|
||||
db.get_database_backend(),
|
||||
check_item_sql,
|
||||
))
|
||||
.await?;
|
||||
let gacha_item_uuid = if let Some(ref row) = item_result {
|
||||
// Item exists, get its ID
|
||||
row.try_get("", "id")?
|
||||
} else {
|
||||
// Item doesn't exist, create it
|
||||
// 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
|
||||
let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1'".to_string())).await.ok();
|
||||
let _ = 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();
|
||||
|
||||
// Create gacha item via SeaORM
|
||||
let new_uuid = Uuid::new_v4();
|
||||
let mut item_model: GachaItemActiveModel = Default::default();
|
||||
item_model.id = Set(new_uuid);
|
||||
@@ -47,7 +54,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
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 mut roll_model: GachaRollActiveModel = Default::default();
|
||||
roll_model.id = Set(gacha_roll_id);
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||
use imphnen_entities::seaorm::auth::roles::{
|
||||
Column as RoleColumn, Entity as RoleEntity,
|
||||
};
|
||||
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||
use imphnen_libs::hash_password;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, EntityTrait,
|
||||
QueryFilter,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use 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;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -16,17 +21,28 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
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.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_users WHERE email = 'mentor@example.com'".to_string())).await.ok();
|
||||
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
|
||||
.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()
|
||||
.filter(RoleColumn::Name.eq("Mentor"))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or("Role 'Mentor' not found")?;
|
||||
|
||||
// Insert user with Mentor role
|
||||
let user_id = Uuid::new_v4();
|
||||
let mut user_model: UsersActiveModel = Default::default();
|
||||
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.insert(db).await?;
|
||||
|
||||
// Insert mentor
|
||||
let mentor_id = Uuid::new_v4();
|
||||
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||
mentor_model.id = Set(mentor_id);
|
||||
mentor_model.user_id = Set(user_id);
|
||||
mentor_model.industries = Set(Some(json!( ["Software", "Education"] )));
|
||||
mentor_model.expertise = Set(Some(json!( ["Rust", "Microservices"] )));
|
||||
mentor_model.languages = Set(Some(json!( ["Indonesian", "English"] )));
|
||||
mentor_model.industries = Set(Some(json!(["Software", "Education"])));
|
||||
mentor_model.expertise = Set(Some(json!(["Rust", "Microservices"])));
|
||||
mentor_model.languages = Set(Some(json!(["Indonesian", "English"])));
|
||||
mentor_model.current_company = Set(Some("PT Contoh".to_string()));
|
||||
mentor_model.current_role = Set(Some("Senior Backend Engineer".to_string()));
|
||||
mentor_model.years_of_experience = Set(Some(5));
|
||||
mentor_model.topics_of_interest = Set(Some(json!( ["Rust Programming", "Backend Development"] )));
|
||||
mentor_model.topics_of_interest =
|
||||
Set(Some(json!(["Rust Programming", "Backend Development"])));
|
||||
mentor_model.preferred_mentee_level = Set(Some("beginner".to_string()));
|
||||
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["online", "offline"] )));
|
||||
mentor_model.availability_commitment = Set(Some("2 jam per minggu untuk mentoring online dan offline".to_string()));
|
||||
mentor_model.preferred_mentoring_formats = Set(Some(json!(["online", "offline"])));
|
||||
mentor_model.availability_commitment = Set(Some(
|
||||
"2 jam per minggu untuk mentoring online dan offline".to_string(),
|
||||
));
|
||||
mentor_model.mentoring_rate = Set(Some(100000.0));
|
||||
mentor_model.status = Set(Some("verified".to_string()));
|
||||
mentor_model.is_deleted = Set(false);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use std::error::Error;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use chrono::Utc;
|
||||
use imphnen_entities::seaorm::auth::permissions::ActiveModel as PermissionActiveModel;
|
||||
use imphnen_entities::seaorm::auth::permissions::Entity as PermissionEntity;
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{ActiveModelTrait};
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -54,17 +54,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
PermissionsEnum::DeleteMentors,
|
||||
PermissionsEnum::Administrator,
|
||||
] {
|
||||
// permission.id() returns a string, try parse to uuid
|
||||
let parsed_id = Uuid::parse_str(&permission.id()).unwrap_or_else(|_| Uuid::new_v4());
|
||||
let parsed_id =
|
||||
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?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {permission}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert permission using active model
|
||||
let mut perm_model: PermissionActiveModel = Default::default();
|
||||
perm_model.id = Set(parsed_id);
|
||||
perm_model.name = Set(permission.to_string());
|
||||
|
||||
@@ -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_entities::seaorm::auth::roles::{RoleBuilder, Entity as RoleEntity};
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait};
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc; // Added chrono
|
||||
|
||||
#[tokio::main]
|
||||
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());
|
||||
|
||||
// Check if role already exists
|
||||
let existing = RoleEntity::find_by_id(uuid).one(db).await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {name}");
|
||||
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()
|
||||
.name(name.to_string())
|
||||
.description("System generated role".to_string())
|
||||
@@ -71,9 +67,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
.build()?;
|
||||
let mut role_model = role_model;
|
||||
role_model.id = Set(uuid);
|
||||
role_model.is_system_role = Set(true); // Set the missing field
|
||||
role_model.created_at = Set(Utc::now()); // Set created_at
|
||||
role_model.updated_at = Set(Utc::now()); // Set updated_at
|
||||
role_model.is_system_role = Set(true);
|
||||
role_model.created_at = Set(Utc::now());
|
||||
role_model.updated_at = Set(Utc::now());
|
||||
|
||||
role_model.insert(db).await?;
|
||||
println!("✅ Inserted role: {name}");
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use std::error::Error;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::auth::roles::Entity as RolesEntity;
|
||||
use imphnen_entities::seaorm::auth::roles::ActiveModel as RoleActiveModel;
|
||||
use imphnen_entities::seaorm::auth::roles::Entity as RolesEntity;
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::EntityTrait;
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
// 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![
|
||||
(
|
||||
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||
vec![
|
||||
// Only Administrator permission - grants access to everything
|
||||
PermissionsEnum::Administrator,
|
||||
],
|
||||
vec![PermissionsEnum::Administrator],
|
||||
),
|
||||
(
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
vec![
|
||||
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
|
||||
PermissionsEnum::ReadListUsers,
|
||||
PermissionsEnum::ReadOwnMentorProfile,
|
||||
PermissionsEnum::UpdateOwnMentorProfile,
|
||||
PermissionsEnum::ReadOwnMentorStatus,
|
||||
@@ -64,7 +61,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
(
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||
vec![
|
||||
// Staff should be able to list roles and permissions in tests
|
||||
PermissionsEnum::ReadListRoles,
|
||||
PermissionsEnum::ReadListPermissions,
|
||||
PermissionsEnum::ReadListUsers,
|
||||
@@ -91,12 +87,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
for (role_id, permissions) in roles_permissions {
|
||||
let role_uuid = Uuid::parse_str(role_id).unwrap_or_else(|_| Uuid::new_v4());
|
||||
// Map permissions enum to JSON array of permission ids
|
||||
let json_permissions = JsonValue::Array(
|
||||
permissions.iter().map(|p| JsonValue::String(p.id())).collect()
|
||||
permissions
|
||||
.iter()
|
||||
.map(|p| JsonValue::String(p.id()))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// Find role and update permissions
|
||||
if let Some(role_model) = RolesEntity::find_by_id(role_uuid).one(db).await? {
|
||||
let mut am: RoleActiveModel = role_model.into();
|
||||
am.permissions = Set(Some(json_permissions));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use std::error::Error;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use chrono::Utc;
|
||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||
use imphnen_entities::seaorm::common::events::ActiveModel as EventsActiveModel;
|
||||
use imphnen_entities::seaorm::common::testimonials::ActiveModel as TestimonialsActiveModel;
|
||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use uuid::Uuid;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde_json::json;
|
||||
use chrono::Utc;
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -17,7 +17,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
// Seed Events - handle existing data
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
let mut event_model: EventsActiveModel = Default::default();
|
||||
event_model.id = Set(Uuid::parse_str(&uuid)?);
|
||||
@@ -32,43 +31,46 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
event_model.is_deleted = Set(false);
|
||||
match event_model.insert(db).await {
|
||||
Ok(_) => println!("✅ Inserted test event"),
|
||||
Err(_) => println!("⚠️ Test event already exists or could not be inserted, skipping"),
|
||||
Err(_) => {
|
||||
println!("⚠️ Test event already exists or could not be inserted, skipping")
|
||||
}
|
||||
};
|
||||
|
||||
// Seed Testimonials - handle existing data
|
||||
let mut testimonial_model: TestimonialsActiveModel = Default::default();
|
||||
testimonial_model.id = Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
||||
testimonial_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
testimonial_model.id =
|
||||
Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
||||
testimonial_model.user_id =
|
||||
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
testimonial_model.role = Set("Student".to_string());
|
||||
testimonial_model.content = Set("This is a great platform!".to_string());
|
||||
testimonial_model.is_deleted = Set(false);
|
||||
match testimonial_model.insert(db).await {
|
||||
Ok(_) => println!("✅ Inserted test testimonial"),
|
||||
Err(_) => println!("⚠️ Test testimonial already exists or could not be inserted, skipping"),
|
||||
Err(_) => println!(
|
||||
"⚠️ Test testimonial already exists or could not be inserted, skipping"
|
||||
),
|
||||
};
|
||||
|
||||
// Seed Mentor - handle existing data
|
||||
let mentor_id = Uuid::new_v4();
|
||||
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||
mentor_model.id = Set(mentor_id);
|
||||
// Use the admin user ID instead of a random one
|
||||
mentor_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
mentor_model.industries = Set(Some(json!( ["Technology", "Education"] )));
|
||||
mentor_model.expertise = Set(Some(json!( ["Software Development"] )));
|
||||
mentor_model.languages = Set(Some(json!( ["English", "Indonesian"] )));
|
||||
mentor_model.user_id =
|
||||
Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
mentor_model.industries = Set(Some(json!(["Technology", "Education"])));
|
||||
mentor_model.expertise = Set(Some(json!(["Software Development"])));
|
||||
mentor_model.languages = Set(Some(json!(["English", "Indonesian"])));
|
||||
mentor_model.current_company = Set(Some("Tech Corp".to_string()));
|
||||
mentor_model.current_role = Set(Some("Senior Engineer".to_string()));
|
||||
mentor_model.years_of_experience = Set(Some(5));
|
||||
mentor_model.topics_of_interest = Set(Some(json!( ["Rust", "Web Development"] )));
|
||||
mentor_model.topics_of_interest = Set(Some(json!(["Rust", "Web Development"])));
|
||||
mentor_model.preferred_mentee_level = Set(Some("Beginner".to_string()));
|
||||
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["1:1", "Group"] )));
|
||||
mentor_model.preferred_mentoring_formats = Set(Some(json!(["1:1", "Group"])));
|
||||
mentor_model.availability_commitment = Set(Some("Weekly".to_string()));
|
||||
mentor_model.mentoring_rate = Set(Some(100.0));
|
||||
mentor_model.status = Set(Some("active".to_string()));
|
||||
mentor_model.is_deleted = Set(false);
|
||||
mentor_model.created_at = Set(chrono::Utc::now());
|
||||
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||
// Create mentor record via SeaORM active model
|
||||
mentor_model.insert(db).await?;
|
||||
println!("✅ Inserted test mentor via SeaORM");
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||
use imphnen_entities::seaorm::auth::users::Entity as UserEntity;
|
||||
use imphnen_libs::hash_password;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as UserEntity; // Added for dynamic role lookup
|
||||
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use std::error::Error;
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, IntoActiveModel};
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -39,7 +39,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"665a3cfc-ea5f-4bcd-8769-4a6d8d1451d4",
|
||||
"testuser1@example.com",
|
||||
"Test User 1",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059", // Fixed UUID
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"3972c139-a450-416c-93b0-c42539dc780f",
|
||||
@@ -51,9 +51,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"b426c0a9-0efb-4e26-b078-4f18767255f3",
|
||||
"testuser3@example.com",
|
||||
"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",
|
||||
"user4@example.com",
|
||||
@@ -70,13 +69,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"33333333-3333-3333-3333-333333333333",
|
||||
"mentor2@example.com",
|
||||
"Mentor Two",
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a", // Mentor Role
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
|
||||
),
|
||||
(
|
||||
"44444444-4444-4444-4444-444444444444",
|
||||
"staff2@example.com",
|
||||
"Staff Two",
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86", // Staff Role
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||
),
|
||||
(
|
||||
"55555555-5555-5555-5555-555555555555",
|
||||
@@ -110,21 +109,25 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
),
|
||||
];
|
||||
|
||||
for (id, email, fullname, role_id_str) in users { // role_id_str directly contains UUID
|
||||
let role_uuid = Some(Uuid::parse_str(role_id_str)
|
||||
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?);
|
||||
for (id, email, fullname, role_id_str) in users {
|
||||
let role_uuid = Some(
|
||||
Uuid::parse_str(role_id_str)
|
||||
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?,
|
||||
);
|
||||
|
||||
// Build SeaORM ActiveModel for users
|
||||
let uid = Uuid::parse_str(id)?; // Should always be valid UUID strings from test data
|
||||
let uid = Uuid::parse_str(id)?;
|
||||
|
||||
let names: Vec<&str> = fullname.split_whitespace().collect();
|
||||
let first_name = names.first().map(|s| s.to_string());
|
||||
let last_name = if names.len() > 1 { Some(names[1..].join(" ")) } else { None };
|
||||
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 existing_user = UserEntity::find_by_id(uid).one(db).await?;
|
||||
let is_update = existing_user.is_some();
|
||||
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
//! PostgreSQL Connection Test Program
|
||||
//! This program tests the PostgreSQL integration with SeaORM
|
||||
|
||||
use std::sync::Arc;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection, PostgresError};
|
||||
use imphnen_entities::seaorm::auth::users::{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 uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
use imphnen_entities::seaorm::auth::roles::{
|
||||
Entity as RolesEntity, Model as RoleModel,
|
||||
};
|
||||
use imphnen_entities::seaorm::auth::users::{
|
||||
Entity as UsersEntity, Model as UserModel,
|
||||
};
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection, PostgresError};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, DbErr, EntityTrait, PaginatorTrait, Set, TransactionTrait,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🚀 Starting PostgreSQL Connection Test");
|
||||
println!("=====================================");
|
||||
|
||||
// Load configuration from environment
|
||||
let config = PostgresConfig::from_env()?;
|
||||
println!("✅ Configuration loaded successfully");
|
||||
println!(" Database URL: {}", config.database_url.replace("postgres://", "postgres://****:****@"));
|
||||
println!(
|
||||
" Database URL: {}",
|
||||
config
|
||||
.database_url
|
||||
.replace("postgres://", "postgres://****:****@")
|
||||
);
|
||||
println!(" Pool size: {}", config.pool_size);
|
||||
println!(" Connect timeout: {}s", config.connect_timeout);
|
||||
println!(" Retry attempts: {}", config.retry_attempts);
|
||||
|
||||
// Test connection
|
||||
println!("\n🔌 Testing PostgreSQL connection...");
|
||||
match test_connection(config).await {
|
||||
Ok(()) => {
|
||||
@@ -37,33 +43,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
async fn test_connection(config: PostgresConfig) -> Result<(), PostgresError> {
|
||||
// Create connection
|
||||
println!(" Creating PostgreSQL connection...");
|
||||
let postgres_conn = PostgresConnection::new(config).await?;
|
||||
let connection = Arc::new(postgres_conn);
|
||||
println!(" ✅ Connection established successfully");
|
||||
|
||||
// Test basic connectivity
|
||||
println!(" Testing basic connectivity...");
|
||||
test_basic_connectivity(&connection).await?;
|
||||
println!(" ✅ Basic connectivity test passed");
|
||||
|
||||
// Test table existence
|
||||
println!(" Testing table existence...");
|
||||
test_table_existence(&connection).await?;
|
||||
println!(" ✅ Table existence test passed");
|
||||
|
||||
// Test CRUD operations
|
||||
println!(" Testing CRUD operations...");
|
||||
test_crud_operations(&connection).await?;
|
||||
println!(" ✅ CRUD operations test passed");
|
||||
|
||||
// Test transaction support
|
||||
println!(" Testing transaction support...");
|
||||
test_transactions(&connection).await?;
|
||||
println!(" ✅ Transaction support test passed");
|
||||
|
||||
// Test error handling
|
||||
println!(" Testing error handling...");
|
||||
test_error_handling(&connection).await?;
|
||||
println!(" ✅ Error handling test passed");
|
||||
@@ -71,38 +71,45 @@ async fn test_connection(config: PostgresConfig) -> Result<(), PostgresError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_basic_connectivity(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
// Execute a simple query
|
||||
async fn test_basic_connectivity(
|
||||
connection: &Arc<PostgresConnection>,
|
||||
) -> Result<(), PostgresError> {
|
||||
let statement = sea_orm::Statement::from_string(
|
||||
connection.get_database_backend(),
|
||||
"SELECT 1 as test_value, current_timestamp as current_time".to_string()
|
||||
"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(
|
||||
"No results returned".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Verify we got expected results
|
||||
let test_value: Option<i32> = result.try_get("", "test_value").ok();
|
||||
let current_time: Option<String> = result.try_get("", "current_time").ok();
|
||||
|
||||
if test_value != Some(1) {
|
||||
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||
format!("Expected test_value=1, got {:?}", test_value)
|
||||
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()
|
||||
"Expected current_time to be set".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
println!(" 📝 Query result: test_value={:?}, current_time={:?}", test_value, current_time);
|
||||
println!(
|
||||
" 📝 Query result: test_value={:?}, current_time={:?}",
|
||||
test_value, current_time
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_table_existence(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
// Test if our tables exist
|
||||
async fn test_table_existence(
|
||||
connection: &Arc<PostgresConnection>,
|
||||
) -> Result<(), PostgresError> {
|
||||
use sea_orm::EntityTrait;
|
||||
|
||||
println!(" 📋 Checking users table...");
|
||||
@@ -110,22 +117,29 @@ async fn test_table_existence(connection: &Arc<PostgresConnection>) -> Result<()
|
||||
.count(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
println!(" 📊 Users table accessible, current count: {}", user_count);
|
||||
println!(
|
||||
" 📊 Users table accessible, current count: {}",
|
||||
user_count
|
||||
);
|
||||
|
||||
println!(" 📋 Checking roles table...");
|
||||
let role_count = RolesEntity::find()
|
||||
.count(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
println!(" 📊 Roles table accessible, current count: {}", role_count);
|
||||
println!(
|
||||
" 📊 Roles table accessible, current count: {}",
|
||||
role_count
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_crud_operations(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
async fn test_crud_operations(
|
||||
connection: &Arc<PostgresConnection>,
|
||||
) -> Result<(), PostgresError> {
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
|
||||
// Create test user
|
||||
println!(" ➕ Creating test user...");
|
||||
let test_user_id = Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
@@ -147,35 +161,45 @@ async fn test_crud_operations(connection: &Arc<PostgresConnection>) -> Result<()
|
||||
deleted_at: Set(None),
|
||||
};
|
||||
|
||||
let created_user = user_model.insert(&connection.conn)
|
||||
let created_user = user_model
|
||||
.insert(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
|
||||
println!(" ✅ Created user with ID: {}", created_user.id);
|
||||
|
||||
// Read user
|
||||
println!(" 🔍 Reading test user...");
|
||||
let found_user = UsersEntity::find_by_id(test_user_id)
|
||||
.one(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?
|
||||
.ok_or_else(|| PostgresError::ConnectionError(sea_orm::DbErr::Custom("User not found after creation".to_string())))?;
|
||||
.ok_or_else(|| {
|
||||
PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||
"User not found after creation".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
println!(" ✅ Found user: {} ({})", found_user.username, found_user.email);
|
||||
println!(
|
||||
" ✅ Found user: {} ({})",
|
||||
found_user.username, found_user.email
|
||||
);
|
||||
|
||||
// Update user
|
||||
println!(" ✏️ Updating test user...");
|
||||
let mut update_model: imphnen_entities::seaorm::auth::users::ActiveModel = found_user.into();
|
||||
let mut update_model: imphnen_entities::seaorm::auth::users::ActiveModel =
|
||||
found_user.into();
|
||||
update_model.first_name = Set(Some("Updated".to_string()));
|
||||
update_model.updated_at = Set(Utc::now());
|
||||
|
||||
let updated_user = update_model.update(&connection.conn)
|
||||
let updated_user = update_model
|
||||
.update(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
|
||||
println!(" ✅ Updated user first name to: {:?}", updated_user.first_name);
|
||||
println!(
|
||||
" ✅ Updated user first name to: {:?}",
|
||||
updated_user.first_name
|
||||
);
|
||||
|
||||
// Delete user
|
||||
println!(" 🗑️ Deleting test user...");
|
||||
UsersEntity::delete_by_id(updated_user.id)
|
||||
.exec(&connection.conn)
|
||||
@@ -187,13 +211,15 @@ async fn test_crud_operations(connection: &Arc<PostgresConnection>) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_transactions(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
async fn test_transactions(
|
||||
connection: &Arc<PostgresConnection>,
|
||||
) -> Result<(), PostgresError> {
|
||||
println!(" 💰 Testing transaction support...");
|
||||
|
||||
// Test transaction with rollback
|
||||
let transaction_result = connection.conn.transaction(|txn| {
|
||||
let transaction_result = connection
|
||||
.conn
|
||||
.transaction(|txn| {
|
||||
Box::pin(async move {
|
||||
// Create a test user within transaction
|
||||
let test_user_id = Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
|
||||
@@ -214,31 +240,34 @@ async fn test_transactions(connection: &Arc<PostgresConnection>) -> Result<(), P
|
||||
deleted_at: Set(None),
|
||||
};
|
||||
|
||||
let _created_user = user_model.insert(txn)
|
||||
.await?;
|
||||
let _created_user = user_model.insert(txn).await?;
|
||||
|
||||
// Simulate an error to trigger rollback (return a sea_orm::DbErr so the TransactionError matches)
|
||||
Err::<(), DbErr>(DbErr::Custom("Simulated transaction failure".to_string()))
|
||||
})
|
||||
}).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
// Transaction should fail and rollback
|
||||
match transaction_result {
|
||||
Err(e) => {
|
||||
let e_text = format!("{:?}", e);
|
||||
if e_text.contains("Simulated transaction failure") {
|
||||
println!(" ✅ Transaction failed as expected, rollback successful");
|
||||
} else {
|
||||
return Err(PostgresError::OperationFailed(format!("Unexpected transaction result: {}", e_text)));
|
||||
return Err(PostgresError::OperationFailed(format!(
|
||||
"Unexpected transaction result: {}",
|
||||
e_text
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
return Err(PostgresError::OperationFailed("Unexpected transaction result: transaction unexpectedly succeeded".to_string()));
|
||||
return Err(PostgresError::OperationFailed(
|
||||
"Unexpected transaction result: transaction unexpectedly succeeded"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify user was not created (due to rollback)
|
||||
let user_exists = UsersEntity::find_by_id(Uuid::nil()) // Use nil UUID as we don't know the actual ID
|
||||
let user_exists = UsersEntity::find_by_id(Uuid::nil())
|
||||
.one(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?
|
||||
@@ -253,27 +282,29 @@ async fn test_transactions(connection: &Arc<PostgresConnection>) -> Result<(), P
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_error_handling(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
async fn test_error_handling(
|
||||
connection: &Arc<PostgresConnection>,
|
||||
) -> Result<(), PostgresError> {
|
||||
println!(" ⚠️ Testing error handling...");
|
||||
|
||||
// Test invalid UUID
|
||||
println!(" 🔍 Testing invalid UUID handling...");
|
||||
let invalid_uuid = Uuid::nil(); // This should exist or be handled gracefully
|
||||
let invalid_uuid = Uuid::nil();
|
||||
|
||||
match UsersEntity::find_by_id(invalid_uuid)
|
||||
.one(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?
|
||||
{
|
||||
Some(_) => println!(" ✅ Found user with nil UUID (expected in some cases)"),
|
||||
Some(_) => {
|
||||
println!(" ✅ Found user with nil UUID (expected in some cases)")
|
||||
}
|
||||
None => println!(" ✅ No user found with nil UUID (expected)"),
|
||||
}
|
||||
|
||||
// Test invalid query
|
||||
println!(" 🔍 Testing invalid query handling...");
|
||||
let invalid_statement = sea_orm::Statement::from_string(
|
||||
connection.get_database_backend(),
|
||||
"SELECT * FROM non_existent_table".to_string()
|
||||
"SELECT * FROM non_existent_table".to_string(),
|
||||
);
|
||||
|
||||
match connection.execute(invalid_statement).await {
|
||||
@@ -284,14 +315,13 @@ async fn test_error_handling(connection: &Arc<PostgresConnection>) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Additional utility functions for comprehensive testing
|
||||
pub mod test_utils {
|
||||
use super::*;
|
||||
|
||||
/// Create a test PostgreSQL configuration
|
||||
pub fn create_test_config() -> PostgresConfig {
|
||||
PostgresConfig {
|
||||
database_url: "postgres://postgres:postgres@localhost:5432/imphnen_test".to_string(),
|
||||
database_url: "postgres://postgres:postgres@localhost:5432/imphnen_test"
|
||||
.to_string(),
|
||||
pool_size: 5,
|
||||
connect_timeout: 10,
|
||||
idle_timeout: 30,
|
||||
@@ -301,7 +331,6 @@ pub mod test_utils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a test user model
|
||||
pub fn create_test_user_model() -> UserModel {
|
||||
UserModel {
|
||||
id: Uuid::new_v4(),
|
||||
@@ -321,7 +350,6 @@ pub mod test_utils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a test role model
|
||||
pub fn create_test_role_model() -> RoleModel {
|
||||
RoleModel {
|
||||
id: Uuid::new_v4(),
|
||||
@@ -355,7 +383,6 @@ mod tests {
|
||||
assert!(!user.email.is_empty());
|
||||
assert!(!user.username.is_empty());
|
||||
assert!(user.is_active);
|
||||
// is_admin field removed; instead, check role-based permission or is_active
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,9 +5,6 @@ use imphnen_libs::axum_init;
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let _ = axum_init(|postgres_conn| async {
|
||||
// PostgreSQL is now the primary database - SurrealDB has been completely removed
|
||||
gateway_service(postgres_conn).await
|
||||
})
|
||||
.await;
|
||||
let _ =
|
||||
axum_init(|postgres_conn| async { gateway_service(postgres_conn).await }).await;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imphnen-cms"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -33,6 +33,9 @@ paginator-rs.workspace = true
|
||||
paginator-utils.workspace = true
|
||||
paginator-sea-orm.workspace = true
|
||||
paginator-axum.workspace = true
|
||||
sqlx.workspace = true
|
||||
image.workspace = true
|
||||
qrcode.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
VALID_URL_REGEX = "^https?://"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
use crate::events::domain::{EventEntity, EventRepository, EventService};
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use crate::events::domain::{EventEntity, EventRepository, EventService};
|
||||
|
||||
pub struct EventServiceImpl {
|
||||
repo: Arc<dyn EventRepository>,
|
||||
@@ -18,7 +18,10 @@ impl EventServiceImpl {
|
||||
|
||||
#[async_trait]
|
||||
impl EventService for EventServiceImpl {
|
||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||
async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||
self.repo.find_all(params).await
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use super::event::EventEntity;
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::event::EventEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventRepository: Send + Sync {
|
||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use super::event::EventEntity;
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::event::EventEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventService: Send + Sync {
|
||||
async fn list(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||
async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<EventEntity>, AppError>;
|
||||
async fn get(&self, id: Uuid) -> Result<EventEntity, AppError>;
|
||||
async fn create(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
async fn update(&self, entity: EventEntity) -> Result<(), AppError>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::events::domain::event::EventEntity;
|
||||
use chrono::{DateTime, Utc};
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use crate::events::domain::event::EventEntity;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsCreateRequestDto {
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{Extension, extract::Path, http::HeaderMap, response::{IntoResponse, Response}};
|
||||
use paginator_axum::PaginationQuery;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_utils::{ApiSuccess, ApiPaginated, ApiMessage};
|
||||
use super::dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||
EventsUpdateRequestDto,
|
||||
};
|
||||
use crate::events::domain::EventService;
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::Path,
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_utils::AppError;
|
||||
use super::dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsUpdateRequestDto};
|
||||
use crate::events::domain::EventService;
|
||||
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/cms/landing/events",
|
||||
path = "/v1/landing/cms/events",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
@@ -33,18 +41,23 @@ pub async fn get_event_list(
|
||||
match service.list(params).await {
|
||||
Ok(result) => {
|
||||
let mapped = PaginatorResponse {
|
||||
data: result.data.into_iter().map(EventsListItemDto::from).collect::<Vec<_>>(),
|
||||
data: result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(EventsListItemDto::from)
|
||||
.collect::<Vec<_>>(),
|
||||
meta: result.meta,
|
||||
};
|
||||
ApiPaginated(mapped).into_response()
|
||||
}
|
||||
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string()).into_response(),
|
||||
Err(e) => ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, e.to_string())
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events/detail/{id}",
|
||||
path = "/v1/landing/cms/events/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
@@ -59,18 +72,25 @@ pub async fn get_event_by_id(
|
||||
) -> 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(),
|
||||
Err(e) => {
|
||||
return ApiMessage::new(
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
format!("Invalid UUID: {e}"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
match service.get(uuid).await {
|
||||
Ok(event) => ApiSuccess(EventsDetailItemDto::from(event)).into_response(),
|
||||
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
||||
Err(e) => ApiMessage::new(axum::http::StatusCode::NOT_FOUND, e.to_string())
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/events/create",
|
||||
path = "/v1/landing/cms/events/create",
|
||||
request_body = EventsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[ADMIN] Create new event")
|
||||
@@ -93,7 +113,7 @@ pub async fn post_create_event(
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/events/update/{id}",
|
||||
path = "/v1/landing/cms/events/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
@@ -136,7 +156,7 @@ pub async fn patch_update_event(
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(("Bearer" = [])),
|
||||
path = "/v1/cms/landing/events/delete/{id}",
|
||||
path = "/v1/landing/cms/events/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
|
||||
@@ -2,4 +2,4 @@ pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::{events_public_routes, events_protected_routes};
|
||||
pub use routes::{events_protected_routes, events_public_routes};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{Router, routing::{delete, get, patch, post}, Extension};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use super::handlers::{
|
||||
delete_event, get_event_by_id, get_event_list, patch_update_event,
|
||||
post_create_event,
|
||||
};
|
||||
use crate::events::application::EventServiceImpl;
|
||||
use crate::events::domain::EventService;
|
||||
use crate::events::infrastructure::persistence::PostgresEventRepository;
|
||||
use super::handlers::{
|
||||
delete_event, get_event_by_id, get_event_list, patch_update_event, post_create_event,
|
||||
use axum::{
|
||||
Extension, Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn build_service(db: DatabaseConnection) -> Arc<dyn EventService> {
|
||||
let repo = Arc::new(PostgresEventRepository::new(db));
|
||||
@@ -16,16 +20,16 @@ fn build_service(db: DatabaseConnection) -> Arc<dyn EventService> {
|
||||
pub fn events_public_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/cms/landing/events", get(get_event_list))
|
||||
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||
.route("/events", get(get_event_list))
|
||||
.route("/events/detail/{id}", get(get_event_by_id))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn events_protected_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/cms/landing/events/create", post(post_create_event))
|
||||
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||
.route("/events/create", post(post_create_event))
|
||||
.route("/events/update/{id}", patch(patch_update_event))
|
||||
.route("/events/delete/{id}", delete(delete_event))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
use crate::events::domain::{event::EventEntity, repository::EventRepository};
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, Order, QueryOrder, PaginatorTrait};
|
||||
use imphnen_entities::seaorm::common::events::{
|
||||
ActiveModel as EventsActiveModel, Column as EventsColumn, Entity as EventsEntity,
|
||||
Model as EventsModel,
|
||||
};
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, Order, PaginatorTrait, QueryOrder};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
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 {
|
||||
EventEntity {
|
||||
@@ -41,12 +41,14 @@ impl PostgresEventRepository {
|
||||
|
||||
#[async_trait]
|
||||
impl EventRepository for PostgresEventRepository {
|
||||
async fn find_all(&self, params: PaginationParams) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<EventEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let mut query = EventsEntity::find()
|
||||
.filter(EventsColumn::IsDeleted.eq(false));
|
||||
let mut query = EventsEntity::find().filter(EventsColumn::IsDeleted.eq(false));
|
||||
|
||||
if let Some(ref search) = params.search {
|
||||
query = query.filter(EventsColumn::Name.contains(&search.query));
|
||||
@@ -58,15 +60,21 @@ impl EventRepository for PostgresEventRepository {
|
||||
_ => query.order_by(EventsColumn::Name, Order::Asc),
|
||||
},
|
||||
_ => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by(EventsColumn::CreatedAt, Order::Asc),
|
||||
Some(SortDirection::Asc) => {
|
||||
query.order_by(EventsColumn::CreatedAt, Order::Asc)
|
||||
}
|
||||
_ => query.order_by(EventsColumn::CreatedAt, Order::Desc),
|
||||
},
|
||||
};
|
||||
|
||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||
let total = paginator.num_items().await
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let events = paginator.fetch_page((page - 1) as u64).await
|
||||
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();
|
||||
@@ -127,7 +135,9 @@ impl EventRepository for PostgresEventRepository {
|
||||
active_model.end_date = ActiveValue::Set(entity.end_date);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(self.db.as_ref()).await
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -142,7 +152,9 @@ impl EventRepository for PostgresEventRepository {
|
||||
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
active_model.update(self.db.as_ref()).await
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ pub mod application;
|
||||
pub mod domain;
|
||||
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,7 @@
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
pub mod qr;
|
||||
|
||||
pub use events::{events_public_routes, events_protected_routes};
|
||||
pub use testimonials::{testimonials_public_routes, testimonials_protected_routes};
|
||||
pub use events::{events_protected_routes, events_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,160 @@
|
||||
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"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden - admin only")
|
||||
),
|
||||
tag = "QR - Campaigns",
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
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"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden - admin only")
|
||||
),
|
||||
tag = "QR - Campaigns",
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
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"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden - admin only")
|
||||
),
|
||||
tag = "QR - Campaigns",
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
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"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden - admin only")
|
||||
),
|
||||
tag = "QR - Campaigns",
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
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 (multipart/form-data with 'file' field)"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
tag = "QR - Campaigns",
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
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(Extension(pool))
|
||||
.layer(from_fn(qr_auth_middleware))
|
||||
}
|
||||
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user