Compare commits

...
Author SHA1 Message Date
MythEclipse 02be09506a refactor: update surrealdb client usage to support 'any' engine type 2025-06-29 23:23:18 +07:00
Maulana SodiqinandGitHub b88ecd1361 Merge pull request #37 from bakunya/develop
crud testi: acc banh
2025-06-29 00:34:44 +07:00
0x6d696b7566616e fa435994f2 crud testi 2025-06-29 00:01:00 +07:00
Maulana SodiqinandGitHub 16c613d699 Merge pull request #36 from IMPHNEN/feat/dotenv-dynamic-engine-seeder
feat: add dotenvy for environment variable management
2025-06-28 20:50:20 +07:00
MythEclipse f598dedb3b feat: add dotenvy for environment variable management and improve error handling in Env struct 2025-06-28 20:21:20 +07:00
Maulana SodiqinandGitHub cfa251337f Create deploy-to-ancikri.yml 2025-05-30 15:40:41 +07:00
Maulana Sodiqin cca9aa6318 chore: optimize dockerfile 2025-05-26 23:41:00 +07:00
Maulana Sodiqin f1e4e02697 fix: landing 2025-05-26 23:09:01 +07:00
Maulana SodiqinandGitHub a3c2715a85 Merge pull request #25 from bakunya/develop
feat: cms event
2025-05-26 15:28:05 +07:00
0x6d696b7566616e 89cc631fb0 feat: cms event 2025-05-26 14:11:17 +07:00
Maulana Sodiqin 36c12110c2 fixing dockerfile 2025-05-22 22:29:28 +07:00
Maulana Sodiqin 070060365a docs: update readme 2025-05-22 16:18:15 +07:00
Maulana Sodiqin 68e816f956 fix: schema default value 2025-05-22 15:25:24 +07:00
Maulana Sodiqin 48db5f07ef fix: register roles missmatch 2025-05-22 15:13:58 +07:00
Maulana Sodiqin a4b5d91906 chore: setup folder structure 2025-05-22 00:40:40 +07:00
Maulana Sodiqin 9878279660 feat: add gacha credits 2025-05-22 00:31:36 +07:00
Maulana Sodiqin 465cbee2c3 feat: finishing gacha feature 2025-05-21 19:37:30 +07:00
Maulana Sodiqin 33a2ae86c6 fix: unit tests 2025-05-21 11:35:35 +07:00
Maulana Sodiqin d906fc4747 chore: update query list 2025-05-20 16:57:45 +07:00
Maulana Sodiqin 055ef6ac8e feat: adjust readme and add new seeder script 2025-05-20 11:54:15 +07:00
Maulana Sodiqin e63658c082 feat: add gacha and init cms 2025-05-20 10:45:01 +07:00
Maulana SodiqinandGitHub f4b0ed0a4a Merge pull request #24 from IMPHNEN/chore/normalize-code
chore: normalize code
2025-05-20 01:50:58 +07:00
Maulana Sodiqin 1e5ed26f76 chore: cleaning code 2025-05-20 01:49:50 +07:00
Maulana Sodiqin d97095a38d chore: cleaning code 2025-05-20 00:26:24 +07:00
Maulana Sodiqin e48507901e chore: normalize code #2 2025-05-18 03:12:20 +07:00
Maulana Sodiqin a43ef8ec04 chore: normalize code 2025-05-17 02:08:59 +07:00
Maulana SodiqinandGitHub 874f25aa80 Merge pull request #23 from bakunya/develop
transform id(thing) to id(string) on get role, permission, users
2025-05-04 17:24:13 +07:00
Bakunya 12a730a394 transform id(thing) to id(string) on get role, permission, users 2025-05-04 17:07:20 +07:00
Bakunya bdcd41f54d transform id(thing) to id(string) on get role, permission, users 2025-05-04 16:37:11 +07:00
Maulana SodiqinandGitHub 96fb8eb7d3 Merge pull request #22 from IMPHNEN/feat/split-middleware
feat: split middleware
2025-04-07 20:43:59 +07:00
Maulana Sodiqin d2c333dbd6 feat: split middleware 2025-04-07 20:41:14 +07:00
Maulana Sodiqin 054f0f63ff chore: adjust README.md 2025-04-07 20:08:26 +07:00
Maulana Sodiqin eb05ea5994 feat: add gacha service 2025-04-07 00:50:57 +07:00
Maulana Sodiqin 4f870ec4c9 fix: unit test 2025-04-06 23:35:02 +07:00
Maulana Sodiqin 31f9b6cfea feat: microservice ready 2025-04-06 22:13:46 +07:00
Maulana SodiqinandGitHub 07a93ff0b7 Merge pull request #21 from bakunya/develop
fix otp and password validation
2025-04-06 20:09:17 +07:00
Bakunya d2b098ed92 fix otp and password validation 2025-04-06 19:15:01 +07:00
Maulana Sodiqin 09a7f4371a chore: remove uneeded cargo package 2025-04-06 13:28:15 +07:00
Maulana Sodiqin b4bf78f0d6 chore: re-map project structure to using multi workspace paradigm 2025-04-06 13:25:44 +07:00
Maulana Sodiqin de9c5a6dc1 feat: implement guard 2025-04-02 09:35:37 +07:00
Maulana Sodiqin 8a661df059 feat: add image url to gacha DTO 2025-04-01 20:05:07 +07:00
Maulana Sodiqin c7c16d4197 feat: add image url 2025-04-01 20:04:28 +07:00
Maulana Sodiqin 3aec0346a9 fix: wrong id 2025-04-01 20:01:40 +07:00
Maulana Sodiqin 8d2f720de1 feat: add gacha 2025-04-01 20:00:00 +07:00
Maulana Sodiqin 4b8da68df4 feat: gacha 2025-03-30 18:58:17 +07:00
Maulana Sodiqin 6460979bab feat: complete backend service 2025-03-30 16:08:58 +07:00
Maulana Sodiqin 3ae9dbd717 feat: success test 2025-03-30 14:13:45 +07:00
Maulana Sodiqin 0816566d03 feat: update logic 2025-03-30 02:46:16 +07:00
Maulana Sodiqin a97044edfc fix: seeder roles permissions 2025-03-28 17:18:07 +07:00
Maulana Sodiqin 9d92a9a136 feat: pagination 2025-03-26 13:06:34 +07:00
Maulana Sodiqin 8e12c79ea6 feat: permissions controller 2025-03-25 20:50:42 +07:00
Maulana Sodiqin 5777268441 feat: fixing role and auth test 2025-03-25 19:30:02 +07:00
Maulana Sodiqin 34eaa418d5 feat: roles router 2025-03-25 10:48:34 +07:00
Maulana Sodiqin 77f12610dd fix: test user 2025-03-24 13:12:54 +07:00
Maulana Sodiqin 5be737ad70 chore: add test coverage 2025-03-23 20:42:38 +07:00
Maulana Sodiqin 0ae1e26ba8 chore: complete unit test of user and authentication system 2025-03-23 12:32:00 +07:00
Maulana Sodiqin 79721b7af6 chore: implement unit test 2025-03-23 07:56:43 +07:00
Maulana Sodiqin 093e290221 feat: complete auth system 2025-03-23 01:19:17 +07:00
Maulana Sodiqin 1e6957720e feat: user management 2025-03-21 17:00:19 +07:00
Maulana Sodiqin a0f263bb4f feat: update query 2025-03-21 13:25:22 +07:00
Maulana Sodiqin 413a28c089 feat: setup data 2025-03-21 05:36:00 +07:00
Maulana Sodiqin a90e8662e1 feat: initial commit 2025-03-20 22:41:20 +07:00
Maulana Sodiqin 955d384f96 fix: docker 2025-03-20 17:36:43 +07:00
Maulana Sodiqin ea5ece9ab2 feat: initial commit 2025-03-20 17:33:57 +07:00
Maulana Sodiqin 8ff532bb67 fix: cors 2025-03-20 17:07:38 +07:00
Maulana Sodiqin eb6cfd51ae feat: auth functionality 2025-03-19 23:17:50 +07:00
Maulana Sodiqin 11d6249007 feat: docs 2025-03-19 20:55:29 +07:00
Maulana Sodiqin 497c250e6d fix: change deployed branch 2025-03-19 19:34:57 +07:00
Maulana Sodiqin 2872c1827a feat: deploy 2025-03-19 19:33:56 +07:00
Maulana Sodiqin f9c49d44b5 feat: gacha roll 2025-03-19 05:34:39 +07:00
Maulana Sodiqin 12043f027c feat: gacha roll, user auth 2025-03-18 14:13:53 +07:00
Maulana Sodiqin 0c28d34d57 feat: apply env 2025-03-17 09:27:19 +07:00
Maulana Sodiqin 271b00c2c4 feat: create gacha item 2025-03-15 02:23:27 +07:00
Maulana Sodiqin b096f4129a feat: add middleware 2025-03-15 01:53:20 +07:00
Maulana Sodiqin 7ab036f5f7 feat: user relation 2025-03-14 17:50:37 +07:00
Maulana Sodiqin 9ffe808797 feat: basic auth 2025-03-14 09:53:20 +07:00
Maulana Sodiqin acfb8259bc feat: auth 2025-03-13 23:40:16 +07:00
Maulana Sodiqin 298144bd36 chore: fix nix 2025-02-26 15:55:48 +07:00
Maulana Sodiqin ff9d8cba69 feat: add cargo 2025-02-26 15:45:39 +07:00
Maulana Sodiqin 8b2769706e feat: add cargo 2025-02-26 15:45:05 +07:00
Maulana Sodiqin b8606ef0bb chore: remove seaorm 2025-02-25 02:02:01 +07:00
Maulana Sodiqin 44de53a49d feat: implement surrealdb and redis 2025-02-25 02:00:19 +07:00
Maulana Sodiqin 9abab01b9c chore: re-format file 2025-02-16 01:09:55 +07:00
Maulana Sodiqin 53414687ff chore: initial commit 2025-02-16 01:02:51 +07:00
Maulana SodiqinandGitHub d2ec21c7b1 chore: add readme 2025-02-08 01:16:14 +07:00
Maulana SodiqinandGitHub 4deee3a9cb chore: configure action 2025-02-08 01:10:48 +07:00
Maulana SodiqinandGitHub b50ce2b060 Merge pull request #1 from IMPHNEN/feat/auth-login
feat: auth login
2025-02-08 01:09:57 +07:00
Maulana Sodiqin ffc72367d5 feat: auth login 2025-02-08 00:58:54 +07:00
178 changed files with 12097 additions and 1195 deletions
+5 -1
View File
@@ -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=
+65
View File
@@ -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"
+65
View File
@@ -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"
+19
View File
@@ -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
+1
View File
@@ -4,6 +4,7 @@
# Nix # Nix
/.direnv /.direnv
/Cargo.nix
# Environment # Environment
.envrc .envrc
Generated
+3237 -869
View File
File diff suppressed because it is too large Load Diff
+36 -11
View File
@@ -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
View File
@@ -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"]
+130
View File
@@ -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)._
+35
View File
@@ -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
+30
View File
@@ -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"
+25
View File
@@ -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"
+8
View File
@@ -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"
+9
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

