Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02be09506a | ||
|
|
b88ecd1361 | ||
|
|
fa435994f2 | ||
|
|
16c613d699 | ||
|
|
f598dedb3b | ||
|
|
cfa251337f | ||
|
|
cca9aa6318 | ||
|
|
f1e4e02697 | ||
|
|
a3c2715a85 | ||
|
|
89cc631fb0 | ||
|
|
36c12110c2 | ||
|
|
070060365a | ||
|
|
68e816f956 | ||
|
|
48db5f07ef | ||
|
|
a4b5d91906 | ||
|
|
9878279660 | ||
|
|
465cbee2c3 | ||
|
|
33a2ae86c6 | ||
|
|
d906fc4747 | ||
|
|
055ef6ac8e | ||
|
|
e63658c082 | ||
|
|
f4b0ed0a4a | ||
|
|
1e5ed26f76 | ||
|
|
d97095a38d | ||
|
|
e48507901e | ||
|
|
a43ef8ec04 | ||
|
|
874f25aa80 | ||
|
|
12a730a394 | ||
|
|
bdcd41f54d | ||
|
|
96fb8eb7d3 | ||
|
|
d2c333dbd6 | ||
|
|
054f0f63ff | ||
|
|
eb05ea5994 | ||
|
|
4f870ec4c9 | ||
|
|
31f9b6cfea | ||
|
|
07a93ff0b7 | ||
|
|
d2b098ed92 | ||
|
|
09a7f4371a | ||
|
|
b4bf78f0d6 | ||
|
|
de9c5a6dc1 | ||
|
|
8a661df059 | ||
|
|
c7c16d4197 | ||
|
|
3aec0346a9 | ||
|
|
8d2f720de1 | ||
|
|
4b8da68df4 | ||
|
|
6460979bab | ||
|
|
3ae9dbd717 | ||
|
|
0816566d03 | ||
|
|
a97044edfc | ||
|
|
9d92a9a136 | ||
|
|
8e12c79ea6 | ||
|
|
5777268441 | ||
|
|
34eaa418d5 | ||
|
|
77f12610dd | ||
|
|
5be737ad70 | ||
|
|
0ae1e26ba8 | ||
|
|
79721b7af6 | ||
|
|
093e290221 | ||
|
|
1e6957720e | ||
|
|
a0f263bb4f | ||
|
|
413a28c089 | ||
|
|
a90e8662e1 | ||
|
|
955d384f96 | ||
|
|
ea5ece9ab2 | ||
|
|
8ff532bb67 | ||
|
|
eb6cfd51ae | ||
|
|
11d6249007 | ||
|
|
497c250e6d | ||
|
|
2872c1827a | ||
|
|
f9c49d44b5 | ||
|
|
12043f027c | ||
|
|
0c28d34d57 | ||
|
|
271b00c2c4 | ||
|
|
b096f4129a | ||
|
|
7ab036f5f7 | ||
|
|
9ffe808797 | ||
|
|
acfb8259bc | ||
|
|
298144bd36 | ||
|
|
ff9d8cba69 | ||
|
|
8b2769706e | ||
|
|
b8606ef0bb | ||
|
|
44de53a49d | ||
|
|
9abab01b9c | ||
|
|
53414687ff | ||
|
|
d2ec21c7b1 | ||
|
|
4deee3a9cb | ||
|
|
b50ce2b060 | ||
|
|
ffc72367d5 |
+5
-1
@@ -1,4 +1,8 @@
|
|||||||
PORT=
|
PORT=
|
||||||
DATABASE_URL=
|
SURREALDB_URL=
|
||||||
|
SURREALDB_USERNAME=
|
||||||
|
SURREALDB_PASSWORD=
|
||||||
|
SURREALDB_NAMESPACE=
|
||||||
|
SURREALDB_DBNAME=
|
||||||
ACCESS_TOKEN_SECRET=
|
ACCESS_TOKEN_SECRET=
|
||||||
REFRESH_TOKEN_SECRET=
|
REFRESH_TOKEN_SECRET=
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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,65 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
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_IP }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_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_IP }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_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_IP }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_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,19 @@
|
|||||||
|
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
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
/.direnv
|
/.direnv
|
||||||
|
/Cargo.nix
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.envrc
|
.envrc
|
||||||
|
|||||||
Generated
+3237
-869
File diff suppressed because it is too large
Load Diff
+36
-11
@@ -1,22 +1,47 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "imphnen-cms-be"
|
resolver = "2"
|
||||||
version = "0.1.0"
|
members = [
|
||||||
edition = "2021"
|
"tests",
|
||||||
|
"imphnen-iam",
|
||||||
|
"imphnen-cms",
|
||||||
|
"imphnen-libs",
|
||||||
|
"imphnen-utils",
|
||||||
|
"imphnen-gacha",
|
||||||
|
"imphnen-gateway",
|
||||||
|
"imphnen-backend",
|
||||||
|
"imphnen-entities",
|
||||||
|
"imphnen-dimentorin",
|
||||||
|
"imphnen-middleware",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
axum = { version = "0.8.1", features = ["multipart"] }
|
[workspace.dependencies]
|
||||||
|
axum = { version = "0.8.4", features = ["multipart"] }
|
||||||
log = "0.4.25"
|
log = "0.4.25"
|
||||||
sea-orm = { version = "1.1.4", features = ["sqlx-postgres", "macros", "with-json", "with-uuid", "runtime-tokio", "runtime-tokio-native-tls"] }
|
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
serde_json = "1.0.138"
|
serde_json = "1.0.138"
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
tokio = { version = "1.45.0" }
|
||||||
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
argon2 = { version = "0.5.3", features = ["password-hash"] }
|
||||||
jsonwebtoken = "9.3.1"
|
jsonwebtoken = "9.3.1"
|
||||||
chrono = "0.4.39"
|
chrono = "0.4.41"
|
||||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
||||||
redis = "0.28.2"
|
lettre = { version = "0.11.16", features = ["tokio1-native-tls"] }
|
||||||
|
surrealdb = { version = "2.3.2", features = ["kv-mem"] }
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
rand = { version = "0.9.1", features = ["std", "alloc"] }
|
||||||
|
rand_distr = "0.5.1"
|
||||||
|
tower-http = { version = "0.6.4", features = ["cors"] }
|
||||||
|
validator = { version = "0.12", features = ["derive"] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
regex = "1.11.1"
|
||||||
|
axum-test = "17.2.0"
|
||||||
|
fancy-regex = "0.14.0"
|
||||||
|
futures = "0.3.31"
|
||||||
|
tower = "0.5.2"
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
+37
-12
@@ -1,22 +1,47 @@
|
|||||||
FROM rust:1.83-slim-bullseye AS builder
|
FROM rust:1.85-alpine AS builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apk add --no-cache \
|
||||||
build-essential \
|
curl \
|
||||||
libssl-dev \
|
musl-dev \
|
||||||
pkg-config \
|
openssl-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
openssl-libs-static \
|
||||||
|
pkgconfig
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY ./src ./src
|
|
||||||
|
|
||||||
RUN cargo build --release && strip /app/target/release/imphnen-cms-api
|
RUN mkdir -p imphnen-backend/src imphnen-cms/src imphnen-dimentorin/src \
|
||||||
|
imphnen-entities/src imphnen-gacha/src imphnen-gateway/src \
|
||||||
|
imphnen-iam/src imphnen-libs/src imphnen-middleware/src \
|
||||||
|
imphnen-utils/src tests/src && \
|
||||||
|
echo "fn main() {}" > imphnen-backend/src/main.rs && \
|
||||||
|
find . -name "src" -type d -exec sh -c 'echo "// dummy" > "$1/lib.rs"' _ {} \;
|
||||||
|
|
||||||
FROM gcr.io/distroless/cc AS runner
|
RUN echo '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /app/target/release/imphnen-cms-api .
|
RUN echo -e '[package]\nname = "tests"\nversion = "0.1.0"\nedition = "2021"' > tests/Cargo.toml
|
||||||
|
|
||||||
CMD ["/app/imphnen-cms-api"]
|
|
||||||
|
COPY imphnen-backend ./imphnen-backend
|
||||||
|
COPY imphnen-cms ./imphnen-cms
|
||||||
|
COPY imphnen-dimentorin ./imphnen-dimentorin
|
||||||
|
COPY imphnen-entities ./imphnen-entities
|
||||||
|
COPY imphnen-gacha ./imphnen-gacha
|
||||||
|
COPY imphnen-gateway ./imphnen-gateway
|
||||||
|
COPY imphnen-iam ./imphnen-iam
|
||||||
|
COPY imphnen-libs ./imphnen-libs
|
||||||
|
COPY imphnen-middleware ./imphnen-middleware
|
||||||
|
COPY imphnen-utils ./imphnen-utils
|
||||||
|
COPY tests ./tests
|
||||||
|
|
||||||
|
RUN RUSTFLAGS="-C target-cpu=generic -C opt-level=s -C panic=abort -C codegen-units=1 -C strip=symbols" \
|
||||||
|
cargo build -p imphnen-backend --release && \
|
||||||
|
strip target/release/api && \
|
||||||
|
upx --best --lzma target/release/api 2>/dev/null || true
|
||||||
|
|
||||||
|
FROM scratch AS runner
|
||||||
|
COPY --from=builder /app/target/release/api /api
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
ENTRYPOINT ["/api"]
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# IMPHNEN Backend Service
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
This repository serves as the **monorepo** for all backend services of IMPHNEN. It encompasses several main services:
|
||||||
|
|
||||||
|
1. **IMPHNEN-Backend** - Provides fundamental functionalities and shared resources for other services.
|
||||||
|
2. **IMPHNEN-IAM** - Handles identity and access management across IMPHNEN applications.
|
||||||
|
3. **IMPHNEN-CMS** - Supports the cms services by IMPHNEN [Landing Page website](https://imphnen.dev/).
|
||||||
|
4. **IMPHNEN-Gacha** - Supports the gacha services by IMPHNEN [Gacha website](https://gacha.imphnen.dev/).
|
||||||
|
5. **IMPHNEN-Dimentorin** - Supports the mentoring services by IMPHNEN [Dimentorin website](https://dimentorin.imphnen.dev/).
|
||||||
|
6. **IMPHNEN-Gateway** - Acts as the API gateway, routing requests to appropriate services.
|
||||||
|
7. **IMPHNEN-Middleware** - Acts as the middleware for the API Gateway, providing authentication and authorization.
|
||||||
|
|
||||||
|
## How to Install
|
||||||
|
|
||||||
|
1. **Clone the repository**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-backend-service.git
|
||||||
|
cd imphnen-backend-service
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up the environment**:
|
||||||
|
|
||||||
|
- Copy the example environment files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
if you use windows based system
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./apply-env.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
if you use unix based system
|
||||||
|
|
||||||
|
```sh
|
||||||
|
source ./apply-env.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- Modify the `.env` files with your specific configuration settings.
|
||||||
|
|
||||||
|
3. **Install dependencies**:
|
||||||
|
|
||||||
|
Ensure you have [Rust](https://www.rust-lang.org/) installed. Then, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the seeders**:
|
||||||
|
|
||||||
|
to run the seeders, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --bin seeder
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
To run the services in development mode:
|
||||||
|
|
||||||
|
1. **Start the database and other dependencies** using Docker Compose:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run using cargo run**. For example, to run the Core Service:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --bin api
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run using cargo watch**. For example, to run the Core Service:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo watch -x "run --bin api"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
For production deployment:
|
||||||
|
|
||||||
|
1. **Build the Docker image**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t imphnen-backend .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the Docker container**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --name imphnen-backend -d --env-file .env -p 3000:3000 imphnen-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust the port and environment variables as needed.
|
||||||
|
|
||||||
|
## How to Run the Tests
|
||||||
|
|
||||||
|
1. **Run the tests**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test -p tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
1. **Fork the repository** and clone it locally.
|
||||||
|
2. **Create a new branch** for your feature or fix:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git checkout -b feat/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Make your changes**, commit them, and push to your forked repository.
|
||||||
|
4. **Create a pull request** to the `develop` branch of this repository.
|
||||||
|
|
||||||
|
If you encounter any issues or have questions, feel free to create a new issue in the repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Note: For detailed API documentation, please refer to our [API Docs](https://api.imphnen.dev/docs)._
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
:: Cek apakah file .env ada
|
||||||
|
if not exist ".env" (
|
||||||
|
echo File .env tidak ditemukan di direktori saat ini.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Memuat variabel dari .env...
|
||||||
|
|
||||||
|
:: Baca file .env baris per baris
|
||||||
|
for /f "tokens=*" %%a in ('type ".env" ^| findstr /v "^$" ^| findstr /v "^#"') do (
|
||||||
|
echo.%%a | findstr "=" >nul && (
|
||||||
|
for /f "tokens=1,2 delims==" %%b in ("%%a") do (
|
||||||
|
set "key=%%b"
|
||||||
|
set "value=%%c"
|
||||||
|
:: Trim whitespace
|
||||||
|
call :trimValue key value
|
||||||
|
echo Set variabel: %%b=%%c
|
||||||
|
setx %%b %%c >nul
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Semua variabel telah dimuat.
|
||||||
|
endlocal
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:: Fungsi trim (sederhana)
|
||||||
|
:trimValue
|
||||||
|
set "%1=%[%1]%"
|
||||||
|
set "%2=%[%2]%"
|
||||||
|
goto :eof
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
function Set-TempEnvFromDotEnv {
|
||||||
|
param (
|
||||||
|
[string]$envFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-Not (Test-Path $envFilePath)) {
|
||||||
|
Write-Error "The .env file at path '$envFilePath' does not exist."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$envContent = Get-Content $envFilePath
|
||||||
|
|
||||||
|
foreach ($line in $envContent) {
|
||||||
|
$trimmedLine = $line.Trim()
|
||||||
|
|
||||||
|
if (-Not [string]::IsNullOrWhiteSpace($trimmedLine) -and -Not $trimmedLine.StartsWith("#")) {
|
||||||
|
$keyValue = $trimmedLine -split "=", 2
|
||||||
|
if ($keyValue.Length -eq 2) {
|
||||||
|
$key = $keyValue[0].Trim()
|
||||||
|
$value = $keyValue[1].Trim()
|
||||||
|
[System.Environment]::SetEnvironmentVariable($key, $value, [System.EnvironmentVariableTarget]::Process)
|
||||||
|
Write-Host "Set temporary environment variable: $key=$value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "All environment variables from '$envFilePath' have been set temporarily."
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-TempEnvFromDotEnv -envFilePath ".env"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set_temp_env_from_dotenv() {
|
||||||
|
local env_file_path="$1"
|
||||||
|
|
||||||
|
if [[ ! -f "$env_file_path" ]]; then
|
||||||
|
echo "Error: The .env file at path '$env_file_path' does not exist."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
trimmed_line=$(echo "$line" | xargs)
|
||||||
|
|
||||||
|
if [[ -n "$trimmed_line" && ! "$trimmed_line" =~ ^# ]]; then
|
||||||
|
key=$(echo "$trimmed_line" | cut -d '=' -f 1 | xargs)
|
||||||
|
value=$(echo "$trimmed_line" | cut -d '=' -f 2- | xargs)
|
||||||
|
export "$key=$value"
|
||||||
|
echo "Set temporary environment variable: $key=$value"
|
||||||
|
fi
|
||||||
|
done < "$env_file_path"
|
||||||
|
|
||||||
|
echo "All environment variables from '$env_file_path' have been set temporarily."
|
||||||
|
}
|
||||||
|
|
||||||
|
set_temp_env_from_dotenv ".env"
|
||||||
@@ -6,3 +6,11 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${PORT}:${PORT}"
|
- "${PORT}:${PORT}"
|
||||||
env_file: ".env"
|
env_file: ".env"
|
||||||
|
depends_on:
|
||||||
|
- surrealdb
|
||||||
|
|
||||||
|
surrealdb:
|
||||||
|
image: surrealdb/surrealdb:latest
|
||||||
|
command: start --log trace --user root --pass root
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 351 KiB |
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1739020877,
|
||||||
|
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -1,24 +1,32 @@
|
|||||||
{
|
{
|
||||||
description = "IMPHNEN CMS API Nix Flake";
|
description = "IMPHNEN Backend Service Nix Flake";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
}: let
|
}: let
|
||||||
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
||||||
|
pkgsFor = system:
|
||||||
|
import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config = {
|
||||||
|
allowUnfree = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
pkgsFor = nixpkgs.legacyPackages;
|
|
||||||
in {
|
in {
|
||||||
packages = forAllSystems (system: {
|
packages = forAllSystems (system: {
|
||||||
default = pkgsFor.${system}.callPackage ./default.nix {};
|
default = (pkgsFor system).callPackage ./default.nix {};
|
||||||
});
|
});
|
||||||
devShells = forAllSystems (system: {
|
devShells = forAllSystems (system: {
|
||||||
default = pkgsFor.${system}.callPackage ./shell.nix {};
|
default = (pkgsFor system).callPackage ./shell.nix {};
|
||||||
});
|
});
|
||||||
dockerImages = forAllSystems (system: {
|
dockerImages = forAllSystems (system: {
|
||||||
tryOutApi = pkgsFor.${system}.callPackage ./docker.nix {};
|
tryOutApi = (pkgsFor system).callPackage ./docker.nix {};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
imphnen-gateway = { version = "0.1.0", path = "../imphnen-gateway" }
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||||
|
imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use env_logger;
|
||||||
|
use imphnen_gateway::gateway_service;
|
||||||
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||||
|
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||||
|
use imphnen_utils::{get_iso_date, Env};
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::engine::any;
|
||||||
|
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||||
|
use imphnen_libs::enviroment::load_env;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
load_env();
|
||||||
|
let env = Env::new();
|
||||||
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
|
db.signin(Root {
|
||||||
|
username: &env.surrealdb_username,
|
||||||
|
password: &env.surrealdb_password,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
db.use_ns(env.surrealdb_namespace)
|
||||||
|
.use_db(env.surrealdb_dbname)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let events = vec![
|
||||||
|
(
|
||||||
|
"e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o",
|
||||||
|
"Tech Conference 2025",
|
||||||
|
"Annual technology conference featuring the latest innovations in software development, AI, and cloud computing.",
|
||||||
|
"https://techconf2025.example.com",
|
||||||
|
150.0,
|
||||||
|
Some("Jakarta Convention Center".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-06-15T09:00:00Z",
|
||||||
|
"2025-06-17T18:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"f2b3c4d5-6e7f-8g9h-0i1j-2k3l4m5n6o7p",
|
||||||
|
"Online Web Development Workshop",
|
||||||
|
"Comprehensive workshop covering modern web development frameworks including React, Vue, and Angular.",
|
||||||
|
"https://webdev-workshop.example.com",
|
||||||
|
75.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2025-07-10T14:00:00Z",
|
||||||
|
"2025-07-10T17:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"g3c4d5e6-7f8g-9h0i-1j2k-3l4m5n6o7p8q",
|
||||||
|
"Startup Pitch Competition",
|
||||||
|
"Exciting competition where emerging startups present their innovative ideas to a panel of expert judges and investors.",
|
||||||
|
"https://startup-pitch.example.com",
|
||||||
|
25.0,
|
||||||
|
Some("Innovation Hub Surabaya".to_string()),
|
||||||
|
false,
|
||||||
|
"2025-08-05T10:00:00Z",
|
||||||
|
"2025-08-05T16:00:00Z",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"h4d5e6f7-8g9h-0i1j-2k3l-4m5n6o7p8q9r",
|
||||||
|
"Digital Marketing Masterclass",
|
||||||
|
"Learn advanced digital marketing strategies, social media optimization, and data-driven marketing techniques.",
|
||||||
|
"https://digital-marketing.example.com",
|
||||||
|
100.0,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
"2025-09-20T13:00:00Z",
|
||||||
|
"2025-09-22T15:00:00Z",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (id, name, description, detail_link, price, location, is_online, start_date, end_date) in events {
|
||||||
|
let event = EventsSchema {
|
||||||
|
id: Thing::from(("app_events", id)),
|
||||||
|
name: name.into(),
|
||||||
|
description: description.into(),
|
||||||
|
detail_link: detail_link.into(),
|
||||||
|
price,
|
||||||
|
location,
|
||||||
|
is_online,
|
||||||
|
is_deleted: false,
|
||||||
|
start_date: start_date.into(),
|
||||||
|
end_date: end_date.into(),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
db.create::<Option<EventsSchema>>(("app_events", id))
|
||||||
|
.content(event)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("✅ Inserted event: {} ({})", name, if is_online { "Online" } else { "In-person" });
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ All Events seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use imphnen_iam::PermissionsEnum;
|
||||||
|
use imphnen_libs::enviroment::load_env;
|
||||||
|
use imphnen_utils::{get_iso_date, Env};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::engine::any;
|
||||||
|
use surrealdb::opt::auth::Root;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
load_env();
|
||||||
|
|
||||||
|
let env = Env::new();
|
||||||
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
|
db.signin(Root {
|
||||||
|
username: &env.surrealdb_username,
|
||||||
|
password: &env.surrealdb_password,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
db.use_ns(env.surrealdb_namespace)
|
||||||
|
.use_db(env.surrealdb_dbname)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for permission in [
|
||||||
|
PermissionsEnum::ReadListUsers,
|
||||||
|
PermissionsEnum::ReadDetailUsers,
|
||||||
|
PermissionsEnum::CreateUsers,
|
||||||
|
PermissionsEnum::DeleteUsers,
|
||||||
|
PermissionsEnum::UpdateUsers,
|
||||||
|
PermissionsEnum::ActivateUsers,
|
||||||
|
PermissionsEnum::ReadListRoles,
|
||||||
|
PermissionsEnum::ReadDetailRoles,
|
||||||
|
PermissionsEnum::CreateRoles,
|
||||||
|
PermissionsEnum::DeleteRoles,
|
||||||
|
PermissionsEnum::UpdateRoles,
|
||||||
|
PermissionsEnum::ReadListPermissions,
|
||||||
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
|
PermissionsEnum::CreatePermissions,
|
||||||
|
PermissionsEnum::DeletePermissions,
|
||||||
|
PermissionsEnum::UpdatePermissions,
|
||||||
|
PermissionsEnum::CreateGachaClaims,
|
||||||
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
|
PermissionsEnum::ReadListGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::CreateGachaItems,
|
||||||
|
PermissionsEnum::DeleteGachaItems,
|
||||||
|
PermissionsEnum::UpdateGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
PermissionsEnum::CreateGachaRolls,
|
||||||
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
] {
|
||||||
|
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||||
|
.bind(("id", permission.id()))
|
||||||
|
.bind((
|
||||||
|
"data",
|
||||||
|
json!({
|
||||||
|
"name": permission.to_string(),
|
||||||
|
"is_deleted": false,
|
||||||
|
"created_at": get_iso_date(),
|
||||||
|
"updated_at": get_iso_date()
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
println!("✅ Inserted: {}", permission.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ All Permissions seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use imphnen_utils::{get_iso_date, Env};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::opt::auth::Root;
|
||||||
|
use imphnen_libs::enviroment::load_env;
|
||||||
|
use surrealdb::engine::any;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
load_env();
|
||||||
|
let env = Env::new();
|
||||||
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
|
db.signin(Root {
|
||||||
|
username: &env.surrealdb_username,
|
||||||
|
password: &env.surrealdb_password,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
db.use_ns(env.surrealdb_namespace)
|
||||||
|
.use_db(env.surrealdb_dbname)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let roles = vec![
|
||||||
|
(
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
|
"Staf",
|
||||||
|
Some("2025-02-24T16:52:27.630453+00"),
|
||||||
|
Some("2025-02-24T16:52:27.630461+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
"User",
|
||||||
|
None,
|
||||||
|
Some("2025-02-28T14:53:58.576688+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"60f1aeb7-dad2-4e06-bcb5-be1ba510c906",
|
||||||
|
"Staff Aktivasi User",
|
||||||
|
Some("2025-02-20T02:47:09.660640+00"),
|
||||||
|
Some("2025-02-20T02:48:30.083283+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0",
|
||||||
|
"Admin Pembayaran",
|
||||||
|
Some("2025-01-29T05:39:28.562667+00"),
|
||||||
|
Some("2025-03-12T22:56:29.597416+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
|
"Admin",
|
||||||
|
None,
|
||||||
|
Some("2025-02-22T15:38:39.868306+00"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (id, name, _created_at, _updated_at) in roles {
|
||||||
|
db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
|
||||||
|
.bind(("id", id))
|
||||||
|
.bind((
|
||||||
|
"data",
|
||||||
|
json!({
|
||||||
|
"name": name,
|
||||||
|
"permissions": [],
|
||||||
|
"is_deleted": false,
|
||||||
|
"created_at": get_iso_date(),
|
||||||
|
"updated_at": get_iso_date(),
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
println!("✅ Inserted role: {}", name);
|
||||||
|
}
|
||||||
|
println!("✅ All Roles seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use imphnen_iam::{get_iso_date, make_thing, Env, PermissionsEnum};
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::opt::auth::Root;
|
||||||
|
use surrealdb::engine::any;
|
||||||
|
use imphnen_libs::enviroment::load_env;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
load_env();
|
||||||
|
let env = Env::new();
|
||||||
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
|
db.signin(Root {
|
||||||
|
username: &env.surrealdb_username,
|
||||||
|
password: &env.surrealdb_password,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
db.use_ns(env.surrealdb_namespace)
|
||||||
|
.use_db(env.surrealdb_dbname)
|
||||||
|
.await?;
|
||||||
|
let permission_refs_admin: Vec<_> = [
|
||||||
|
PermissionsEnum::ReadListUsers,
|
||||||
|
PermissionsEnum::ReadDetailUsers,
|
||||||
|
PermissionsEnum::CreateUsers,
|
||||||
|
PermissionsEnum::DeleteUsers,
|
||||||
|
PermissionsEnum::UpdateUsers,
|
||||||
|
PermissionsEnum::ActivateUsers,
|
||||||
|
PermissionsEnum::ReadListRoles,
|
||||||
|
PermissionsEnum::ReadDetailRoles,
|
||||||
|
PermissionsEnum::CreateRoles,
|
||||||
|
PermissionsEnum::DeleteRoles,
|
||||||
|
PermissionsEnum::UpdateRoles,
|
||||||
|
PermissionsEnum::ReadListPermissions,
|
||||||
|
PermissionsEnum::ReadDetailPermissions,
|
||||||
|
PermissionsEnum::CreatePermissions,
|
||||||
|
PermissionsEnum::DeletePermissions,
|
||||||
|
PermissionsEnum::UpdatePermissions,
|
||||||
|
PermissionsEnum::CreateGachaClaims,
|
||||||
|
PermissionsEnum::ReadDetailGachaClaims,
|
||||||
|
PermissionsEnum::ReadListGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaItems,
|
||||||
|
PermissionsEnum::CreateGachaItems,
|
||||||
|
PermissionsEnum::DeleteGachaItems,
|
||||||
|
PermissionsEnum::UpdateGachaItems,
|
||||||
|
PermissionsEnum::ReadDetailGachaRolls,
|
||||||
|
PermissionsEnum::CreateGachaRolls,
|
||||||
|
PermissionsEnum::ExecuteGachaRolls,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|perm| make_thing("app_permissions", perm.id()))
|
||||||
|
.collect();
|
||||||
|
let admin_role_id = "f6b03f25-e416-4893-ac88-caaa690afb07";
|
||||||
|
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||||
|
.bind(("role_id", admin_role_id))
|
||||||
|
.bind(("permissions", permission_refs_admin))
|
||||||
|
.bind(("updated_at", get_iso_date()))
|
||||||
|
.await?;
|
||||||
|
println!("✅ All permissions successfully added to Admin role");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use imphnen_iam::UsersSchema;
|
||||||
|
use imphnen_libs::enviroment::load_env;
|
||||||
|
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
load_env();
|
||||||
|
let env = Env::new();
|
||||||
|
use surrealdb::engine::any;
|
||||||
|
let db = any::connect(&env.surrealdb_url).await?;
|
||||||
|
db.signin(Root {
|
||||||
|
username: &env.surrealdb_username,
|
||||||
|
password: &env.surrealdb_password,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
db.use_ns(env.surrealdb_namespace)
|
||||||
|
.use_db(env.surrealdb_dbname)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let users = vec![
|
||||||
|
(
|
||||||
|
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2",
|
||||||
|
"admin@example.com",
|
||||||
|
"Admin",
|
||||||
|
"f6b03f25-e416-4893-ac88-caaa690afb07",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a4d23fb5-9e31-423c-9842-fbd6e75a5298",
|
||||||
|
"staff@example.com",
|
||||||
|
"Staff",
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"d5e89c12-72af-4b1a-abc3-ff1234567890",
|
||||||
|
"user@example.com",
|
||||||
|
"User",
|
||||||
|
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (id, email, fullname, role_id) in users {
|
||||||
|
let user = UsersSchema {
|
||||||
|
id: Thing::from(("app_users", id)),
|
||||||
|
fullname: fullname.into(),
|
||||||
|
email: email.into(),
|
||||||
|
password: hash_password("password").unwrap(),
|
||||||
|
avatar: None,
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
gender: None,
|
||||||
|
birthdate: None,
|
||||||
|
role: Thing::from(("app_roles", role_id)),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
db.create::<Option<UsersSchema>>(("app_users", id))
|
||||||
|
.content(user)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("✅ Inserted user: {} ({})", fullname, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ All Users seeded");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
println!("🔧 Seeding: {bin}");
|
||||||
|
let status = Command::new("cargo").args(["run", "--bin", bin]).status()?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
Err(format!("❌ Failed to run seed: {bin}").into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
println!("🚀 Running all seeders...\n");
|
||||||
|
|
||||||
|
run_seed("seed_permissions")?;
|
||||||
|
run_seed("seed_roles")?;
|
||||||
|
run_seed("seed_roles_permissions")?;
|
||||||
|
run_seed("seed_users")?;
|
||||||
|
run_seed("seed_events")?;
|
||||||
|
|
||||||
|
println!("\n✅ All seeding completed successfully.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use imphnen_gateway::gateway_service;
|
||||||
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||||
|
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-cms"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod v1;
|
||||||
|
pub use v1::*;
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
use super::{
|
||||||
|
events_dto::{
|
||||||
|
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||||
|
EventsUpdateRequestDto,
|
||||||
|
},
|
||||||
|
events_service::EventsService,
|
||||||
|
};
|
||||||
|
use axum::extract::{Path, Query};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use imphnen_libs::{
|
||||||
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
|
ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/events",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
("filter" = Option<String>, Query, description = "Filter value"),
|
||||||
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn get_event_list(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(meta): Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
EventsService::get_event_list(&state, meta).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/events/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn get_event_by_id(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
EventsService::get_event_by_id(&state, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/cms/landing/events/create",
|
||||||
|
request_body = EventsCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new event", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_event(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<EventsCreateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
EventsService::create_event(&state, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/cms/landing/events/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
request_body = EventsUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update event", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn patch_update_event(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<EventsUpdateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
EventsService::update_event(&state, id, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/cms/landing/events/delete/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Event ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Soft delete event", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Events"
|
||||||
|
)]
|
||||||
|
pub async fn delete_event(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
EventsService::delete_event(&state, id).await
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
// Lazy static regex for URL validation
|
||||||
|
lazy_static! {
|
||||||
|
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct EventsCreateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Name is required"))]
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[validate(length(min = 1, message = "Description is required"))]
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
#[validate(regex(
|
||||||
|
path = "VALID_URL_REGEX",
|
||||||
|
message = "Detail link must be a valid URL"
|
||||||
|
))]
|
||||||
|
pub detail_link: String,
|
||||||
|
|
||||||
|
#[validate(range(min = 0, message = "Price cannot be negative"))]
|
||||||
|
pub price: f64,
|
||||||
|
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub is_online: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct EventsUpdateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Name is required"))]
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub start_date: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EventsDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub start_date: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct EventsQueryDto {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub start_date: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventsQueryDto {
|
||||||
|
pub fn from(self) -> EventsListItemDto {
|
||||||
|
EventsListItemDto {
|
||||||
|
id: self.id.id.to_raw(),
|
||||||
|
name: self.name,
|
||||||
|
description: self.description,
|
||||||
|
detail_link: self.detail_link,
|
||||||
|
price: self.price,
|
||||||
|
location: self.location,
|
||||||
|
is_online: self.is_online,
|
||||||
|
start_date: self.start_date,
|
||||||
|
end_date: self.end_date,
|
||||||
|
created_at: self.created_at,
|
||||||
|
is_deleted: self.is_deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||||
|
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||||
|
|
||||||
|
pub struct EventsRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EventsRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_event_list(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> {
|
||||||
|
let query = ListQueryBuilder::new(&ResourceEnum::Events.to_string())
|
||||||
|
.with_select_fields(vec!["*"])
|
||||||
|
.with_pagination(meta.page, Some(10))
|
||||||
|
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||||
|
.build();
|
||||||
|
let res: Vec<EventsQueryDto> =
|
||||||
|
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||||
|
let data = ResponseListSuccessDto {
|
||||||
|
data: res,
|
||||||
|
meta: None,
|
||||||
|
};
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get event by ID
|
||||||
|
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||||
|
.with_id(&id)
|
||||||
|
.with_select_fields(vec!["*"]);
|
||||||
|
let sql = builder.build();
|
||||||
|
let result: Option<EventsQueryDto> =
|
||||||
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Some(event) => {
|
||||||
|
if event.is_deleted {
|
||||||
|
bail!("Event not found");
|
||||||
|
}
|
||||||
|
Ok(event)
|
||||||
|
}
|
||||||
|
None => bail!("Event not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new event
|
||||||
|
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<EventsSchema> = db
|
||||||
|
.create(ResourceEnum::Events.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success create event".into()),
|
||||||
|
None => bail!("Failed to create event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing event
|
||||||
|
pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
|
||||||
|
// Cek apakah event ada
|
||||||
|
let existing = self.query_event_by_id(data.id.id.to_raw()).await?;
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Event already deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge field tertentu jika diperlukan
|
||||||
|
let merged = EventsSchema {
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
..data
|
||||||
|
};
|
||||||
|
|
||||||
|
let record_key = get_id(&merged.id)?;
|
||||||
|
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update event".into()),
|
||||||
|
None => bail!("Failed to update event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft delete event (mark is_deleted = true)
|
||||||
|
pub async fn query_delete_event(&self, id: String) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let event = self.query_event_by_id(id).await?;
|
||||||
|
if event.is_deleted {
|
||||||
|
bail!("Event not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let record_key = get_id(&event.id)?;
|
||||||
|
let record: Option<EventsSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete event".into()),
|
||||||
|
None => bail!("Failed to delete event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
|
use surrealdb::Uuid;
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use imphnen_utils::{get_iso_date, make_thing};
|
||||||
|
|
||||||
|
use super::events_dto::{EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct EventsSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub price: f64,
|
||||||
|
pub is_online: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub name: String,
|
||||||
|
pub end_date: String,
|
||||||
|
pub start_date: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub description: String,
|
||||||
|
pub detail_link: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventsSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Events.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
name: String::new(),
|
||||||
|
description: String::new(),
|
||||||
|
detail_link: String::new(),
|
||||||
|
price: 0.0,
|
||||||
|
location: None,
|
||||||
|
is_online: false,
|
||||||
|
is_deleted: false,
|
||||||
|
start_date: String::new(),
|
||||||
|
end_date: String::new(),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventsSchema {
|
||||||
|
pub fn from(dto: EventsQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description,
|
||||||
|
detail_link: dto.detail_link,
|
||||||
|
price: dto.price,
|
||||||
|
location: dto.location,
|
||||||
|
is_online: dto.is_online,
|
||||||
|
is_deleted: false,
|
||||||
|
start_date: dto.start_date,
|
||||||
|
end_date: dto.end_date,
|
||||||
|
created_at: dto.created_at,
|
||||||
|
updated_at: dto.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(payload: EventsCreateRequestDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Events.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
name: payload.name,
|
||||||
|
description: payload.description,
|
||||||
|
detail_link: payload.detail_link,
|
||||||
|
price: payload.price,
|
||||||
|
location: payload.location,
|
||||||
|
is_online: payload.is_online,
|
||||||
|
is_deleted: false,
|
||||||
|
end_date: payload.end_date.to_string(),
|
||||||
|
start_date: payload.start_date.to_string(),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(payload: EventsUpdateRequestDto, id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(&ResourceEnum::Events.to_string(), &id),
|
||||||
|
name: payload.name,
|
||||||
|
price: payload.price,
|
||||||
|
location: payload.location,
|
||||||
|
is_online: payload.is_online,
|
||||||
|
description: payload.description,
|
||||||
|
detail_link: payload.detail_link,
|
||||||
|
end_date: payload.end_date.to_string(),
|
||||||
|
start_date: payload.start_date.to_string(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use super::{
|
||||||
|
events_dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto, EventsUpdateRequestDto},
|
||||||
|
events_repository::EventsRepository,
|
||||||
|
events_schema::EventsSchema,
|
||||||
|
};
|
||||||
|
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||||
|
use imphnen_utils::{common_response, success_list_response, success_response, validate_request};
|
||||||
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
|
||||||
|
pub struct EventsService;
|
||||||
|
|
||||||
|
impl EventsService {
|
||||||
|
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||||
|
let repo = EventsRepository::new(state);
|
||||||
|
match repo.query_event_list(meta).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let items: Vec<EventsListItemDto> = data.data
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| !e.is_deleted)
|
||||||
|
.map(EventsQueryDto::from)
|
||||||
|
.collect();
|
||||||
|
let response = ResponseListSuccessDto {
|
||||||
|
data: items,
|
||||||
|
meta: data.meta,
|
||||||
|
};
|
||||||
|
success_list_response(response)
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = EventsRepository::new(state);
|
||||||
|
match repo.query_event_by_id(id).await {
|
||||||
|
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
|
||||||
|
data: EventsDetailItemDto {
|
||||||
|
id: event.id.id.to_raw(),
|
||||||
|
name: event.name,
|
||||||
|
description: event.description,
|
||||||
|
detail_link: event.detail_link,
|
||||||
|
price: event.price,
|
||||||
|
is_online: event.is_online,
|
||||||
|
start_date: event.start_date,
|
||||||
|
end_date: event.end_date,
|
||||||
|
created_at: event.created_at,
|
||||||
|
updated_at: event.updated_at,
|
||||||
|
location: event.location,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_event(state: &AppState, payload: EventsCreateRequestDto) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = EventsRepository::new(state);
|
||||||
|
let schema = EventsSchema::create(payload);
|
||||||
|
match repo.query_create_event(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_event(state: &AppState, id: String, payload: EventsUpdateRequestDto) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = EventsRepository::new(state);
|
||||||
|
let schema = EventsSchema::update(payload, id);
|
||||||
|
match repo.query_update_event(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_event(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = EventsRepository::new(state);
|
||||||
|
match repo.query_delete_event(id).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{delete, get, patch, post},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod events_controller;
|
||||||
|
pub mod events_dto;
|
||||||
|
pub mod events_repository;
|
||||||
|
pub mod events_schema;
|
||||||
|
pub mod events_service;
|
||||||
|
|
||||||
|
pub use events_controller::*;
|
||||||
|
pub use events_dto::*;
|
||||||
|
pub use events_repository::*;
|
||||||
|
pub use events_schema::*;
|
||||||
|
pub use events_service::*;
|
||||||
|
|
||||||
|
pub fn events_public_routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/cms/landing/events",
|
||||||
|
get(events_controller::get_event_list),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/cms/landing/events/detail/{id}",
|
||||||
|
get(events_controller::get_event_by_id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events_protected_routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/cms/landing/events/create",
|
||||||
|
post(events_controller::post_create_event),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/cms/landing/events/update/{id}",
|
||||||
|
patch(events_controller::patch_update_event),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/cms/landing/events/delete/{id}",
|
||||||
|
delete(events_controller::delete_event),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod events;
|
||||||
|
pub mod testimonials;
|
||||||
|
|
||||||
|
pub use events::*;
|
||||||
|
pub use testimonials::*;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{delete, get, patch, post},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod testimonials_controller;
|
||||||
|
pub mod testimonials_dto;
|
||||||
|
pub mod testimonials_repository;
|
||||||
|
pub mod testimonials_schema;
|
||||||
|
pub mod testimonials_service;
|
||||||
|
|
||||||
|
pub use testimonials_controller::*;
|
||||||
|
pub use testimonials_dto::*;
|
||||||
|
pub use testimonials_repository::*;
|
||||||
|
pub use testimonials_schema::*;
|
||||||
|
pub use testimonials_service::*;
|
||||||
|
|
||||||
|
pub fn testimonials_public_routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/cms/landing/testimonials",
|
||||||
|
get(testimonials_controller::get_testimonial_list),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/cms/landing/testimonials/detail/{id}",
|
||||||
|
get(testimonials_controller::get_testimonial_by_id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn testimonials_protected_routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/cms/landing/testimonials/create",
|
||||||
|
post(testimonials_controller::post_create_testimonial),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/cms/landing/testimonials/update/{id}",
|
||||||
|
patch(testimonials_controller::patch_update_testimonial),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/cms/landing/testimonials/delete/{id}",
|
||||||
|
delete(testimonials_controller::delete_testimonial),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
use super::{
|
||||||
|
testimonials_dto::{
|
||||||
|
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||||
|
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||||
|
},
|
||||||
|
testimonials_service::TestimonialsService,
|
||||||
|
};
|
||||||
|
use axum::extract::{Path, Query};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use imphnen_iam::UsersDetailQueryDto;
|
||||||
|
use imphnen_libs::{
|
||||||
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
|
ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/testimonials",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
("filter" = Option<String>, Query, description = "Filter value"),
|
||||||
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn get_testimonial_list(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(meta): Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
TestimonialsService::get_testimonial_list(&state, meta).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/cms/landing/testimonials/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn get_testimonial_by_id(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
TestimonialsService::get_testimonial_by_id(&state, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/cms/landing/testimonials/create",
|
||||||
|
request_body = TestimonialsCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new testimonial", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_testimonial(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
Json(payload): Json<TestimonialsCreateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
println!("Authenticated User Now: {:?}", authenticated_user);
|
||||||
|
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/cms/landing/testimonials/update/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
|
),
|
||||||
|
request_body = TestimonialsUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update testimonial", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn patch_update_testimonial(
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
Json(payload): Json<TestimonialsUpdateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/cms/landing/testimonials/delete/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Testimonial ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Soft delete testimonial", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Testimonials"
|
||||||
|
)]
|
||||||
|
pub async fn delete_testimonial(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use imphnen_iam::users::UsersSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct TestimonialsCreateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Role is required"))]
|
||||||
|
pub role: String,
|
||||||
|
|
||||||
|
#[validate(length(
|
||||||
|
min = 1,
|
||||||
|
max = 500,
|
||||||
|
message = "Content must be between 1 and 500 characters"
|
||||||
|
))]
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct TestimonialsUpdateRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Role is required"))]
|
||||||
|
pub role: String,
|
||||||
|
|
||||||
|
#[validate(length(
|
||||||
|
min = 1,
|
||||||
|
max = 500,
|
||||||
|
message = "Content must be between 1 and 500 characters"
|
||||||
|
))]
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TestimonialsListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub user_fullname: String, // Assuming we'll fetch user's full name
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TestimonialsDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub user_fullname: String, // Assuming we'll fetch user's full name
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TestimonialsQueryDto {
|
||||||
|
pub id: Thing,
|
||||||
|
pub user: UsersSchema, // Change from Thing to UsersSchema
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestimonialsQueryDto {
|
||||||
|
pub fn from(self) -> TestimonialsListItemDto {
|
||||||
|
TestimonialsListItemDto {
|
||||||
|
id: self.id.id.to_raw(),
|
||||||
|
user_id: self.user.id.id.to_raw(),
|
||||||
|
user_fullname: self.user.fullname, // Extract fullname from UsersSchema
|
||||||
|
role: self.role,
|
||||||
|
content: self.content,
|
||||||
|
created_at: self.created_at,
|
||||||
|
is_deleted: self.is_deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use super::{
|
||||||
|
testimonials_dto::TestimonialsQueryDto, testimonials_schema::TestimonialsSchema,
|
||||||
|
};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||||
|
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||||
|
|
||||||
|
pub struct TestimonialsRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TestimonialsRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_testimonial_list(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
|
||||||
|
let query = ListQueryBuilder::new(&ResourceEnum::Testimonials.to_string())
|
||||||
|
.with_select_fields(vec!["*", "user.* as user"]) // Select user details
|
||||||
|
.with_pagination(meta.page, Some(10))
|
||||||
|
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||||
|
.build();
|
||||||
|
let res: Vec<TestimonialsQueryDto> =
|
||||||
|
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||||
|
let data = ResponseListSuccessDto {
|
||||||
|
data: res,
|
||||||
|
meta: None,
|
||||||
|
};
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_testimonial_by_id(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
) -> Result<TestimonialsQueryDto> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||||
|
.with_id(&id)
|
||||||
|
.with_select_fields(vec!["*", "user.* as user"]); // Select user details
|
||||||
|
let sql = builder.build();
|
||||||
|
let result: Option<TestimonialsQueryDto> =
|
||||||
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Some(testimonial) => {
|
||||||
|
if testimonial.is_deleted {
|
||||||
|
bail!("Testimonial not found");
|
||||||
|
}
|
||||||
|
Ok(testimonial)
|
||||||
|
}
|
||||||
|
None => bail!("Testimonial not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_create_testimonial(
|
||||||
|
&self,
|
||||||
|
data: TestimonialsSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<TestimonialsSchema> = db
|
||||||
|
.create(ResourceEnum::Testimonials.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success create testimonial".into()),
|
||||||
|
None => bail!("Failed to create testimonial"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_update_testimonial(
|
||||||
|
&self,
|
||||||
|
data: TestimonialsSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
|
||||||
|
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Testimonial already deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
let merged = TestimonialsSchema {
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
user: existing.user.id, // Preserve user ID
|
||||||
|
..data
|
||||||
|
};
|
||||||
|
|
||||||
|
let record_key = get_id(&merged.id)?;
|
||||||
|
let record: Option<TestimonialsSchema> =
|
||||||
|
db.update(record_key).merge(merged).await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update testimonial".into()),
|
||||||
|
None => bail!("Failed to update testimonial"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let testimonial = self.query_testimonial_by_id(id).await?;
|
||||||
|
if testimonial.is_deleted {
|
||||||
|
bail!("Testimonial not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let record_key = get_id(&testimonial.id)?;
|
||||||
|
let record: Option<TestimonialsSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete testimonial".into()),
|
||||||
|
None => bail!("Failed to delete testimonial"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use imphnen_libs::ResourceEnum;
|
||||||
|
use imphnen_utils::{get_iso_date, make_thing};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::Uuid;
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
use super::testimonials_dto::{
|
||||||
|
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TestimonialsSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub user: Thing, // Link to app_users table
|
||||||
|
pub role: String,
|
||||||
|
pub content: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestimonialsSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Testimonials.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
user: make_thing(
|
||||||
|
&ResourceEnum::Users.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(), // Placeholder, will be replaced by actual user ID
|
||||||
|
),
|
||||||
|
role: String::new(),
|
||||||
|
content: String::new(),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestimonialsSchema {
|
||||||
|
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id,
|
||||||
|
user: dto.user.id,
|
||||||
|
role: dto.role,
|
||||||
|
content: dto.content,
|
||||||
|
is_deleted: dto.is_deleted,
|
||||||
|
created_at: dto.created_at,
|
||||||
|
updated_at: dto.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Testimonials.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
user: user_id.clone(),
|
||||||
|
role: payload.role,
|
||||||
|
content: payload.content,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
payload: TestimonialsUpdateRequestDto,
|
||||||
|
id: String,
|
||||||
|
user_id: &Thing,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(&ResourceEnum::Testimonials.to_string(), &id),
|
||||||
|
role: payload.role,
|
||||||
|
content: payload.content,
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
user: user_id.clone(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
use super::{
|
||||||
|
testimonials_dto::{
|
||||||
|
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||||
|
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||||
|
},
|
||||||
|
testimonials_repository::TestimonialsRepository,
|
||||||
|
testimonials_schema::TestimonialsSchema,
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
use imphnen_libs::{
|
||||||
|
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
use imphnen_utils::{
|
||||||
|
common_response, success_list_response, success_response, validate_request,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct TestimonialsService;
|
||||||
|
|
||||||
|
impl TestimonialsService {
|
||||||
|
pub async fn get_testimonial_list(
|
||||||
|
state: &AppState,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = TestimonialsRepository::new(state);
|
||||||
|
match repo.query_testimonial_list(meta).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let items: Vec<TestimonialsListItemDto> = data
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| !e.is_deleted)
|
||||||
|
.map(|e| e.from())
|
||||||
|
.collect();
|
||||||
|
let response = ResponseListSuccessDto {
|
||||||
|
data: items,
|
||||||
|
meta: data.meta,
|
||||||
|
};
|
||||||
|
success_list_response(response)
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_testimonial_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = TestimonialsRepository::new(state);
|
||||||
|
match repo.query_testimonial_by_id(id).await {
|
||||||
|
Ok(testimonial) if !testimonial.is_deleted => {
|
||||||
|
success_response(ResponseSuccessDto {
|
||||||
|
data: TestimonialsDetailItemDto {
|
||||||
|
id: testimonial.id.id.to_raw(),
|
||||||
|
user_id: testimonial.user.id.id.to_raw(),
|
||||||
|
user_fullname: testimonial.user.fullname,
|
||||||
|
role: testimonial.role,
|
||||||
|
content: testimonial.content,
|
||||||
|
created_at: testimonial.created_at,
|
||||||
|
updated_at: testimonial.updated_at,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "Testimonial not found"),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_testimonial(
|
||||||
|
state: &AppState,
|
||||||
|
payload: TestimonialsCreateRequestDto,
|
||||||
|
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = TestimonialsRepository::new(state);
|
||||||
|
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
||||||
|
match repo.query_create_testimonial(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_testimonial(
|
||||||
|
state: &AppState,
|
||||||
|
id: String,
|
||||||
|
payload: TestimonialsUpdateRequestDto,
|
||||||
|
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = TestimonialsRepository::new(state);
|
||||||
|
let schema = TestimonialsSchema::update(payload, id, &authenticated_user.id);
|
||||||
|
match repo.query_update_testimonial(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_testimonial(
|
||||||
|
state: &AppState,
|
||||||
|
id: String,
|
||||||
|
_authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = TestimonialsRepository::new(state);
|
||||||
|
match repo.query_delete_testimonial(id).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod landing;
|
||||||
|
|
||||||
|
pub use landing::*;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-dimentorin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-entities"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{
|
||||||
|
engine::local::Db,
|
||||||
|
engine::any::Any,
|
||||||
|
Surreal,
|
||||||
|
};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct MessageResponseDto {
|
||||||
|
pub message: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoParams)]
|
||||||
|
pub struct MetaRequestDto {
|
||||||
|
pub page: Option<u64>,
|
||||||
|
pub per_page: Option<u64>,
|
||||||
|
pub search: Option<String>,
|
||||||
|
pub sort_by: Option<String>,
|
||||||
|
pub order: Option<String>,
|
||||||
|
pub filter: Option<String>,
|
||||||
|
pub filter_by: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MetaRequestDto {
|
||||||
|
fn default() -> Self {
|
||||||
|
MetaRequestDto {
|
||||||
|
page: Some(1),
|
||||||
|
per_page: Some(10),
|
||||||
|
search: None,
|
||||||
|
sort_by: None,
|
||||||
|
order: None,
|
||||||
|
filter: None,
|
||||||
|
filter_by: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoParams)]
|
||||||
|
pub struct MetaResponseDto {
|
||||||
|
pub page: Option<u64>,
|
||||||
|
pub per_page: Option<u64>,
|
||||||
|
pub total: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ResponseSuccessDto<T: Serialize> {
|
||||||
|
pub data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ResponseListSuccessDto<T: Serialize> {
|
||||||
|
pub data: T,
|
||||||
|
pub meta: Option<MetaResponseDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SurrealWsClient = Surreal<Any>;
|
||||||
|
pub type SurrealMemClient = Surreal<Db>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub surrealdb_ws: SurrealWsClient,
|
||||||
|
pub surrealdb_mem: SurrealMemClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct CountResult {
|
||||||
|
pub count: u64,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pub mod error {
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::response::Response;
|
||||||
|
use axum::Json;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("database error")]
|
||||||
|
Db,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(self.to_string())).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<surrealdb::Error> for Error {
|
||||||
|
fn from(error: surrealdb::Error) -> Self {
|
||||||
|
eprintln!("{error}");
|
||||||
|
Self::Db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod common_dto;
|
pub mod common_dto;
|
||||||
|
pub mod error_dto;
|
||||||
pub use common_dto::*;
|
pub use common_dto::*;
|
||||||
|
pub use error_dto::*;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-gacha"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-iam ={ version = "0.1.0", path = "../imphnen-iam" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
|
rand_distr.workspace = true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use imphnen_entities::*;
|
||||||
|
use imphnen_libs::*;
|
||||||
|
use imphnen_utils::*;
|
||||||
|
|
||||||
|
pub mod v1;
|
||||||
|
|
||||||
|
pub use imphnen_entities::*;
|
||||||
|
pub use imphnen_libs::*;
|
||||||
|
pub use imphnen_utils::*;
|
||||||
|
pub use v1::*;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use axum::Extension;
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::{Json, extract::Path};
|
||||||
|
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||||
|
use imphnen_libs::{AppState, MessageResponseDto, ResponseSuccessDto};
|
||||||
|
|
||||||
|
use super::{GachaClaimItemDto, GachaClaimRequestDto, GachaClaimService};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/gacha/claims/detail/{id}",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
params(("id" = String, Path, description = "Gacha Claim ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get Gacha Claim by ID", body = ResponseSuccessDto<GachaClaimItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn get_detail_gacha_claim(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::CreateGachaClaims],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaClaimService::get_gacha_claim_by_id(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/gacha/claims/create",
|
||||||
|
request_body = GachaClaimRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new gacha claim", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_gacha_claim(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<GachaClaimRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::CreateGachaClaims],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaClaimService::create_gacha_claim(&state, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::{GachaItemDto, GachaItemSchema};
|
||||||
|
use imphnen_iam::{UsersDetailItemDto, UsersDetailQueryDto};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct GachaClaimRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "User ID must not be empty"))]
|
||||||
|
pub user_id: String,
|
||||||
|
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
||||||
|
pub item_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GachaClaimItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user: UsersDetailItemDto,
|
||||||
|
pub item: GachaItemDto,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaClaimQueryDto {
|
||||||
|
pub id: Thing,
|
||||||
|
pub user: UsersDetailQueryDto,
|
||||||
|
pub item: GachaItemSchema,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GachaClaimItemDto {
|
||||||
|
pub fn from(dto: &GachaClaimQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id.id.to_raw(),
|
||||||
|
user: UsersDetailItemDto::from(&dto.user),
|
||||||
|
item: GachaItemDto::from(dto.item.clone()),
|
||||||
|
is_deleted: dto.is_deleted,
|
||||||
|
created_at: dto.created_at.clone(),
|
||||||
|
updated_at: dto.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use super::{GachaClaimQueryDto, GachaClaimSchema};
|
||||||
|
use crate::{AppState, ResourceEnum};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use imphnen_iam::DetailQueryBuilder;
|
||||||
|
|
||||||
|
pub struct GachaClaimRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GachaClaimRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_gacha_claim_by_id(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
) -> Result<GachaClaimQueryDto> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string())
|
||||||
|
.with_id(id.clone())
|
||||||
|
.with_select_fields(vec!["*"])
|
||||||
|
.with_fetch("item")
|
||||||
|
.with_fetch("user");
|
||||||
|
let sql = builder.build();
|
||||||
|
let result: Option<GachaClaimQueryDto> =
|
||||||
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
|
match result {
|
||||||
|
Some(claim) if !claim.is_deleted => Ok(claim),
|
||||||
|
_ => bail!("Gacha Claim not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_create_gacha_claim(
|
||||||
|
&self,
|
||||||
|
data: GachaClaimSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<GachaClaimSchema> = db
|
||||||
|
.create(ResourceEnum::GachaClaims.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success create Gacha Claim".into()),
|
||||||
|
None => bail!("Failed to create Gacha Claim"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use crate::{GachaRollQueryDto, ResourceEnum, make_thing};
|
||||||
|
use imphnen_iam::get_iso_date;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{Uuid, sql::Thing};
|
||||||
|
|
||||||
|
use super::GachaClaimRequestDto;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaClaimSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub user: Thing,
|
||||||
|
pub item: Thing,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GachaClaimSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
GachaClaimSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaClaims.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
user: make_thing(
|
||||||
|
&ResourceEnum::Users.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
item: make_thing(
|
||||||
|
&ResourceEnum::GachaItems.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GachaClaimSchema {
|
||||||
|
pub fn from(dto: GachaClaimRequestDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaClaims.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
user: make_thing(&ResourceEnum::Users.to_string(), &dto.user_id),
|
||||||
|
item: make_thing(&ResourceEnum::GachaItems.to_string(), &dto.item_id),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roll(roll: GachaRollQueryDto, user_id: Thing) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaClaims.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
user: user_id,
|
||||||
|
item: roll.item.id.clone(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::{
|
||||||
|
AppState, GachaClaimItemDto, GachaClaimRepository, GachaClaimRequestDto,
|
||||||
|
GachaClaimSchema, ResponseSuccessDto, common_response, success_response,
|
||||||
|
validate_request,
|
||||||
|
};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::Response;
|
||||||
|
|
||||||
|
pub struct GachaClaimService;
|
||||||
|
|
||||||
|
impl GachaClaimService {
|
||||||
|
pub async fn get_gacha_claim_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = GachaClaimRepository::new(state);
|
||||||
|
match repo.query_gacha_claim_by_id(id).await {
|
||||||
|
Ok(claim) => success_response(ResponseSuccessDto {
|
||||||
|
data: GachaClaimItemDto::from(&claim),
|
||||||
|
}),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_gacha_claim(
|
||||||
|
state: &AppState,
|
||||||
|
payload: GachaClaimRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = GachaClaimRepository::new(state);
|
||||||
|
let schema = GachaClaimSchema::from(payload);
|
||||||
|
match repo.query_create_gacha_claim(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod gacha_claims_controller;
|
||||||
|
pub mod gacha_claims_dto;
|
||||||
|
pub mod gacha_claims_repository;
|
||||||
|
pub mod gacha_claims_schema;
|
||||||
|
pub mod gacha_claims_service;
|
||||||
|
|
||||||
|
pub use gacha_claims_controller::*;
|
||||||
|
pub use gacha_claims_dto::*;
|
||||||
|
pub use gacha_claims_repository::*;
|
||||||
|
pub use gacha_claims_schema::*;
|
||||||
|
pub use gacha_claims_service::*;
|
||||||
|
|
||||||
|
pub fn gacha_claim_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/create", post(post_create_gacha_claim))
|
||||||
|
.route("/detail/{id}", get(get_detail_gacha_claim))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GachaCreditRequestDto {
|
||||||
|
pub user_id: String,
|
||||||
|
pub amount: i32,
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use super::{GachaCreditRequestDto, GachaCreditSchema};
|
||||||
|
use crate::{AppState, ResourceEnum};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use imphnen_iam::make_thing;
|
||||||
|
use surrealdb::Uuid;
|
||||||
|
|
||||||
|
pub struct GachaCreditRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GachaCreditRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_by_user_id(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
) -> Result<Option<GachaCreditSchema>> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT * FROM {} WHERE user = {}:⟨$user_id⟩ AND is_deleted = false LIMIT 1",
|
||||||
|
ResourceEnum::GachaCredits.to_string(),
|
||||||
|
ResourceEnum::Users.to_string()
|
||||||
|
);
|
||||||
|
let result: Vec<GachaCreditSchema> =
|
||||||
|
db.query(sql).bind(("user_id", user_id)).await?.take(0)?;
|
||||||
|
Ok(result.into_iter().next())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_consume_credit(&self, user_id: String) -> Result<()> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let credit_opt = self.query_by_user_id(user_id).await?;
|
||||||
|
let Some(mut credit) = credit_opt else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if credit.available_rolls <= 0 {
|
||||||
|
bail!("No extra roll credits remaining");
|
||||||
|
}
|
||||||
|
credit.available_rolls -= 1;
|
||||||
|
let _: Option<GachaCreditSchema> = db
|
||||||
|
.update((
|
||||||
|
&ResourceEnum::GachaCredits.to_string(),
|
||||||
|
credit.id.id.to_raw(),
|
||||||
|
))
|
||||||
|
.merge(credit)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_add_credit(
|
||||||
|
&self,
|
||||||
|
payload: GachaCreditRequestDto,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
|
||||||
|
credit.available_rolls += payload.amount;
|
||||||
|
let _: Option<GachaCreditSchema> = db
|
||||||
|
.update((
|
||||||
|
&ResourceEnum::GachaCredits.to_string(),
|
||||||
|
credit.id.id.to_raw(),
|
||||||
|
))
|
||||||
|
.merge(credit)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let data = GachaCreditSchema::from(&GachaCreditSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaCredits.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id),
|
||||||
|
available_rolls: payload.amount,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let _: Option<GachaCreditSchema> = db
|
||||||
|
.create(&ResourceEnum::GachaCredits.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use imphnen_iam::get_iso_date;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GachaCreditSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub user: Thing,
|
||||||
|
pub available_rolls: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GachaCreditSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
GachaCreditSchema {
|
||||||
|
id: Thing::from(("app_gacha_credits", "uuid")),
|
||||||
|
user: Thing::from(("app_users", "uuid")),
|
||||||
|
available_rolls: 0,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GachaCreditSchema {
|
||||||
|
pub fn from(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
id: self.id.clone(),
|
||||||
|
user: self.user.clone(),
|
||||||
|
available_rolls: self.available_rolls,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod gacha_credits_dto;
|
||||||
|
pub mod gacha_credits_repository;
|
||||||
|
pub mod gacha_credits_schema;
|
||||||
|
|
||||||
|
pub use gacha_credits_dto::*;
|
||||||
|
pub use gacha_credits_repository::*;
|
||||||
|
pub use gacha_credits_schema::*;
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use crate::{
|
||||||
|
AppState, GachaItemDto, GachaItemRequestDto, GachaItemService, MessageResponseDto,
|
||||||
|
MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
Extension, Json,
|
||||||
|
extract::{Path, Query},
|
||||||
|
http::HeaderMap,
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/gacha/items",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
("filter" = Option<String>, Query, description = "Filter value"),
|
||||||
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get gacha item list", body = ResponseListSuccessDto<Vec<GachaItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn get_gacha_item_list(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(meta): Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadListGachaItems],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaItemService::get_gacha_item_list(&state, meta).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/gacha/items/detail/{id}",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
params(("id" = String, Path, description = "Gacha Item ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get gacha item by ID", body = ResponseSuccessDto<GachaItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn get_gacha_item_by_id(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadDetailGachaItems],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaItemService::get_gacha_item_by_id(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/gacha/items/create",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
request_body = GachaItemRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create gacha item", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_gacha_item(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<GachaItemRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::CreateGachaItems],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaItemService::create_gacha_item(&state, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/gacha/items/update/{id}",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
request_body = GachaItemRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update gacha item", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn put_update_gacha_item(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<GachaItemRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::UpdateGachaItems],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaItemService::update_gacha_item(&state, payload, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/gacha/items/delete/{id}",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Delete gacha item", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn delete_gacha_item(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::DeleteGachaItems],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaItemService::delete_gacha_item(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use super::GachaItemSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct GachaItemRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Item name must not be empty"))]
|
||||||
|
pub name: String,
|
||||||
|
#[validate(length(min = 1, message = "Image URL must not be empty"))]
|
||||||
|
pub image_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GachaItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GachaItemDto {
|
||||||
|
pub fn from(dto: GachaItemSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id.id.to_raw(),
|
||||||
|
name: dto.name.clone(),
|
||||||
|
is_deleted: dto.is_deleted,
|
||||||
|
created_at: dto.created_at.clone(),
|
||||||
|
updated_at: dto.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use super::GachaItemSchema;
|
||||||
|
use crate::{
|
||||||
|
AppState, GachaItemDto, MetaRequestDto, ResourceEnum, ResponseListSuccessDto,
|
||||||
|
get_id, make_thing,
|
||||||
|
};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use imphnen_iam::QueryListBuilder;
|
||||||
|
|
||||||
|
pub struct GachaItemRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GachaItemRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_gacha_item_list(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
|
||||||
|
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
||||||
|
QueryListBuilder::new(
|
||||||
|
&self.state.surrealdb_ws,
|
||||||
|
&ResourceEnum::GachaItems.to_string(),
|
||||||
|
&meta,
|
||||||
|
)
|
||||||
|
.with_condition("is_deleted = false")
|
||||||
|
.search_field("name")
|
||||||
|
.select_fields(vec!["*"])
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
let data = raw_result
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(GachaItemDto::from)
|
||||||
|
.collect();
|
||||||
|
Ok(ResponseListSuccessDto {
|
||||||
|
data,
|
||||||
|
meta: raw_result.meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let result: Option<GachaItemSchema> = db
|
||||||
|
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
||||||
|
.await?;
|
||||||
|
match result {
|
||||||
|
Some(item) if !item.is_deleted => Ok(item),
|
||||||
|
_ => bail!("Gacha Item not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_create_gacha_item(
|
||||||
|
&self,
|
||||||
|
data: GachaItemSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<GachaItemSchema> = db
|
||||||
|
.create(ResourceEnum::GachaItems.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success create Gacha Item".into()),
|
||||||
|
None => bail!("Failed to create Gacha Item"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_update_gacha_item(
|
||||||
|
&self,
|
||||||
|
data: GachaItemSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(&data.id)?;
|
||||||
|
let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?;
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Gacha Item already deleted");
|
||||||
|
}
|
||||||
|
let merged = GachaItemSchema {
|
||||||
|
created_at: existing.created_at,
|
||||||
|
..data.clone()
|
||||||
|
};
|
||||||
|
let record: Option<GachaItemSchema> =
|
||||||
|
db.update(record_key).merge(merged).await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update Gacha Item".into()),
|
||||||
|
None => bail!("Failed to update Gacha Item"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
|
||||||
|
let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?;
|
||||||
|
if item.is_deleted {
|
||||||
|
bail!("Gacha Item already deleted");
|
||||||
|
}
|
||||||
|
let record_key = get_id(&item.id)?;
|
||||||
|
let record: Option<GachaItemSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete Gacha Item".into()),
|
||||||
|
None => bail!("Failed to delete Gacha Item"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
use crate::{ResourceEnum, make_thing};
|
||||||
|
use imphnen_iam::get_iso_date;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{Uuid, sql::Thing};
|
||||||
|
|
||||||
|
use super::GachaItemRequestDto;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaItemSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub image_url: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GachaItemSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
GachaItemSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaItems.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
name: String::new(),
|
||||||
|
image_url: String::new(),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GachaItemSchema {
|
||||||
|
pub fn from(dto: GachaItemRequestDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaItems.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
name: dto.name,
|
||||||
|
image_url: dto.image_url,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use crate::{
|
||||||
|
AppState, GachaItemDto, GachaItemRepository, GachaItemRequestDto, GachaItemSchema,
|
||||||
|
MetaRequestDto, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
common_response, make_thing, success_list_response, success_response,
|
||||||
|
validate_request,
|
||||||
|
};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::Response;
|
||||||
|
|
||||||
|
pub struct GachaItemService;
|
||||||
|
|
||||||
|
impl GachaItemService {
|
||||||
|
pub async fn get_gacha_item_list(
|
||||||
|
state: &AppState,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = GachaItemRepository::new(state);
|
||||||
|
match repo.query_gacha_item_list(meta).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let response = ResponseListSuccessDto {
|
||||||
|
data: data.data,
|
||||||
|
meta: data.meta,
|
||||||
|
};
|
||||||
|
success_list_response(response)
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_gacha_item_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = GachaItemRepository::new(state);
|
||||||
|
match repo.query_gacha_item_by_id(id).await {
|
||||||
|
Ok(item) => success_response(ResponseSuccessDto {
|
||||||
|
data: GachaItemDto::from(item),
|
||||||
|
}),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_gacha_item(
|
||||||
|
state: &AppState,
|
||||||
|
payload: GachaItemRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = GachaItemRepository::new(state);
|
||||||
|
let schema = GachaItemSchema::from(payload);
|
||||||
|
match repo.query_create_gacha_item(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_gacha_item(
|
||||||
|
state: &AppState,
|
||||||
|
payload: GachaItemRequestDto,
|
||||||
|
id: String,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = GachaItemRepository::new(state);
|
||||||
|
let schema = GachaItemSchema {
|
||||||
|
id: make_thing(&ResourceEnum::GachaItems.to_string(), &id),
|
||||||
|
name: payload.name,
|
||||||
|
image_url: payload.image_url,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match repo.query_update_gacha_item(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => {
|
||||||
|
if e.to_string().contains("not found") {
|
||||||
|
common_response(StatusCode::NOT_FOUND, "Gacha Item not found")
|
||||||
|
} else {
|
||||||
|
common_response(StatusCode::BAD_REQUEST, &e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_gacha_item(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = GachaItemRepository::new(state);
|
||||||
|
match repo.query_delete_gacha_item(id).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => {
|
||||||
|
if e.to_string().contains("not found") {
|
||||||
|
common_response(StatusCode::NOT_FOUND, "Gacha Item not found")
|
||||||
|
} else {
|
||||||
|
common_response(StatusCode::BAD_REQUEST, &e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod gacha_items_controller;
|
||||||
|
pub mod gacha_items_dto;
|
||||||
|
pub mod gacha_items_repository;
|
||||||
|
pub mod gacha_items_schema;
|
||||||
|
pub mod gacha_items_service;
|
||||||
|
|
||||||
|
pub use gacha_items_controller::*;
|
||||||
|
pub use gacha_items_dto::*;
|
||||||
|
pub use gacha_items_repository::*;
|
||||||
|
pub use gacha_items_schema::*;
|
||||||
|
pub use gacha_items_service::*;
|
||||||
|
|
||||||
|
pub fn gacha_item_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_gacha_item_list))
|
||||||
|
.route("/create", post(post_create_gacha_item))
|
||||||
|
.route("/detail/{id}", get(get_gacha_item_by_id))
|
||||||
|
.route("/update/{id}", put(put_update_gacha_item))
|
||||||
|
.route("/delete/{id}", delete(delete_gacha_item))
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use crate::{
|
||||||
|
AppState, GachaRollItemDto, GachaRollRequestDto, GachaRollService,
|
||||||
|
MessageResponseDto, ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
Extension, Json, extract::Path, http::HeaderMap, response::IntoResponse,
|
||||||
|
};
|
||||||
|
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/gacha/rolls/detail/{id}",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
params(("id" = String, Path, description = "Gacha Roll ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get Gacha Roll by ID", body = ResponseSuccessDto<GachaRollItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn get_detail_gacha_roll(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadDetailGachaRolls],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaRollService::get_gacha_roll_by_id(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/gacha/rolls/create",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
request_body = GachaRollRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new gacha roll", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_gacha_roll(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<GachaRollRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::CreateGachaRolls],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaRollService::create_gacha_roll(&state, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/gacha/rolls/execute",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Execute and get 1 gacha result", body = ResponseSuccessDto<GachaRollItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Gacha"
|
||||||
|
)]
|
||||||
|
pub async fn post_execute_gacha_roll(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ExecuteGachaRolls],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => GachaRollService::execute_roll_once(headers, &state).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::{GachaItemDto, GachaItemSchema};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct GachaRollRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
||||||
|
pub item_id: String,
|
||||||
|
pub weight: f32,
|
||||||
|
#[validate(range(min = 1, message = "Quantity must be at least 1"))]
|
||||||
|
pub quantity: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GachaRollItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub item: GachaItemDto,
|
||||||
|
pub weight: f32,
|
||||||
|
pub quantity: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GachaRollItemDto {
|
||||||
|
pub fn from(dto: &GachaRollQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id.id.to_raw(),
|
||||||
|
item: GachaItemDto::from(dto.item.clone()),
|
||||||
|
weight: dto.weight.clone(),
|
||||||
|
quantity: dto.quantity,
|
||||||
|
is_deleted: dto.is_deleted,
|
||||||
|
created_at: dto.created_at.clone(),
|
||||||
|
updated_at: dto.updated_at.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaRollQueryDto {
|
||||||
|
pub id: Thing,
|
||||||
|
pub item: GachaItemSchema,
|
||||||
|
pub weight: f32,
|
||||||
|
pub quantity: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use super::GachaRollQueryDto;
|
||||||
|
use super::GachaRollSchema;
|
||||||
|
use crate::{AppState, DetailQueryBuilder, ResourceEnum};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use imphnen_iam::ListQueryBuilder;
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand::rng;
|
||||||
|
use rand_distr::weighted::WeightedIndex;
|
||||||
|
|
||||||
|
pub struct GachaRollRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GachaRollRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_gacha_roll_by_id(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
) -> Result<GachaRollQueryDto> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let builder = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string())
|
||||||
|
.with_id(id.clone())
|
||||||
|
.with_select_fields(vec!["*"])
|
||||||
|
.with_fetch("item");
|
||||||
|
let sql = builder.build();
|
||||||
|
let result: Option<GachaRollQueryDto> =
|
||||||
|
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||||
|
match result {
|
||||||
|
Some(roll) if !roll.is_deleted => Ok(roll),
|
||||||
|
_ => bail!("Gacha Roll not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_create_gacha_roll(
|
||||||
|
&self,
|
||||||
|
data: GachaRollSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<GachaRollSchema> = db
|
||||||
|
.create(ResourceEnum::GachaRolls.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success create Gacha Roll".into()),
|
||||||
|
None => bail!("Failed to create Gacha Roll"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_all_active_rolls(&self) -> Result<Vec<GachaRollQueryDto>> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let builder = ListQueryBuilder::new(ResourceEnum::GachaRolls.to_string())
|
||||||
|
.with_select_fields(vec!["*"])
|
||||||
|
.with_fetch(Some(vec!["item"]));
|
||||||
|
let sql = builder.build();
|
||||||
|
let result: Vec<GachaRollQueryDto> = db.query(sql).await?.take(0)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roll_once(rolls: &[GachaRollQueryDto]) -> Option<GachaRollQueryDto> {
|
||||||
|
let filtered: Vec<_> = rolls
|
||||||
|
.iter()
|
||||||
|
.filter(|r| !r.is_deleted && r.quantity > 0)
|
||||||
|
.collect();
|
||||||
|
let weights: Vec<f32> = filtered
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.weight * r.quantity as f32)
|
||||||
|
.collect();
|
||||||
|
if weights.iter().all(|&w| w <= 0.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dist = WeightedIndex::new(&weights).ok()?;
|
||||||
|
let mut rng = rng();
|
||||||
|
let index = dist.sample(&mut rng);
|
||||||
|
Some(filtered[index].clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
use crate::{ResourceEnum, make_thing};
|
||||||
|
use imphnen_iam::get_iso_date;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{Uuid, sql::Thing};
|
||||||
|
|
||||||
|
use super::GachaRollRequestDto;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaRollSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub item: Thing,
|
||||||
|
pub weight: f32,
|
||||||
|
pub quantity: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GachaRollSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
GachaRollSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaRolls.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
item: make_thing(
|
||||||
|
&ResourceEnum::GachaItems.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
weight: 0.0,
|
||||||
|
quantity: 0,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Some(get_iso_date()),
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GachaRollSchema {
|
||||||
|
pub fn create(dto: GachaRollRequestDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::GachaRolls.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
item: make_thing(&ResourceEnum::GachaItems.to_string(), &dto.item_id),
|
||||||
|
weight: dto.weight,
|
||||||
|
quantity: dto.quantity,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::{
|
||||||
|
AppState, GachaClaimRepository, GachaClaimSchema, GachaRollItemDto,
|
||||||
|
GachaRollRepository, GachaRollRequestDto, GachaRollSchema, ResponseSuccessDto,
|
||||||
|
common_response, success_response, validate_request,
|
||||||
|
};
|
||||||
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
|
use axum::response::Response;
|
||||||
|
use imphnen_iam::{UsersRepository, extract_email};
|
||||||
|
|
||||||
|
pub struct GachaRollService;
|
||||||
|
|
||||||
|
impl GachaRollService {
|
||||||
|
pub async fn get_gacha_roll_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = GachaRollRepository::new(state);
|
||||||
|
match repo.query_gacha_roll_by_id(id).await {
|
||||||
|
Ok(roll) => success_response(ResponseSuccessDto {
|
||||||
|
data: GachaRollItemDto::from(&roll),
|
||||||
|
}),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_gacha_roll(
|
||||||
|
state: &AppState,
|
||||||
|
payload: GachaRollRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let schema = GachaRollSchema::create(payload);
|
||||||
|
let repo = GachaRollRepository::new(state);
|
||||||
|
match repo.query_create_gacha_roll(schema).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute_roll_once(headers: HeaderMap, state: &AppState) -> Response {
|
||||||
|
let repo = GachaRollRepository::new(state);
|
||||||
|
let repo_claim = GachaClaimRepository::new(state);
|
||||||
|
let repo_user = UsersRepository::new(state);
|
||||||
|
let Some(email) = extract_email(&headers) else {
|
||||||
|
return common_response(StatusCode::UNAUTHORIZED, "Unauthorized");
|
||||||
|
};
|
||||||
|
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
||||||
|
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||||
|
};
|
||||||
|
match repo.query_all_active_rolls().await {
|
||||||
|
Ok(rolls) => match GachaRollRepository::roll_once(&rolls) {
|
||||||
|
Some(roll) => {
|
||||||
|
let claim = GachaClaimSchema::roll(roll.clone(), user.id);
|
||||||
|
match repo_claim.query_create_gacha_claim(claim).await {
|
||||||
|
Ok(_) => success_response(ResponseSuccessDto {
|
||||||
|
data: GachaRollItemDto::from(&roll),
|
||||||
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => common_response(StatusCode::NOT_FOUND, "No rollable item available"),
|
||||||
|
},
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
pub mod gacha_rolls_controller;
|
||||||
|
pub mod gacha_rolls_dto;
|
||||||
|
pub mod gacha_rolls_repository;
|
||||||
|
pub mod gacha_rolls_schema;
|
||||||
|
pub mod gacha_rolls_service;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
pub use gacha_rolls_controller::*;
|
||||||
|
pub use gacha_rolls_dto::*;
|
||||||
|
pub use gacha_rolls_repository::*;
|
||||||
|
pub use gacha_rolls_schema::*;
|
||||||
|
pub use gacha_rolls_service::*;
|
||||||
|
|
||||||
|
pub fn gacha_roll_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/create", post(post_create_gacha_roll))
|
||||||
|
.route("/execute", post(post_execute_gacha_roll))
|
||||||
|
.route("/detail/{id}", get(get_detail_gacha_roll))
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
pub mod gacha_claims;
|
||||||
|
pub mod gacha_credits;
|
||||||
|
pub mod gacha_items;
|
||||||
|
pub mod gacha_rolls;
|
||||||
|
|
||||||
|
pub use gacha_claims::*;
|
||||||
|
pub use gacha_credits::*;
|
||||||
|
pub use gacha_items::*;
|
||||||
|
pub use gacha_rolls::*;
|
||||||
|
|
||||||
|
pub fn gacha_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.nest("/gacha/claims", gacha_claim_router())
|
||||||
|
.nest("/gacha/items", gacha_item_router())
|
||||||
|
.nest("/gacha/rolls", gacha_roll_router())
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-gateway"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
imphnen-gacha = { version = "0.1.0", path = "../imphnen-gacha" }
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
imphnen-middleware = { version = "0.1.0", path = "../imphnen-middleware" }
|
||||||
|
imphnen-cms = { version = "0.1.0", path = "../imphnen-cms" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
use imphnen_iam::{
|
||||||
|
auth, permissions, roles, users, AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, UsersListItemDto, UsersUpdateRequestDto
|
||||||
|
};
|
||||||
|
use utoipa::{
|
||||||
|
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||||
|
Modify, OpenApi,
|
||||||
|
};
|
||||||
|
use imphnen_gacha::{gacha_claims, gacha_items, gacha_rolls, GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto, GachaRollItemDto, GachaRollRequestDto};
|
||||||
|
use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsListItemDto}, testimonials_controller, testimonials_dto::{TestimonialsCreateRequestDto, TestimonialsDetailItemDto, TestimonialsListItemDto, TestimonialsUpdateRequestDto}};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
auth::auth_controller::post_login,
|
||||||
|
auth::auth_controller::post_register,
|
||||||
|
auth::auth_controller::post_verify_email,
|
||||||
|
auth::auth_controller::post_resend_otp,
|
||||||
|
auth::auth_controller::post_refresh_token,
|
||||||
|
auth::auth_controller::post_forgot_password,
|
||||||
|
auth::auth_controller::post_new_password,
|
||||||
|
users::users_controller::post_create_user,
|
||||||
|
users::users_controller::put_update_user,
|
||||||
|
users::users_controller::put_update_user_me,
|
||||||
|
users::users_controller::patch_user_active_status,
|
||||||
|
users::users_controller::delete_user,
|
||||||
|
users::users_controller::get_user_by_id,
|
||||||
|
users::users_controller::get_user_me,
|
||||||
|
users::users_controller::get_user_list,
|
||||||
|
roles::roles_controller::get_role_list,
|
||||||
|
roles::roles_controller::get_role_by_id,
|
||||||
|
roles::roles_controller::post_create_role,
|
||||||
|
roles::roles_controller::put_update_role,
|
||||||
|
roles::roles_controller::delete_role,
|
||||||
|
permissions::permissions_controller::get_permission_list,
|
||||||
|
permissions::permissions_controller::get_permission_by_id,
|
||||||
|
permissions::permissions_controller::post_create_permission,
|
||||||
|
permissions::permissions_controller::put_update_permission,
|
||||||
|
permissions::permissions_controller::delete_permission,
|
||||||
|
gacha_claims::get_detail_gacha_claim,
|
||||||
|
gacha_claims::post_create_gacha_claim,
|
||||||
|
gacha_items::get_gacha_item_list,
|
||||||
|
gacha_items::get_gacha_item_by_id,
|
||||||
|
gacha_items::post_create_gacha_item,
|
||||||
|
gacha_items::put_update_gacha_item,
|
||||||
|
gacha_items::delete_gacha_item,
|
||||||
|
gacha_rolls::get_detail_gacha_roll,
|
||||||
|
gacha_rolls::post_create_gacha_roll,
|
||||||
|
gacha_rolls::post_execute_gacha_roll,
|
||||||
|
events_controller::get_event_list,
|
||||||
|
events_controller::get_event_by_id,
|
||||||
|
events_controller::post_create_event,
|
||||||
|
events_controller::patch_update_event,
|
||||||
|
events_controller::delete_event,
|
||||||
|
testimonials_controller::get_testimonial_list,
|
||||||
|
testimonials_controller::get_testimonial_by_id,
|
||||||
|
testimonials_controller::post_create_testimonial,
|
||||||
|
testimonials_controller::patch_update_testimonial,
|
||||||
|
testimonials_controller::delete_testimonial,
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(
|
||||||
|
MetaRequestDto,
|
||||||
|
MetaResponseDto,
|
||||||
|
MessageResponseDto,
|
||||||
|
AuthLoginRequestDto,
|
||||||
|
AuthLoginResponsetDto,
|
||||||
|
AuthVerifyEmailRequestDto,
|
||||||
|
AuthResendOtpRequestDto,
|
||||||
|
AuthNewPasswordRequestDto,
|
||||||
|
AuthRefreshTokenRequestDto,
|
||||||
|
ResponseSuccessDto<TokenDto>,
|
||||||
|
RolesListItemDto,
|
||||||
|
RolesRequestCreateDto,
|
||||||
|
RolesRequestUpdateDto,
|
||||||
|
PermissionsRequestDto,
|
||||||
|
PermissionsItemDto,
|
||||||
|
UsersDetailItemDto,
|
||||||
|
UsersListItemDto,
|
||||||
|
UsersUpdateRequestDto,
|
||||||
|
UsersCreateRequestDto,
|
||||||
|
GachaClaimItemDto,
|
||||||
|
GachaClaimRequestDto,
|
||||||
|
GachaItemDto,
|
||||||
|
GachaItemRequestDto,
|
||||||
|
GachaRollItemDto,
|
||||||
|
GachaRollRequestDto,
|
||||||
|
ResponseListSuccessDto<Vec<GachaItemDto>>,
|
||||||
|
ResponseSuccessDto<GachaRollItemDto>,
|
||||||
|
ResponseSuccessDto<GachaItemDto>,
|
||||||
|
ResponseSuccessDto<GachaClaimItemDto>,
|
||||||
|
ResponseSuccessDto<AuthLoginResponsetDto>,
|
||||||
|
ResponseListSuccessDto<Vec<RolesListItemDto>>,
|
||||||
|
ResponseSuccessDto<RolesDetailItemDto>,
|
||||||
|
ResponseListSuccessDto<Vec<UsersListItemDto>>,
|
||||||
|
ResponseSuccessDto<UsersDetailItemDto>,
|
||||||
|
ResponseListSuccessDto<Vec<PermissionsItemDto>>,
|
||||||
|
ResponseSuccessDto<PermissionsItemDto>,
|
||||||
|
ResponseListSuccessDto<Vec<EventsListItemDto>>,
|
||||||
|
ResponseSuccessDto<EventsDetailItemDto>,
|
||||||
|
ResponseListSuccessDto<Vec<TestimonialsListItemDto>>,
|
||||||
|
ResponseSuccessDto<TestimonialsDetailItemDto>,
|
||||||
|
TestimonialsCreateRequestDto,
|
||||||
|
TestimonialsUpdateRequestDto,
|
||||||
|
MessageResponseDto,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
info(
|
||||||
|
title = "IMPHNEN Backend Service",
|
||||||
|
description = "IMPHNEN Backend Service for Provide Gacha, Dimentorin and Backoffice Web App",
|
||||||
|
version = "0.1.0",
|
||||||
|
contact(
|
||||||
|
name = "Maulana Sodiqin",
|
||||||
|
url = ""
|
||||||
|
),
|
||||||
|
license(
|
||||||
|
name = "MIT",
|
||||||
|
url = "https://opensource.org/licenses/MIT"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
modifiers(&SecurityAddon),
|
||||||
|
tags(
|
||||||
|
(name = "Authentication", description = "List of Authentication Endpoints"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
|
||||||
|
pub struct ApiDoc;
|
||||||
|
|
||||||
|
struct SecurityAddon;
|
||||||
|
|
||||||
|
impl Modify for SecurityAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
if let Some(components) = openapi.components.as_mut() {
|
||||||
|
components.add_security_scheme(
|
||||||
|
"Bearer",
|
||||||
|
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn docs_router() -> utoipa::openapi::OpenApi {
|
||||||
|
ApiDoc::openapi()
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
use axum::{
|
||||||
|
Extension, Router, middleware::from_fn, response::Redirect, routing::get,
|
||||||
|
};
|
||||||
|
use imphnen_cms::{
|
||||||
|
events_protected_routes, events_public_routes, testimonials_protected_routes,
|
||||||
|
testimonials_public_routes,
|
||||||
|
};
|
||||||
|
use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient};
|
||||||
|
use imphnen_gacha::gacha_router;
|
||||||
|
use imphnen_iam::{iam_protected_routes, iam_public_routes};
|
||||||
|
use imphnen_middleware::{auth_middleware, cors_middleware};
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
|
pub mod docs;
|
||||||
|
pub use docs::*;
|
||||||
|
|
||||||
|
pub async fn gateway_service(
|
||||||
|
surrealdb_ws: SurrealWsClient,
|
||||||
|
surrealdb_mem: SurrealMemClient,
|
||||||
|
) -> Router {
|
||||||
|
let state = AppState {
|
||||||
|
surrealdb_ws,
|
||||||
|
surrealdb_mem,
|
||||||
|
};
|
||||||
|
|
||||||
|
let public_routes = Router::new()
|
||||||
|
.merge(iam_public_routes())
|
||||||
|
.merge(testimonials_public_routes())
|
||||||
|
.merge(events_public_routes());
|
||||||
|
|
||||||
|
let protected_routes = Router::new()
|
||||||
|
.merge(iam_protected_routes())
|
||||||
|
.merge(events_protected_routes())
|
||||||
|
.merge(testimonials_protected_routes())
|
||||||
|
.merge(gacha_router())
|
||||||
|
.layer(from_fn(auth_middleware));
|
||||||
|
|
||||||
|
let routes = public_routes.merge(protected_routes);
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(Redirect::to("/docs")))
|
||||||
|
.nest("/v1", routes)
|
||||||
|
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
||||||
|
.layer(cors_middleware())
|
||||||
|
.layer(Extension(state))
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-iam"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use ::surrealdb::Uuid;
|
||||||
|
use imphnen_entities::*;
|
||||||
|
use imphnen_libs::*;
|
||||||
|
use imphnen_utils::*;
|
||||||
|
|
||||||
|
pub mod v1;
|
||||||
|
|
||||||
|
pub use imphnen_entities::*;
|
||||||
|
pub use imphnen_libs::*;
|
||||||
|
pub use imphnen_utils::*;
|
||||||
|
pub use v1::*;
|
||||||
|
|
||||||
|
pub fn create_test_user(
|
||||||
|
email: &str,
|
||||||
|
fullname: &str,
|
||||||
|
is_active: bool,
|
||||||
|
role_id: &str,
|
||||||
|
) -> UsersSchema {
|
||||||
|
UsersSchema {
|
||||||
|
id: make_thing("app_users", &Uuid::new_v4().to_string()),
|
||||||
|
email: email.to_string(),
|
||||||
|
fullname: format!("Randomize {} {}", fullname, rand::random::<u32>()),
|
||||||
|
password: hash_password("secret").unwrap(),
|
||||||
|
is_deleted: false,
|
||||||
|
avatar: None,
|
||||||
|
phone_number: "081234567890".to_string(),
|
||||||
|
is_active,
|
||||||
|
gender: None,
|
||||||
|
birthdate: None,
|
||||||
|
role: make_thing("app_roles", role_id),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
use super::{
|
||||||
|
AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto,
|
||||||
|
AuthResendOtpRequestDto, AuthService, AuthVerifyEmailRequestDto,
|
||||||
|
};
|
||||||
|
use crate::{v1::AuthLoginResponsetDto, AppState};
|
||||||
|
use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto};
|
||||||
|
use axum::{response::IntoResponse, Extension, Json};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/login",
|
||||||
|
request_body = AuthLoginRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Login successful", body = ResponseSuccessDto<AuthLoginResponsetDto>),
|
||||||
|
(status = 401, description = "Login failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_login(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthLoginRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_login(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/register",
|
||||||
|
request_body = AuthRegisterRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Register successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Register failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_register(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthRegisterRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_register(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/verify-email",
|
||||||
|
request_body = AuthVerifyEmailRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Verify email successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Verify email failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_verify_email(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthVerifyEmailRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_verify_email(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/send-otp",
|
||||||
|
request_body = AuthResendOtpRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Resend otp successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Resend otp failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_resend_otp(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthResendOtpRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_resend_otp(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/forgot",
|
||||||
|
request_body = AuthResendOtpRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Forgot password request successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Forgot password request failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_forgot_password(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthResendOtpRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_forgot_password(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/new-password",
|
||||||
|
request_body = AuthNewPasswordRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "New password request successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "New password request failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_new_password(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthNewPasswordRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_new_password(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/refresh",
|
||||||
|
request_body = AuthRefreshTokenRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Refresh token request successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Refresh token request failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_refresh_token(
|
||||||
|
Json(payload): Json<AuthRefreshTokenRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_refresh_token(payload).await
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
use crate::UsersDetailItemDto;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
fn validate_password_complexity(password: &str) -> Result<(), ValidationError> {
|
||||||
|
let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
|
||||||
|
let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
|
||||||
|
let has_digit = password.chars().any(|c| c.is_ascii_digit());
|
||||||
|
let has_special = password.chars().any(|c| "@$!%*?&".contains(c));
|
||||||
|
if has_uppercase && has_lowercase && has_digit && has_special {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("complexity"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthLoginRequestDto {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
|
email(message = "Email not valid")
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(min = 1, message = "Password cannot be empty"))]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AuthLoginResponsetDto {
|
||||||
|
pub token: TokenDto,
|
||||||
|
pub user: UsersDetailItemDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TokenDto {
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthRegisterRequestDto {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
|
email(message = "Email not valid")
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 8,
|
||||||
|
message = "Password must have at least 8 characters"
|
||||||
|
))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_password_complexity",
|
||||||
|
message = "Password must include uppercase, lowercase, number, and special character"
|
||||||
|
))]
|
||||||
|
pub password: String,
|
||||||
|
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||||
|
pub fullname: String,
|
||||||
|
#[validate(length(min = 1, message = "Student type is required"))]
|
||||||
|
pub phone_number: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthVerifyEmailRequestDto {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
|
email(message = "Email not valid")
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
pub otp: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthResendOtpRequestDto {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
|
email(message = "Email not valid")
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthRefreshTokenRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Refresh token cannot be empty"))]
|
||||||
|
pub refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthNewPasswordRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Token cannot be empty"))]
|
||||||
|
pub token: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 8,
|
||||||
|
message = "Password must have at least 8 characters"
|
||||||
|
))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_password_complexity",
|
||||||
|
message = "Password must include uppercase, lowercase, number, and special character"
|
||||||
|
))]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthSetNewPasswordRequestDto {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
|
email(message = "Email not valid")
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 8,
|
||||||
|
message = "Password must have at least 8 characters"
|
||||||
|
))]
|
||||||
|
#[validate(custom(
|
||||||
|
function = "validate_password_complexity",
|
||||||
|
message = "Password must include uppercase, lowercase, number, and special character"
|
||||||
|
))]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use super::AuthOtpSchema;
|
||||||
|
use crate::{AppState, ResourceEnum, UsersDetailQueryDto, make_thing};
|
||||||
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
|
||||||
|
pub struct AuthRepository<'a> {
|
||||||
|
pub state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AuthRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_store_user(&self, user: UsersDetailQueryDto) -> Result<String> {
|
||||||
|
if user.email.trim().is_empty() {
|
||||||
|
bail!("Email is required");
|
||||||
|
}
|
||||||
|
let table = ResourceEnum::UsersCache.to_string();
|
||||||
|
let user_id = user.email.clone();
|
||||||
|
let id = make_thing(&table, &user_id);
|
||||||
|
let _ = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.delete::<Option<UsersDetailQueryDto>>((table.clone(), user_id.clone()))
|
||||||
|
.await?;
|
||||||
|
let mut user_to_store = user.clone();
|
||||||
|
user_to_store.id = id.clone();
|
||||||
|
let record: Option<UsersDetailQueryDto> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.create((table, user_id))
|
||||||
|
.content(user_to_store)
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success store user data".to_string()),
|
||||||
|
None => bail!("Failed store user data"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_get_stored_user(
|
||||||
|
&self,
|
||||||
|
email: String,
|
||||||
|
) -> Result<UsersDetailQueryDto> {
|
||||||
|
let user: Option<UsersDetailQueryDto> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.select((ResourceEnum::UsersCache.to_string(), email))
|
||||||
|
.await?;
|
||||||
|
match user {
|
||||||
|
Some(u) => Ok(u),
|
||||||
|
None => bail!("No stored user data found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_stored_user(&self, email: String) -> Result<String> {
|
||||||
|
let record: Option<UsersDetailQueryDto> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.delete((ResourceEnum::UsersCache.to_string(), email))
|
||||||
|
.await?;
|
||||||
|
dbg!(record.clone());
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete stored user".to_string()),
|
||||||
|
None => bail!("Failed delete stored user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_get_stored_otp(&self, email: String) -> Result<u32> {
|
||||||
|
let table = ResourceEnum::OtpCache.to_string();
|
||||||
|
let key = (table.as_str(), email.as_str());
|
||||||
|
let result: Option<AuthOtpSchema> = self.state.surrealdb_mem.select(key).await?;
|
||||||
|
match result {
|
||||||
|
Some(data) => match Utc::now() > data.expires_at {
|
||||||
|
true => {
|
||||||
|
let _ = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.delete::<Option<AuthOtpSchema>>(key)
|
||||||
|
.await?;
|
||||||
|
Err(anyhow!("OTP expired"))
|
||||||
|
}
|
||||||
|
false => Ok(data.otp),
|
||||||
|
},
|
||||||
|
None => bail!("No stored OTP found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_store_otp(&self, email: String, otp: u32) -> Result<String> {
|
||||||
|
let expires_at = Utc::now() + Duration::seconds(300);
|
||||||
|
let table: String = ResourceEnum::OtpCache.to_string();
|
||||||
|
let record: Option<AuthOtpSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.create((table.as_str(), email.as_str()))
|
||||||
|
.content(AuthOtpSchema { otp, expires_at })
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success store otp".to_string()),
|
||||||
|
None => bail!("Failed store otp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_stored_otp(&self, email: String) -> Result<String> {
|
||||||
|
let record: Option<AuthOtpSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.delete((ResourceEnum::OtpCache.to_string(), email))
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete stored otp".to_string()),
|
||||||
|
None => bail!("Failed delete stored otp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthOtpSchema {
|
||||||
|
pub otp: u32,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
use super::{
|
||||||
|
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
|
||||||
|
AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthRepository,
|
||||||
|
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
AppState, Env, ResourceEnum, ResponseSuccessDto, RolesEnum, RolesRepository,
|
||||||
|
UsersDetailItemDto, UsersRepository, UsersSchema, common_response,
|
||||||
|
decode_refresh_token, encode_access_token, encode_refresh_token,
|
||||||
|
encode_reset_password_token, extract_email_token, generate_otp, get_iso_date,
|
||||||
|
hash_password, make_thing, send_email, success_response, validate_request,
|
||||||
|
verify_password,
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
use surrealdb::Uuid;
|
||||||
|
|
||||||
|
pub struct AuthService;
|
||||||
|
|
||||||
|
impl AuthService {
|
||||||
|
pub async fn mutation_login(
|
||||||
|
payload: AuthLoginRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_repo = UsersRepository::new(state);
|
||||||
|
let auth_repo = AuthRepository::new(state);
|
||||||
|
|
||||||
|
match user_repo.query_user_by_email(payload.email.clone()).await {
|
||||||
|
Ok(user) => {
|
||||||
|
let is_password_correct =
|
||||||
|
verify_password(&payload.password, &user.password).unwrap_or(false);
|
||||||
|
|
||||||
|
if !is_password_correct {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Email or password not correct",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.is_active {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Account not active, please verify your email",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let access_token = match encode_access_token(payload.email.clone()) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate access token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate refresh token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = ResponseSuccessDto {
|
||||||
|
data: AuthLoginResponsetDto {
|
||||||
|
user: UsersDetailItemDto::from(&user),
|
||||||
|
token: TokenDto {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(_err) = auth_repo.query_store_user(user).await {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User already login");
|
||||||
|
}
|
||||||
|
|
||||||
|
success_response(response)
|
||||||
|
}
|
||||||
|
Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_register(
|
||||||
|
payload: AuthRegisterRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let user_repo = UsersRepository::new(state);
|
||||||
|
let auth_repo = AuthRepository::new(state);
|
||||||
|
let role_repo = RolesRepository::new(state);
|
||||||
|
let role = match role_repo
|
||||||
|
.query_role_by_name(RolesEnum::User.to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(role) => role,
|
||||||
|
Err(_) => return common_response(StatusCode::BAD_REQUEST, "Role Not Found"),
|
||||||
|
};
|
||||||
|
if user_repo
|
||||||
|
.query_user_by_email(payload.email.clone())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User already exists");
|
||||||
|
}
|
||||||
|
let hashed_password = match hash_password(&payload.password) {
|
||||||
|
Ok(hash) => hash,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to hash password",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let new_user = AuthRegisterRequestDto {
|
||||||
|
email: payload.email,
|
||||||
|
password: hashed_password,
|
||||||
|
fullname: payload.fullname,
|
||||||
|
phone_number: payload.phone_number,
|
||||||
|
};
|
||||||
|
let otp = generate_otp::OtpManager::generate_otp();
|
||||||
|
match auth_repo
|
||||||
|
.query_store_otp(new_user.email.clone(), otp.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
let message = format!("your otp code is {}", otp);
|
||||||
|
if let Err(err) = send_email(&new_user.email, "OTP Verification", &message) {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&err.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &role.id);
|
||||||
|
let user_thing = make_thing(
|
||||||
|
&ResourceEnum::Users.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
);
|
||||||
|
match user_repo
|
||||||
|
.query_create_user(UsersSchema {
|
||||||
|
id: user_thing,
|
||||||
|
email: new_user.email.clone(),
|
||||||
|
fullname: new_user.fullname.clone(),
|
||||||
|
password: new_user.password.clone(),
|
||||||
|
phone_number: new_user.phone_number.clone(),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
role: role_thing,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(err) => {
|
||||||
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_resend_otp(
|
||||||
|
payload: AuthResendOtpRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let user_repo = UsersRepository::new(state);
|
||||||
|
if user_repo
|
||||||
|
.query_user_by_email(payload.email.clone())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
|
}
|
||||||
|
let auth_repo = AuthRepository::new(state);
|
||||||
|
let _ = auth_repo.query_get_stored_otp(payload.email.clone()).await;
|
||||||
|
let otp = generate_otp::OtpManager::generate_otp();
|
||||||
|
let message = format!("Your OTP code is {}", otp);
|
||||||
|
match auth_repo.query_store_otp(payload.email.clone(), otp).await {
|
||||||
|
Ok(_) => match send_email(&payload.email, "OTP Verification", &message) {
|
||||||
|
Ok(_) => common_response(StatusCode::OK, "OTP resent successfully"),
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
},
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_refresh_token(
|
||||||
|
payload: AuthRefreshTokenRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let email = match decode_refresh_token(&payload.refresh_token) {
|
||||||
|
Ok(token) => token.claims.sub,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let access_token = match encode_access_token(email.clone()) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate access token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let refresh_token = match encode_refresh_token(email.clone()) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate refresh token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = ResponseSuccessDto {
|
||||||
|
data: TokenDto {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
success_response(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_forgot_password(
|
||||||
|
payload: AuthResendOtpRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let user_repo = UsersRepository::new(state);
|
||||||
|
let user_result = user_repo.query_user_by_email(payload.email.clone()).await;
|
||||||
|
let user = match user_result {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(err) if err.to_string().contains("User not found") => {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let token = match encode_reset_password_token(user.email) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate access token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let env = Env::new();
|
||||||
|
let fe_url = env.fe_url;
|
||||||
|
let message = format!(
|
||||||
|
"You have requested a password reset. Please click the link below to continue: {}/auth/reset-password?token={}",
|
||||||
|
fe_url, token
|
||||||
|
);
|
||||||
|
match send_email(&payload.email, "Reset Password Request", &message) {
|
||||||
|
Ok(_) => common_response(StatusCode::OK, "Reset Password request send"),
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_verify_email(
|
||||||
|
payload: AuthVerifyEmailRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let user_repo = UsersRepository::new(state);
|
||||||
|
let auth_repo = AuthRepository::new(state);
|
||||||
|
let email = payload.email.clone();
|
||||||
|
let user = match user_repo.query_user_by_email(email.clone()).await {
|
||||||
|
Ok(user) if !user.is_deleted => user,
|
||||||
|
_ => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
|
};
|
||||||
|
let patch = UsersSchema {
|
||||||
|
id: user.id.clone(),
|
||||||
|
is_active: true,
|
||||||
|
..UsersSchema::from(user)
|
||||||
|
};
|
||||||
|
match auth_repo.query_get_stored_otp(email.clone()).await {
|
||||||
|
Ok(stored_otp) => match stored_otp == payload.otp {
|
||||||
|
true => match user_repo.query_update_user(patch).await {
|
||||||
|
Ok(_) => match auth_repo.query_delete_stored_otp(email).await {
|
||||||
|
Ok(_) => common_response(StatusCode::OK, "Email verified successfully"),
|
||||||
|
Err(e) => {
|
||||||
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
},
|
||||||
|
false => match auth_repo.query_delete_stored_otp(email).await {
|
||||||
|
Ok(_) => common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP"),
|
||||||
|
Err(e) => common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("Failed to delete OTP: {}", e),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_new_password(
|
||||||
|
payload: AuthNewPasswordRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
let email = match extract_email_token(payload.token.clone()) {
|
||||||
|
Some(email) => email,
|
||||||
|
None => {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "Invalid or missing token");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let password = match hash_password(&payload.password) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to hash password",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let user = match repo.query_user_by_email(email.clone()).await {
|
||||||
|
Ok(user) if !user.is_deleted => user,
|
||||||
|
_ => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
|
};
|
||||||
|
let patch = UsersSchema {
|
||||||
|
id: user.id.clone(),
|
||||||
|
password,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match repo.query_update_user(patch).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use axum::{Router, routing::post};
|
||||||
|
|
||||||
|
pub mod auth_controller;
|
||||||
|
pub mod auth_dto;
|
||||||
|
pub mod auth_repository;
|
||||||
|
pub mod auth_schema;
|
||||||
|
pub mod auth_service;
|
||||||
|
|
||||||
|
pub use auth_dto::*;
|
||||||
|
pub use auth_repository::*;
|
||||||
|
pub use auth_schema::*;
|
||||||
|
pub use auth_service::*;
|
||||||
|
|
||||||
|
pub fn auth_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/forgot", post(auth_controller::post_forgot_password))
|
||||||
|
.route("/login", post(auth_controller::post_login))
|
||||||
|
.route("/new-password", post(auth_controller::post_new_password))
|
||||||
|
.route("/refresh", post(auth_controller::post_refresh_token))
|
||||||
|
.route("/register", post(auth_controller::post_register))
|
||||||
|
.route("/send-otp", post(auth_controller::post_resend_otp))
|
||||||
|
.route("/verify-email", post(auth_controller::post_verify_email))
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user