Generated
+27
View File
@@ -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
}
+13 -5
View File
@@ -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 {};
}); });
}; };
} }
+28
View File
@@ -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
+12
View File
@@ -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;
}
+93
View File
@@ -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(())
}
+72
View File
@@ -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(())
}
+68
View File
@@ -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(())
}
+26
View File
@@ -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(())
}
+10
View File
@@ -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;
}
+25
View File
@@ -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
+2
View File
@@ -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()),
}
}
}
+44
View File
@@ -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),
)
}
+5
View File
@@ -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()),
}
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod landing;
pub use landing::*;
+24
View File
@@ -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
+14
View File
@@ -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);
}
}
View File
+11
View File
@@ -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
+70
View File
@@ -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,
}
+26
View File
@@ -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::*;
+26
View File
@@ -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
+10
View File
@@ -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()),
}
}
}
+22
View File
@@ -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())
}
}
}
}
}
+25
View File
@@ -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()),
}
}
}
+22
View File
@@ -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))
}
+18
View File
@@ -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())
}
View File
+28
View File
@@ -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
+144
View File
@@ -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()
}
+46
View File
@@ -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))
}
+24
View File
@@ -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
+34
View File
@@ -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(),
}
}
+125
View File
@@ -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
}
+119
View File
@@ -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,
}
+115
View File
@@ -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"),
}
}
}
+8
View File
@@ -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>,
}
+358
View File
@@ -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()),
}
}
}
+23
View File
@@ -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