Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f870ec4c9 | ||
|
|
31f9b6cfea | ||
|
|
07a93ff0b7 | ||
|
|
d2b098ed92 | ||
|
|
09a7f4371a | ||
|
|
b4bf78f0d6 | ||
|
|
de9c5a6dc1 | ||
|
|
8a661df059 | ||
|
|
c7c16d4197 | ||
|
|
3aec0346a9 | ||
|
|
8d2f720de1 | ||
|
|
4b8da68df4 | ||
|
|
6460979bab | ||
|
|
3ae9dbd717 | ||
|
|
0816566d03 | ||
|
|
a97044edfc | ||
|
|
9d92a9a136 | ||
|
|
8e12c79ea6 | ||
|
|
5777268441 | ||
|
|
34eaa418d5 | ||
|
|
77f12610dd | ||
|
|
5be737ad70 | ||
|
|
0ae1e26ba8 | ||
|
|
79721b7af6 | ||
|
|
093e290221 | ||
|
|
1e6957720e | ||
|
|
a0f263bb4f | ||
|
|
413a28c089 | ||
|
|
a90e8662e1 | ||
|
|
955d384f96 | ||
|
|
ea5ece9ab2 | ||
|
|
8ff532bb67 | ||
|
|
eb6cfd51ae | ||
|
|
11d6249007 | ||
|
|
497c250e6d | ||
|
|
2872c1827a | ||
|
|
f9c49d44b5 | ||
|
|
12043f027c | ||
|
|
0c28d34d57 | ||
|
|
271b00c2c4 | ||
|
|
b096f4129a | ||
|
|
7ab036f5f7 | ||
|
|
9ffe808797 | ||
|
|
acfb8259bc | ||
|
|
298144bd36 | ||
|
|
ff9d8cba69 | ||
|
|
8b2769706e | ||
|
|
b8606ef0bb | ||
|
|
44de53a49d | ||
|
|
9abab01b9c | ||
|
|
53414687ff | ||
|
|
d2ec21c7b1 | ||
|
|
4deee3a9cb | ||
|
|
b50ce2b060 |
+5
-1
@@ -1,4 +1,8 @@
|
|||||||
PORT=
|
PORT=
|
||||||
DATABASE_URL=
|
SURREALDB_URL=
|
||||||
|
SURREALDB_USERNAME=
|
||||||
|
SURREALDB_PASSWORD=
|
||||||
|
SURREALDB_NAMESPACE=
|
||||||
|
SURREALDB_DBNAME=
|
||||||
ACCESS_TOKEN_SECRET=
|
ACCESS_TOKEN_SECRET=
|
||||||
REFRESH_TOKEN_SECRET=
|
REFRESH_TOKEN_SECRET=
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Build the project
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Stop service on VPS before upload
|
||||||
|
uses: appleboy/ssh-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_IP }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
echo "Stopping the service before uploading the binary"
|
||||||
|
sudo systemctl stop imphnen-backend-service
|
||||||
|
|
||||||
|
- name: Upload artifact to VPS
|
||||||
|
uses: appleboy/scp-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_IP }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PORT }}
|
||||||
|
source: ./target/release/*
|
||||||
|
target: /opt/imphnen-backend-service/imphnen-backend-service
|
||||||
|
rm: true
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_IP }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Restarting the service"
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
sudo systemctl restart imphnen-backend-service
|
||||||
|
|
||||||
|
echo "Deployment completed successfully"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["develop"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["develop"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
/.direnv
|
/.direnv
|
||||||
|
/Cargo.nix
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.envrc
|
.envrc
|
||||||
|
|||||||
Generated
+3059
-871
File diff suppressed because it is too large
Load Diff
+19
-8
@@ -1,21 +1,32 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "imphnen-cms-be"
|
resolver = "2"
|
||||||
version = "0.1.0"
|
members = [
|
||||||
edition = "2021"
|
"imphnen-*",
|
||||||
|
"tests",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[workspace.dependencies]
|
||||||
axum = { version = "0.8.1", features = ["multipart"] }
|
axum = { version = "0.8.1", 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.43.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.39"
|
||||||
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.12", features = ["tokio1-native-tls"] }
|
||||||
|
surrealdb = { version = "2.2.1", features = ["kv-mem"] }
|
||||||
|
thiserror = "2.0.11"
|
||||||
|
anyhow = "1.0.97"
|
||||||
|
rand = "0.9.0"
|
||||||
|
tower-http = { version = "0.6.2", 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"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
|
|||||||
+3
-3
@@ -11,12 +11,12 @@ WORKDIR /app
|
|||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY ./src ./src
|
COPY ./src ./src
|
||||||
|
|
||||||
RUN cargo build --release && strip /app/target/release/imphnen-cms-api
|
RUN cargo build --release && strip /app/target/release/imphnen-backend-service
|
||||||
|
|
||||||
FROM gcr.io/distroless/cc AS runner
|
FROM gcr.io/distroless/cc AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/target/release/imphnen-cms-api .
|
COPY --from=builder /app/target/release/imphnen-backend-service .
|
||||||
|
|
||||||
CMD ["/app/imphnen-cms-api"]
|
CMD ["/app/imphnen-backend-service"]
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Axum SurrealDB Boilerplate
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Authentication Ready**: Preconfigured authentication and middleware for secure API access.
|
||||||
|
- **Database Integration**: SurrealDB seamlessly integrated as an Axum Extension.
|
||||||
|
- **CORS Handling**: Fine-tuned CORS management with Tower HTTP `CorsLayer`.
|
||||||
|
- **API Documentation**: Fully documented with OpenAPI and Swagger UI.
|
||||||
|
- **Optimized for Performance**: Asynchronous, lightweight, and scalable architecture.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Rust**: Install Rust from [rust-lang.org](https://www.rust-lang.org/).
|
||||||
|
- **Database**: Set up a SurrealDB instance and configure connection details.
|
||||||
|
- **Docker**: Required for containerized deployment, install from [docker.com](https://www.docker.com/).
|
||||||
|
- **Nix (Optional)**: For reproducible builds, install from [nixos.org](https://nixos.org/).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. **Clone the Repository**:
|
||||||
|
|
||||||
|
- `git clone https://github.com/maulanasdqn/axum-surrealdb-boilerplate`
|
||||||
|
|
||||||
|
2. **Set Up Environment Variables**:
|
||||||
|
|
||||||
|
- Copy `.env.example` and rename it to `.env`
|
||||||
|
|
||||||
|
- **Windows**: Run the script: `./apply-env.ps1`
|
||||||
|
- **Unix-based systems (Linux, macOS, BSD)**: Run the script: `./apply-env.sh`
|
||||||
|
|
||||||
|
3. **Install Dependencies**:
|
||||||
|
|
||||||
|
- `cargo install .`
|
||||||
|
|
||||||
|
4. **Setup Database**:
|
||||||
|
|
||||||
|
- Install the surrealDB
|
||||||
|
- **Windows**: `iwr https://windows.surrealdb.com -useb | iex`
|
||||||
|
- **Unix-based systems (Linux, macOS, BSD)**: `curl -sSf https://install.surrealdb.com | sh`
|
||||||
|
- Start the database `surreal start --user root --pass root`
|
||||||
|
|
||||||
|
5. **Start the Server**:
|
||||||
|
|
||||||
|
- Install Cargo Watch `cargo install cargo-watch`
|
||||||
|
- Run it with cargo watch `cargo watch -x run`
|
||||||
|
|
||||||
|
The API will be available at `http://localhost:3000/docs`.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
1. **Build the Docker Image**:
|
||||||
|
|
||||||
|
2. **Run the Docker Container**:
|
||||||
|
|
||||||
|
The API will be accessible at `http://localhost:3000/docs`.
|
||||||
|
|
||||||
|
## Using Nix as Builder (Optional)
|
||||||
|
|
||||||
|
1. **Install Nix**:
|
||||||
|
|
||||||
|
2. **Enter Nix Shell or Use Nix Flakes**:
|
||||||
|
|
||||||
|
3. **Build the Project**:
|
||||||
|
|
||||||
|
4. **Run the Server**:
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Fork the repository and create a pull request with your improvements.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
- [Axum](https://github.com/tokio-rs/axum)
|
||||||
|
- [SurrealDB](https://github.com/surrealdb/surrealdb)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
function Set-TempEnvFromDotEnv {
|
||||||
|
param (
|
||||||
|
[string]$envFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-Not (Test-Path $envFilePath)) {
|
||||||
|
Write-Error "The .env file at path '$envFilePath' does not exist."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$envContent = Get-Content $envFilePath
|
||||||
|
|
||||||
|
foreach ($line in $envContent) {
|
||||||
|
$trimmedLine = $line.Trim()
|
||||||
|
|
||||||
|
if (-Not [string]::IsNullOrWhiteSpace($trimmedLine) -and -Not $trimmedLine.StartsWith("#")) {
|
||||||
|
$keyValue = $trimmedLine -split "=", 2
|
||||||
|
if ($keyValue.Length -eq 2) {
|
||||||
|
$key = $keyValue[0].Trim()
|
||||||
|
$value = $keyValue[1].Trim()
|
||||||
|
[System.Environment]::SetEnvironmentVariable($key, $value, [System.EnvironmentVariableTarget]::Process)
|
||||||
|
Write-Host "Set temporary environment variable: $key=$value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "All environment variables from '$envFilePath' have been set temporarily."
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-TempEnvFromDotEnv -envFilePath ".env"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set_temp_env_from_dotenv() {
|
||||||
|
local env_file_path="$1"
|
||||||
|
|
||||||
|
if [[ ! -f "$env_file_path" ]]; then
|
||||||
|
echo "Error: The .env file at path '$env_file_path' does not exist."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
trimmed_line=$(echo "$line" | xargs)
|
||||||
|
|
||||||
|
if [[ -n "$trimmed_line" && ! "$trimmed_line" =~ ^# ]]; then
|
||||||
|
key=$(echo "$trimmed_line" | cut -d '=' -f 1 | xargs)
|
||||||
|
value=$(echo "$trimmed_line" | cut -d '=' -f 2- | xargs)
|
||||||
|
export "$key=$value"
|
||||||
|
echo "Set temporary environment variable: $key=$value"
|
||||||
|
fi
|
||||||
|
done < "$env_file_path"
|
||||||
|
|
||||||
|
echo "All environment variables from '$env_file_path' have been set temporarily."
|
||||||
|
}
|
||||||
|
|
||||||
|
set_temp_env_from_dotenv ".env"
|
||||||
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1739020877,
|
||||||
|
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -1,24 +1,32 @@
|
|||||||
{
|
{
|
||||||
description = "IMPHNEN CMS API Nix Flake";
|
description = "IMPHNEN Backend Service Nix Flake";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
}: let
|
}: let
|
||||||
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux"];
|
||||||
|
pkgsFor = system:
|
||||||
|
import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config = {
|
||||||
|
allowUnfree = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
pkgsFor = nixpkgs.legacyPackages;
|
|
||||||
in {
|
in {
|
||||||
packages = forAllSystems (system: {
|
packages = forAllSystems (system: {
|
||||||
default = pkgsFor.${system}.callPackage ./default.nix {};
|
default = (pkgsFor system).callPackage ./default.nix {};
|
||||||
});
|
});
|
||||||
devShells = forAllSystems (system: {
|
devShells = forAllSystems (system: {
|
||||||
default = pkgsFor.${system}.callPackage ./shell.nix {};
|
default = (pkgsFor system).callPackage ./shell.nix {};
|
||||||
});
|
});
|
||||||
dockerImages = forAllSystems (system: {
|
dockerImages = forAllSystems (system: {
|
||||||
tryOutApi = pkgsFor.${system}.callPackage ./docker.nix {};
|
tryOutApi = (pkgsFor system).callPackage ./docker.nix {};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-backend-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
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
|
||||||
|
imphnen-gateway-service = { version = "0.1.0", path = "../imphnen-gateway-service" }
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use imphnen_gateway_service::gateway_service;
|
||||||
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||||
|
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use imphnen_utils::{get_iso_date, Env};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let env = Env::new();
|
||||||
|
let db = Surreal::new::<Ws>(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 permissions = vec![
|
||||||
|
(
|
||||||
|
"023e2dfe-93c3-4008-94a8-b5dff403f73b",
|
||||||
|
"Create Users",
|
||||||
|
Some("2025-01-29T06:08:23.838311+00"),
|
||||||
|
Some("2025-01-29T06:08:23.838312+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"0269ed71-0ae0-4c43-ad29-e3d861d8f9a0",
|
||||||
|
"Create Permissions",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"299cb4d5-6556-4cc9-b6c1-32e6d31e0f9b",
|
||||||
|
"Update Permissions",
|
||||||
|
Some("2025-01-29T05:11:01.265+00"),
|
||||||
|
Some("2025-01-29T05:11:01.265001+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"319ee593-ff0a-4f29-bbaf-9feb3174a3a2",
|
||||||
|
"Create Roles",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6",
|
||||||
|
"Read Detail Users",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"35b0d992-65c8-4b62-b030-e6e0320e4048",
|
||||||
|
"Delete Roles",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"4da8b434-89f9-4d91-85ae-eebd63cdbeda",
|
||||||
|
"Update Activate Users",
|
||||||
|
Some("2025-02-01T12:38:09.741726+00"),
|
||||||
|
Some("2025-02-01T12:38:09.741727+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"73888d18-b3e9-4f62-95a5-ba2c0d69fccb",
|
||||||
|
"Read Detail Roles",
|
||||||
|
Some("2025-01-29T05:13:06.445925+00"),
|
||||||
|
Some("2025-01-29T10:31:46.408564+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"7c15e31d-36e2-49f9-97db-138c03fb0cf6",
|
||||||
|
"Read List Users",
|
||||||
|
Some("2025-01-28T15:02:41.772931+00"),
|
||||||
|
Some("2025-01-28T15:02:41.772933+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"7d4b1379-4960-416a-b045-98cd82c0cac9",
|
||||||
|
"Read Detail Sessions",
|
||||||
|
Some("2025-02-24T16:52:26.886664+00"),
|
||||||
|
Some("2025-02-24T16:52:26.886673+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"8195eeb8-e64f-4172-aa57-596492c84a72",
|
||||||
|
"Read List Permissions",
|
||||||
|
Some("2025-01-28T15:05:28.6299+00"),
|
||||||
|
Some("2025-01-28T15:05:28.629901+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"81eba91d-b8ab-44b9-bbfe-4e6da2f98952",
|
||||||
|
"Read List Tests",
|
||||||
|
Some("2025-02-24T16:52:27.179542+00"),
|
||||||
|
Some("2025-02-24T16:52:27.179551+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"9164ca6e-c7e3-4238-a15f-f36ab9577e7e",
|
||||||
|
"Read List Roles",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"96df0689-2ae9-4894-bf00-837c19415e5c",
|
||||||
|
"Delete Users",
|
||||||
|
Some("2025-02-02T06:52:05.195565+00"),
|
||||||
|
Some("2025-02-02T06:52:05.195565+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"98b3dc4c-0124-461f-afcd-166637c5e6e8",
|
||||||
|
"Update Users",
|
||||||
|
Some("2025-01-29T05:34:40.621554+00"),
|
||||||
|
Some("2025-01-29T05:34:40.621555+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a00d5608-4c48-4542-845c-dfe004687022",
|
||||||
|
"Update Roles",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"b2dc3928-86ba-4c59-a03d-0b57d5183ebc",
|
||||||
|
"Delete Permissions",
|
||||||
|
Some("2025-01-29T05:14:22.511084+00"),
|
||||||
|
Some("2025-01-29T05:14:22.511085+00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dad435cf-042c-41bd-a946-cea61ed2ffbc",
|
||||||
|
"Read Detail Permissions",
|
||||||
|
Some("2025-01-28T15:07:10.990214+00"),
|
||||||
|
Some("2025-01-28T15:07:10.990214+00"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (id, name, _created_at, _updated_at) in permissions {
|
||||||
|
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||||
|
.bind(("id", id))
|
||||||
|
.bind((
|
||||||
|
"data",
|
||||||
|
json!({
|
||||||
|
"name": name,
|
||||||
|
"is_deleted": false,
|
||||||
|
"created_at": get_iso_date(),
|
||||||
|
"updated_at": get_iso_date()
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
println!("✅ Inserted: {}", name);
|
||||||
|
}
|
||||||
|
println!("✅ Semua permissions berhasil disimpan ke SurrealDB!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use imphnen_utils::{get_iso_date, Env};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let env = Env::new();
|
||||||
|
let db = Surreal::new::<Ws>(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!("✅ Semua role berhasil disimpan ke SurrealDB!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use imphnen_utils::{get_iso_date, make_thing, Env};
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let env = Env::new();
|
||||||
|
let db = Surreal::new::<Ws>(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 role_permissions = vec![
|
||||||
|
(
|
||||||
|
"50133429-f4b1-4249-9f97-7b86e6ee9d86", // Staf
|
||||||
|
vec![
|
||||||
|
"7c15e31d-36e2-49f9-97db-138c03fb0cf6", // Read List Users
|
||||||
|
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6", // Read Detail Users
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"f6b03f25-e416-4893-ac88-caaa690afb07", // Admin
|
||||||
|
vec![
|
||||||
|
"023e2dfe-93c3-4008-94a8-b5dff403f73b", // Create Users
|
||||||
|
"96df0689-2ae9-4894-bf00-837c19415e5c", // Delete Users
|
||||||
|
"98b3dc4c-0124-461f-afcd-166637c5e6e8", // Update Users
|
||||||
|
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6", // Read Detail Users
|
||||||
|
"7c15e31d-36e2-49f9-97db-138c03fb0cf6", // Read List Users
|
||||||
|
"9164ca6e-c7e3-4238-a15f-f36ab9577e7e", // Read List Roles
|
||||||
|
"319ee593-ff0a-4f29-bbaf-9feb3174a3a2", // Create Roles
|
||||||
|
"a00d5608-4c48-4542-845c-dfe004687022", // Update Roles
|
||||||
|
"35b0d992-65c8-4b62-b030-e6e0320e4048", // Delete Roles
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (role_id, permission_ids) in role_permissions {
|
||||||
|
let permission_refs: Vec<_> = permission_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|perm_id| make_thing("app_permissions", perm_id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at")
|
||||||
|
.bind(("role_id", role_id))
|
||||||
|
.bind(("permissions", permission_refs))
|
||||||
|
.bind(("updated_at", get_iso_date()))
|
||||||
|
.await?;
|
||||||
|
println!("✅ Updated permissions for role ID: {}", role_id);
|
||||||
|
}
|
||||||
|
println!("✅ Semua roles telah diperbarui dengan permissions di SurrealDB!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use imphnen_utils::{get_iso_date, hash_password, Env};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::error::Error;
|
||||||
|
use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, sql::Thing, Surreal};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UsersSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub referral_code: Option<String>,
|
||||||
|
pub referred_by: Option<String>,
|
||||||
|
pub identity_number: Option<String>,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub student_type: String,
|
||||||
|
pub religion: Option<String>,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
pub is_profile_completed: bool,
|
||||||
|
pub role: Thing,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let env = Env::new();
|
||||||
|
let db = Surreal::new::<Ws>(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(),
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
identity_number: None,
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
student_type: "TNI".into(),
|
||||||
|
religion: None,
|
||||||
|
gender: None,
|
||||||
|
birthdate: None,
|
||||||
|
is_profile_completed: false,
|
||||||
|
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!("✅ Semua users berhasil disimpan ke SurrealDB!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use imphnen_gateway_service::gateway_service;
|
||||||
|
use imphnen_libs::axum_init;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||||
|
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-dimentorin-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-entities"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
surrealdb = { workspace = true, features = ["kv-mem"] }
|
||||||
|
thiserror.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{
|
||||||
|
engine::{local::Db, remote::ws::Client},
|
||||||
|
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<Client>;
|
||||||
|
pub type SurrealMemClient = Surreal<Db>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub surrealdb_ws: SurrealWsClient,
|
||||||
|
pub surrealdb_mem: SurrealMemClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct CountResult {
|
||||||
|
pub count: u64,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pub mod error {
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::response::Response;
|
||||||
|
use axum::Json;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("database error")]
|
||||||
|
Db,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(self.to_string())).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<surrealdb::Error> for Error {
|
||||||
|
fn from(error: surrealdb::Error) -> Self {
|
||||||
|
eprintln!("{error}");
|
||||||
|
Self::Db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod common_dto;
|
pub mod common_dto;
|
||||||
|
pub mod error_dto;
|
||||||
pub use common_dto::*;
|
pub use common_dto::*;
|
||||||
|
pub use error_dto::*;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-gacha-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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,93 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
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, 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, Validate)]
|
||||||
|
pub struct GachaRollRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Item ID must not be empty"))]
|
||||||
|
pub item_id: String,
|
||||||
|
|
||||||
|
#[validate(range(min = 1, message = "Weight must be greater than zero"))]
|
||||||
|
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 GachaItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaItemDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GachaClaimDto {
|
||||||
|
pub id: String,
|
||||||
|
pub user: String,
|
||||||
|
pub item: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaClaimDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub user: Thing,
|
||||||
|
pub item: Thing,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GachaRollDto {
|
||||||
|
pub id: String,
|
||||||
|
pub item: String,
|
||||||
|
pub weight: String,
|
||||||
|
pub quantity: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GachaRollDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub item: Thing,
|
||||||
|
pub weight: String,
|
||||||
|
pub quantity: i32,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
use super::{GachaClaimSchema, GachaItemSchema, GachaRollSchema};
|
||||||
|
use crate::{
|
||||||
|
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id,
|
||||||
|
make_thing, query_list_with_meta,
|
||||||
|
};
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
|
pub struct GachaRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GachaRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_gacha_item_list(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<GachaItemSchema>>> {
|
||||||
|
let mut conditions = vec!["is_deleted = false".into()];
|
||||||
|
if meta.search.is_some() {
|
||||||
|
conditions.push("string::contains(name, $search)".into());
|
||||||
|
}
|
||||||
|
query_list_with_meta(
|
||||||
|
&self.state.surrealdb_ws,
|
||||||
|
&ResourceEnum::GachaItems.to_string(),
|
||||||
|
&meta,
|
||||||
|
conditions,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_gacha_claim_by_id(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
) -> Result<GachaClaimSchema> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let result: Option<GachaClaimSchema> = db
|
||||||
|
.select((ResourceEnum::GachaClaims.to_string(), id.clone()))
|
||||||
|
.await?;
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_gacha_roll_by_id(&self, id: String) -> Result<GachaRollSchema> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let result: Option<GachaRollSchema> = db
|
||||||
|
.select((ResourceEnum::GachaRolls.to_string(), id.clone()))
|
||||||
|
.await?;
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
use crate::{make_thing, ResourceEnum};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{sql::Thing, Uuid};
|
||||||
|
|
||||||
|
#[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: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.2,
|
||||||
|
quantity: 2,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod gacha_dto;
|
||||||
|
pub mod gacha_repository;
|
||||||
|
pub mod gacha_schema;
|
||||||
|
|
||||||
|
pub use gacha_repository::*;
|
||||||
|
pub use gacha_schema::*;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod gacha;
|
||||||
|
pub use gacha::*;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-gateway-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-iam-service = { version = "0.1.0", path = "../imphnen-iam-service" }
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use imphnen_iam_service::{
|
||||||
|
auth, permissions, roles, users, AuthLoginRequestDto, AuthLoginResponsetDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto,
|
||||||
|
AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, RolesItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, UsersItemDto, UsersListItemDto, UsersUpdateRequestDto
|
||||||
|
};
|
||||||
|
use utoipa::{
|
||||||
|
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||||
|
Modify, OpenApi,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(
|
||||||
|
MetaRequestDto,
|
||||||
|
MetaResponseDto,
|
||||||
|
MessageResponseDto,
|
||||||
|
AuthLoginRequestDto,
|
||||||
|
AuthLoginResponsetDto,
|
||||||
|
AuthVerifyEmailRequestDto,
|
||||||
|
AuthResendOtpRequestDto,
|
||||||
|
AuthNewPasswordRequestDto,
|
||||||
|
AuthRefreshTokenRequestDto,
|
||||||
|
ResponseSuccessDto<TokenDto>,
|
||||||
|
RolesItemDto,
|
||||||
|
RolesRequestCreateDto,
|
||||||
|
RolesRequestUpdateDto,
|
||||||
|
PermissionsRequestDto,
|
||||||
|
PermissionsItemDto,
|
||||||
|
UsersItemDto,
|
||||||
|
UsersListItemDto,
|
||||||
|
UsersUpdateRequestDto,
|
||||||
|
UsersCreateRequestDto,
|
||||||
|
ResponseSuccessDto<AuthLoginResponsetDto>,
|
||||||
|
ResponseListSuccessDto<Vec<RolesItemDto>>,
|
||||||
|
ResponseSuccessDto<RolesItemDto>,
|
||||||
|
ResponseListSuccessDto<Vec<UsersListItemDto>>,
|
||||||
|
ResponseSuccessDto<UsersDetailItemDto>,
|
||||||
|
ResponseListSuccessDto<Vec<PermissionsItemDto>>,
|
||||||
|
ResponseSuccessDto<PermissionsItemDto>
|
||||||
|
)
|
||||||
|
),
|
||||||
|
info(
|
||||||
|
title = "IMPHNEN Backend Service",
|
||||||
|
description = "IMPHNEN Backend Service for Provide Gacha, Dimentorin and Backoffice Web App",
|
||||||
|
version = "0.1.0",
|
||||||
|
contact(
|
||||||
|
name = "Maulana Sodiqin",
|
||||||
|
url = ""
|
||||||
|
),
|
||||||
|
license(
|
||||||
|
name = "MIT",
|
||||||
|
url = "https://opensource.org/licenses/MIT"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
modifiers(&SecurityAddon),
|
||||||
|
tags(
|
||||||
|
(name = "Authentication", description = "List of Authentication Endpoints"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
|
||||||
|
pub struct ApiDoc;
|
||||||
|
|
||||||
|
struct SecurityAddon;
|
||||||
|
|
||||||
|
impl Modify for SecurityAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
if let Some(components) = openapi.components.as_mut() {
|
||||||
|
components.add_security_scheme(
|
||||||
|
"Bearer",
|
||||||
|
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn docs_router() -> utoipa::openapi::OpenApi {
|
||||||
|
ApiDoc::openapi()
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use axum::{
|
||||||
|
Extension, Router, middleware::from_fn, response::Redirect, routing::get,
|
||||||
|
};
|
||||||
|
use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient};
|
||||||
|
use imphnen_iam_service::{iam_protected_routes, iam_public_routes};
|
||||||
|
|
||||||
|
pub mod docs;
|
||||||
|
pub mod middleware;
|
||||||
|
|
||||||
|
pub use docs::*;
|
||||||
|
pub use middleware::*;
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
|
pub async fn gateway_service(
|
||||||
|
surrealdb_ws: SurrealWsClient,
|
||||||
|
surrealdb_mem: SurrealMemClient,
|
||||||
|
) -> Router {
|
||||||
|
let state = AppState {
|
||||||
|
surrealdb_ws,
|
||||||
|
surrealdb_mem,
|
||||||
|
};
|
||||||
|
|
||||||
|
let public_routes = iam_public_routes();
|
||||||
|
let protected_routes = iam_protected_routes().layer(from_fn(auth_middleware));
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(Redirect::to("/docs")))
|
||||||
|
.nest("/v1", public_routes.merge(protected_routes))
|
||||||
|
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
||||||
|
.layer(cors_middleware())
|
||||||
|
.layer(Extension(state.clone()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use axum::{
|
||||||
|
Extension,
|
||||||
|
extract::Request,
|
||||||
|
http::{HeaderValue, Method, StatusCode, header},
|
||||||
|
middleware::Next,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use imphnen_iam_service::{UsersItemDtoRaw, UsersRepository};
|
||||||
|
use imphnen_libs::{AppState, Env};
|
||||||
|
use imphnen_utils::{common_response, extract_email};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
|
pub async fn auth_middleware(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
mut req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, Infallible> {
|
||||||
|
let headers = req.headers();
|
||||||
|
let email = match extract_email(headers) {
|
||||||
|
Some(email) => email,
|
||||||
|
None => {
|
||||||
|
return Ok(common_response(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid or expired token",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let repository = UsersRepository::new(&state);
|
||||||
|
let user: Option<UsersItemDtoRaw> =
|
||||||
|
match repository.query_user_by_email(email).await {
|
||||||
|
Ok(user) => Some(user),
|
||||||
|
Err(err) => {
|
||||||
|
return Ok(common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&err.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if user.is_none() {
|
||||||
|
return Ok(common_response(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Unauthorized user",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
req.extensions_mut().insert(user.unwrap());
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cors_middleware() -> CorsLayer {
|
||||||
|
let env = Env::new();
|
||||||
|
let cors_origins = match env.rust_env.as_str() {
|
||||||
|
"development" => vec!["http://localhost:3000"],
|
||||||
|
"production" => {
|
||||||
|
vec!["https://gacha.imphnen.dev", "https://imphnen.dev"]
|
||||||
|
}
|
||||||
|
_ => vec![
|
||||||
|
"http://localhost:3000",
|
||||||
|
"https://gacha.imphnen.dev",
|
||||||
|
"https://imphnen.dev",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let allowed_origins: Vec<HeaderValue> = cors_origins
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|origin| origin.parse::<HeaderValue>().ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin(allowed_origins)
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
||||||
|
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
|
||||||
|
.allow_credentials(true)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-iam-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
|
||||||
|
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
|
||||||
|
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
|
||||||
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
surrealdb.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
utoipa-swagger-ui.workspace = true
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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 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),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
use super::{
|
||||||
|
AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto,
|
||||||
|
AuthResendOtpRequestDto, AuthService, AuthVerifyEmailRequestDto,
|
||||||
|
};
|
||||||
|
use crate::{v1::AuthLoginResponsetDto, AppState};
|
||||||
|
use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto};
|
||||||
|
use axum::{response::IntoResponse, Extension, Json};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/login",
|
||||||
|
request_body = AuthLoginRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Login successful", body = ResponseSuccessDto<AuthLoginResponsetDto>),
|
||||||
|
(status = 401, description = "Login failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_login(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthLoginRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_login(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/register",
|
||||||
|
request_body = AuthRegisterRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Register successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Register failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_register(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthRegisterRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_register(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/verify-email",
|
||||||
|
request_body = AuthVerifyEmailRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Verify email successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Verify email failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_verify_email(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthVerifyEmailRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_verify_email(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/send-otp",
|
||||||
|
request_body = AuthResendOtpRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Resend otp successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Resend otp failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_resend_otp(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthResendOtpRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_resend_otp(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/forgot",
|
||||||
|
request_body = AuthResendOtpRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Forgot password request successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Forgot password request failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_forgot_password(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthResendOtpRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_forgot_password(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/new-password",
|
||||||
|
request_body = AuthNewPasswordRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "New password request successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "New password request failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_new_password(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthNewPasswordRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_new_password(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/refresh",
|
||||||
|
request_body = AuthRefreshTokenRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Refresh token request successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Refresh token request failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_refresh_token(
|
||||||
|
Json(payload): Json<AuthRefreshTokenRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_refresh_token(payload).await
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
use crate::RolesItemDto;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref PASSWORD_REGEX: Regex = Regex::new(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AuthUserItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub role: RolesItemDto,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AuthLoginResponsetDto {
|
||||||
|
pub token: TokenDto,
|
||||||
|
pub user: AuthUserItemDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 student_type: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 10,
|
||||||
|
message = "Phone number at least have 10 character"
|
||||||
|
))]
|
||||||
|
pub phone_number: String,
|
||||||
|
#[validate(length(
|
||||||
|
max = 4,
|
||||||
|
message = "Referal code cannot be more than 4 character"
|
||||||
|
))]
|
||||||
|
pub referral_code: Option<String>,
|
||||||
|
pub referred_by: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct AuthActiveInactiveRequestDto {
|
||||||
|
pub is_active: bool,
|
||||||
|
#[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 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 {
|
||||||
|
pub token: String,
|
||||||
|
#[validate(length(min = 1, message = "Token cannot be empty"))]
|
||||||
|
#[validate(regex(
|
||||||
|
path = "PASSWORD_REGEX",
|
||||||
|
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(regex(
|
||||||
|
path = "PASSWORD_REGEX",
|
||||||
|
message = "Password must include uppercase, lowercase, number, and special character"
|
||||||
|
))]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use super::AuthOtpSchema;
|
||||||
|
use crate::{make_thing, AppState, ResourceEnum, UsersItemDtoRaw};
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
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: UsersItemDtoRaw) -> 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<UsersItemDtoRaw>>((table.clone(), user_id.clone()))
|
||||||
|
.await?;
|
||||||
|
let mut user_to_store = user.clone();
|
||||||
|
user_to_store.id = id.clone();
|
||||||
|
let record: Option<UsersItemDtoRaw> = 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<UsersItemDtoRaw> {
|
||||||
|
let user: Option<UsersItemDtoRaw> = 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<UsersItemDtoRaw> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.delete((ResourceEnum::UsersCache.to_string(), email))
|
||||||
|
.await?;
|
||||||
|
dbg!(record.clone());
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete stored user".to_string()),
|
||||||
|
None => bail!("Failed delete stored user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_get_stored_otp(&self, email: String) -> Result<u32> {
|
||||||
|
let table = ResourceEnum::OtpCache.to_string();
|
||||||
|
let key = (table.as_str(), email.as_str());
|
||||||
|
let result: Option<AuthOtpSchema> = self.state.surrealdb_mem.select(key).await?;
|
||||||
|
match result {
|
||||||
|
Some(data) => match Utc::now() > data.expires_at {
|
||||||
|
true => {
|
||||||
|
let _ = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.delete::<Option<AuthOtpSchema>>(key)
|
||||||
|
.await?;
|
||||||
|
Err(anyhow!("OTP expired"))
|
||||||
|
}
|
||||||
|
false => Ok(data.otp),
|
||||||
|
},
|
||||||
|
None => bail!("No stored OTP found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_store_otp(&self, email: String, otp: u32) -> Result<String> {
|
||||||
|
let expires_at = Utc::now() + Duration::seconds(300);
|
||||||
|
let table: String = ResourceEnum::OtpCache.to_string();
|
||||||
|
let record: Option<AuthOtpSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.create((table.as_str(), email.as_str()))
|
||||||
|
.content(AuthOtpSchema { otp, expires_at })
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success store otp".to_string()),
|
||||||
|
None => bail!("Failed store otp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_stored_otp(&self, email: String) -> Result<String> {
|
||||||
|
let record: Option<AuthOtpSchema> = self
|
||||||
|
.state
|
||||||
|
.surrealdb_mem
|
||||||
|
.delete((ResourceEnum::OtpCache.to_string(), email))
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete stored otp".to_string()),
|
||||||
|
None => bail!("Failed delete stored otp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AuthOtpSchema {
|
||||||
|
pub otp: u32,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
use super::{
|
||||||
|
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
|
||||||
|
AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthRepository,
|
||||||
|
AuthResendOtpRequestDto, AuthUserItemDto, AuthVerifyEmailRequestDto, TokenDto,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
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, AppState, Env, ResourceEnum, ResponseSuccessDto, RolesEnum,
|
||||||
|
RolesItemDto, RolesRepository, UsersActiveInactiveSchema, UsersRepository,
|
||||||
|
UsersSchema, UsersSetNewPasswordSchema,
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
let role_repo = RolesRepository::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 role_response = role_repo
|
||||||
|
.query_role_by_id(user.role.id.id.to_raw())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = ResponseSuccessDto {
|
||||||
|
data: AuthLoginResponsetDto {
|
||||||
|
user: AuthUserItemDto {
|
||||||
|
id: user.id.id.to_raw(),
|
||||||
|
fullname: user.fullname.clone(),
|
||||||
|
email: user.email.clone(),
|
||||||
|
is_active: user.is_active.clone(),
|
||||||
|
avatar: user.avatar.clone(),
|
||||||
|
phone_number: user.phone_number.clone(),
|
||||||
|
gender: user.gender.clone(),
|
||||||
|
birthdate: user.birthdate.clone(),
|
||||||
|
role: RolesItemDto {
|
||||||
|
id: role_response.id,
|
||||||
|
name: role_response.name,
|
||||||
|
permissions: role_response.permissions,
|
||||||
|
created_at: role_response.created_at,
|
||||||
|
updated_at: role_response.updated_at,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
student_type: payload.student_type,
|
||||||
|
phone_number: payload.phone_number,
|
||||||
|
referral_code: payload.referral_code,
|
||||||
|
referred_by: payload.referred_by,
|
||||||
|
};
|
||||||
|
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();
|
||||||
|
match auth_repo.query_get_stored_otp(email.clone()).await {
|
||||||
|
Ok(stored_otp) => match stored_otp == payload.otp {
|
||||||
|
true => match user_repo
|
||||||
|
.query_active_inactive_user(
|
||||||
|
email.clone(),
|
||||||
|
UsersActiveInactiveSchema { is_active: true },
|
||||||
|
)
|
||||||
|
.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 user_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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match user_repo
|
||||||
|
.query_update_password_user(email, UsersSetNewPasswordSchema { password })
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use axum::{Router, routing::post};
|
||||||
|
|
||||||
|
pub mod auth_controller;
|
||||||
|
pub mod auth_dto;
|
||||||
|
pub mod auth_repository;
|
||||||
|
pub mod auth_schema;
|
||||||
|
pub mod auth_service;
|
||||||
|
|
||||||
|
pub use auth_dto::*;
|
||||||
|
pub use auth_repository::*;
|
||||||
|
pub use auth_schema::*;
|
||||||
|
pub use auth_service::*;
|
||||||
|
|
||||||
|
pub fn auth_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/forgot", post(auth_controller::post_forgot_password))
|
||||||
|
.route("/login", post(auth_controller::post_login))
|
||||||
|
.route("/new-password", post(auth_controller::post_new_password))
|
||||||
|
.route("/refresh", post(auth_controller::post_refresh_token))
|
||||||
|
.route("/register", post(auth_controller::post_register))
|
||||||
|
.route("/send-otp", post(auth_controller::post_resend_otp))
|
||||||
|
.route("/verify-email", post(auth_controller::post_verify_email))
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
pub mod auth;
|
||||||
|
pub mod permissions;
|
||||||
|
pub mod roles;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
pub use auth::*;
|
||||||
|
pub use permissions::*;
|
||||||
|
pub use roles::*;
|
||||||
|
pub use users::*;
|
||||||
|
|
||||||
|
pub fn iam_public_routes() -> Router {
|
||||||
|
Router::new().nest("/auth", auth_router())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iam_protected_routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.nest("/users", users_router())
|
||||||
|
.nest("/roles", roles_router())
|
||||||
|
.nest("/permissions", permissions_router())
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
};
|
||||||
|
pub mod permissions_controller;
|
||||||
|
pub mod permissions_dto;
|
||||||
|
pub mod permissions_enum;
|
||||||
|
pub mod permissions_guard;
|
||||||
|
pub mod permissions_repository;
|
||||||
|
pub mod permissions_schema;
|
||||||
|
pub mod permissions_service;
|
||||||
|
|
||||||
|
pub use permissions_controller::*;
|
||||||
|
pub use permissions_dto::*;
|
||||||
|
pub use permissions_enum::*;
|
||||||
|
pub use permissions_guard::*;
|
||||||
|
pub use permissions_repository::*;
|
||||||
|
pub use permissions_schema::*;
|
||||||
|
|
||||||
|
pub fn permissions_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_permission_list))
|
||||||
|
.route("/create", post(post_create_permission))
|
||||||
|
.route("/detail/{id}", get(get_permission_by_id))
|
||||||
|
.route("/update/{id}", put(put_update_permission))
|
||||||
|
.route("/delete/{id}", delete(delete_permission))
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, Query},
|
||||||
|
response::IntoResponse,
|
||||||
|
Extension, Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
v1::{
|
||||||
|
permissions_dto::{PermissionsItemDto, PermissionsRequestDto},
|
||||||
|
permissions_service::PermissionsService,
|
||||||
|
},
|
||||||
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
|
ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{permissions_guard, PermissionsEnum};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/permissions",
|
||||||
|
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 permission list", body = ResponseListSuccessDto<Vec<PermissionsItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Permissions"
|
||||||
|
)]
|
||||||
|
pub async fn get_permission_list(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(meta): Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadListPermissions],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => PermissionsService::get_permission_list(&state, meta).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/permissions/detail/{id}",
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
params(("id" = String, Path, description = "Permission ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get permission by ID", body = ResponseSuccessDto<PermissionsItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Permissions"
|
||||||
|
)]
|
||||||
|
pub async fn get_permission_by_id(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadDetailPermissions],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => PermissionsService::get_permission_by_id(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/permissions/create",
|
||||||
|
request_body = PermissionsRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new permission", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Permissions"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_permission(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<PermissionsRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::CreatePermissions],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => PermissionsService::create_role(&state, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/permissions/update/{id}",
|
||||||
|
request_body = PermissionsRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update permission", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Permissions"
|
||||||
|
)]
|
||||||
|
pub async fn put_update_permission(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<PermissionsRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::UpdatePermissions],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => PermissionsService::update_permission(&state, payload, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/permissions/delete/{id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Delete permission", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Permissions"
|
||||||
|
)]
|
||||||
|
pub async fn delete_permission(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::DeletePermissions],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => PermissionsService::delete_permission(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct PermissionsRequestDto {
|
||||||
|
#[validate(length(min = 1, message = "Permission name must not be empty"))]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct PermissionsItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PermissionsItemDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PermissionsEnum {
|
||||||
|
ReadListUsers,
|
||||||
|
ReadDetailUsers,
|
||||||
|
CreateUsers,
|
||||||
|
DeleteUsers,
|
||||||
|
UpdateUsers,
|
||||||
|
ReadListRoles,
|
||||||
|
ReadDetailRoles,
|
||||||
|
CreateRoles,
|
||||||
|
DeleteRoles,
|
||||||
|
UpdateRoles,
|
||||||
|
ReadListPermissions,
|
||||||
|
ReadDetailPermissions,
|
||||||
|
CreatePermissions,
|
||||||
|
DeletePermissions,
|
||||||
|
UpdatePermissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PermissionsEnum {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let permission_str = match self {
|
||||||
|
PermissionsEnum::ReadListUsers => "Read List Users",
|
||||||
|
PermissionsEnum::ReadDetailUsers => "Read Detail Users",
|
||||||
|
PermissionsEnum::CreateUsers => "Create Users",
|
||||||
|
PermissionsEnum::DeleteUsers => "Delete Users",
|
||||||
|
PermissionsEnum::UpdateUsers => "Update Users",
|
||||||
|
PermissionsEnum::ReadListRoles => "Read List Roles",
|
||||||
|
PermissionsEnum::ReadDetailRoles => "Read Detail Roles",
|
||||||
|
PermissionsEnum::CreateRoles => "Create Roles",
|
||||||
|
PermissionsEnum::DeleteRoles => "Delete Roles",
|
||||||
|
PermissionsEnum::UpdateRoles => "Update Roles",
|
||||||
|
PermissionsEnum::ReadListPermissions => "Read List Permissions",
|
||||||
|
PermissionsEnum::ReadDetailPermissions => "Read Detail Permissions",
|
||||||
|
PermissionsEnum::CreatePermissions => "Create Permissions",
|
||||||
|
PermissionsEnum::DeletePermissions => "Delete Permissions",
|
||||||
|
PermissionsEnum::UpdatePermissions => "Update Permissions",
|
||||||
|
};
|
||||||
|
write!(f, "{}", permission_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use super::PermissionsEnum;
|
||||||
|
use crate::{common_response, extract_email, AppState, AuthRepository};
|
||||||
|
use axum::{
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn permissions_guard(
|
||||||
|
headers: &HeaderMap,
|
||||||
|
state: AppState,
|
||||||
|
required_permissions: Vec<PermissionsEnum>,
|
||||||
|
) -> Result<(), Response> {
|
||||||
|
let auth_repo = AuthRepository::new(&state);
|
||||||
|
let email = extract_email(headers).ok_or_else(|| {
|
||||||
|
common_response(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid or missing authorization token",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let raw_user = auth_repo
|
||||||
|
.query_get_stored_user(email.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
common_response(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"User session expired or not found",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let role = raw_user.role;
|
||||||
|
let role_permissions: Vec<String> =
|
||||||
|
role.permissions.into_iter().map(|perm| perm.name).collect();
|
||||||
|
let has_all_permissions = required_permissions
|
||||||
|
.iter()
|
||||||
|
.all(|required| role_permissions.contains(&required.to_string()));
|
||||||
|
if !has_all_permissions {
|
||||||
|
return Err(common_response(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You don't have the required permissions",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use super::PermissionsSchema;
|
||||||
|
use crate::{
|
||||||
|
get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto, ResourceEnum,
|
||||||
|
ResponseListSuccessDto,
|
||||||
|
};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
|
pub struct PermissionsRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PermissionsRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_permission_list(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<PermissionsSchema>>> {
|
||||||
|
let mut conditions = vec!["is_deleted = false".into()];
|
||||||
|
if meta.search.is_some() {
|
||||||
|
conditions.push("string::contains(name, $search)".into());
|
||||||
|
}
|
||||||
|
if meta.filter_by.is_some() && meta.filter.is_some() {
|
||||||
|
let filter_by = meta.filter_by.as_ref().unwrap();
|
||||||
|
conditions.push(format!("{} = $filter", filter_by));
|
||||||
|
}
|
||||||
|
query_list_with_meta(
|
||||||
|
&self.state.surrealdb_ws,
|
||||||
|
&ResourceEnum::Permissions.to_string(),
|
||||||
|
&meta,
|
||||||
|
conditions,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_permission_by_id(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
) -> Result<PermissionsSchema> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let result: Option<PermissionsSchema> = db
|
||||||
|
.select((ResourceEnum::Permissions.to_string(), id.clone()))
|
||||||
|
.await?;
|
||||||
|
match result {
|
||||||
|
Some(permission) if !permission.is_deleted => Ok(permission),
|
||||||
|
_ => bail!("Permission not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_permission_by_name(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
) -> Result<PermissionsSchema> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT * FROM {} WHERE name = $name AND is_deleted = false",
|
||||||
|
ResourceEnum::Permissions.to_string()
|
||||||
|
);
|
||||||
|
let result: Vec<PermissionsSchema> =
|
||||||
|
db.query(sql).bind(("name", name.clone())).await?.take(0)?;
|
||||||
|
if let Some(permission) = result.into_iter().next() {
|
||||||
|
Ok(permission.into())
|
||||||
|
} else {
|
||||||
|
bail!("Permission not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_create_permission(
|
||||||
|
&self,
|
||||||
|
data: PermissionsSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<PermissionsSchema> = db
|
||||||
|
.create(ResourceEnum::Permissions.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success create permission".into()),
|
||||||
|
None => bail!("Failed to create permission"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_update_permission(
|
||||||
|
&self,
|
||||||
|
data: PermissionsSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(&data.id)?;
|
||||||
|
let existing = self.query_permission_by_id(data.id.id.to_raw()).await?;
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Permission already deleted");
|
||||||
|
}
|
||||||
|
let merged = PermissionsSchema {
|
||||||
|
created_at: existing.created_at,
|
||||||
|
..data.clone()
|
||||||
|
};
|
||||||
|
let record: Option<PermissionsSchema> =
|
||||||
|
db.update(record_key).merge(merged).await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update permission".into()),
|
||||||
|
None => bail!("Failed to update permission"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_permission(&self, id: String) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let permission_id = make_thing(&ResourceEnum::Permissions.to_string(), &id);
|
||||||
|
let permission = self
|
||||||
|
.query_permission_by_id(permission_id.id.to_raw())
|
||||||
|
.await?;
|
||||||
|
if permission.is_deleted {
|
||||||
|
bail!("Permission already deleted");
|
||||||
|
}
|
||||||
|
let record_key = get_id(&permission.id)?;
|
||||||
|
let record: Option<PermissionsSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete permission".into()),
|
||||||
|
None => bail!("Failed to delete permission"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use crate::{make_thing, ResourceEnum};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{sql::Thing, Uuid};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PermissionsSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PermissionsSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
PermissionsSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Permissions.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
name: String::new(),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
use crate::{
|
||||||
|
common_response, make_thing, success_list_response, success_response,
|
||||||
|
validate_request, AppState, MetaRequestDto, PermissionsRepository,
|
||||||
|
PermissionsSchema, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::Response;
|
||||||
|
|
||||||
|
use super::PermissionsRequestDto;
|
||||||
|
|
||||||
|
pub struct PermissionsService;
|
||||||
|
|
||||||
|
impl PermissionsService {
|
||||||
|
pub async fn get_permission_list(
|
||||||
|
state: &AppState,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = PermissionsRepository::new(state);
|
||||||
|
match repo.query_permission_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_permission_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = PermissionsRepository::new(state);
|
||||||
|
match repo.query_permission_by_id(id).await {
|
||||||
|
Ok(permission) => success_response(ResponseSuccessDto { data: permission }),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_role(
|
||||||
|
state: &AppState,
|
||||||
|
payload: PermissionsRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = PermissionsRepository::new(state);
|
||||||
|
match repo.query_permission_by_name(payload.name.clone()).await {
|
||||||
|
Ok(_role) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"Permission name already exists",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) if err.to_string().contains("not found") => {}
|
||||||
|
Err(e) => {
|
||||||
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match repo
|
||||||
|
.query_create_permission(PermissionsSchema {
|
||||||
|
name: payload.name,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_permission(
|
||||||
|
state: &AppState,
|
||||||
|
payload: PermissionsRequestDto,
|
||||||
|
id: String,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = PermissionsRepository::new(state);
|
||||||
|
match repo
|
||||||
|
.query_update_permission(PermissionsSchema {
|
||||||
|
id: make_thing(&ResourceEnum::Permissions.to_string(), &id),
|
||||||
|
name: payload.name,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => {
|
||||||
|
if e.to_string().contains("not found") {
|
||||||
|
common_response(StatusCode::NOT_FOUND, "Permission not found")
|
||||||
|
} else {
|
||||||
|
common_response(StatusCode::BAD_REQUEST, &e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_permission(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = PermissionsRepository::new(state);
|
||||||
|
match repo.query_delete_permission(id).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => {
|
||||||
|
if e.to_string().contains("not found") {
|
||||||
|
common_response(StatusCode::NOT_FOUND, "Permission not found")
|
||||||
|
} else {
|
||||||
|
common_response(StatusCode::BAD_REQUEST, &e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod roles_controller;
|
||||||
|
pub mod roles_dto;
|
||||||
|
pub mod roles_enum;
|
||||||
|
pub mod roles_repository;
|
||||||
|
pub mod roles_schema;
|
||||||
|
pub mod roles_service;
|
||||||
|
|
||||||
|
pub use roles_controller::*;
|
||||||
|
pub use roles_dto::*;
|
||||||
|
pub use roles_enum::*;
|
||||||
|
pub use roles_repository::*;
|
||||||
|
pub use roles_schema::*;
|
||||||
|
pub use roles_service::*;
|
||||||
|
|
||||||
|
pub fn roles_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_role_list))
|
||||||
|
.route("/detail/{id}", get(get_role_by_id))
|
||||||
|
.route("/create", post(post_create_role))
|
||||||
|
.route("/update/{id}", put(put_update_role))
|
||||||
|
.route("/delete/{id}", delete(delete_role))
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, Query},
|
||||||
|
response::IntoResponse,
|
||||||
|
Extension, Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{RolesItemDto, RolesRequestCreateDto, RolesRequestUpdateDto};
|
||||||
|
use crate::{
|
||||||
|
permissions_guard, v1::roles_service::RolesService, AppState, MessageResponseDto,
|
||||||
|
MetaRequestDto, PermissionsEnum, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/roles",
|
||||||
|
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 role list", body = ResponseListSuccessDto<Vec<RolesItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Roles"
|
||||||
|
)]
|
||||||
|
pub async fn get_role_list(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(meta): Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadListRoles],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => RolesService::get_role_list(&state, meta).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/roles/detail/{id}",
|
||||||
|
params(("id" = String, Path, description = "Role ID")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get role by ID", body = ResponseSuccessDto<RolesItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Roles"
|
||||||
|
)]
|
||||||
|
pub async fn get_role_by_id(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadDetailRoles],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => RolesService::get_role_by_id(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/roles/create",
|
||||||
|
request_body = RolesRequestCreateDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new role", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Roles"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_role(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<RolesRequestCreateDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::CreateRoles],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => RolesService::create_role(&state, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/roles/update/{id}",
|
||||||
|
request_body = RolesRequestUpdateDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update role", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Roles"
|
||||||
|
)]
|
||||||
|
pub async fn put_update_role(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<RolesRequestUpdateDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::UpdateRoles],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => RolesService::update_role(&state, id, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/roles/delete/{id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Delete role", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Roles"
|
||||||
|
)]
|
||||||
|
pub async fn delete_role(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::DeleteRoles],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => RolesService::delete_role(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::{PermissionsItemDto, PermissionsItemDtoRaw};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct RolesRequestUpdateDto {
|
||||||
|
#[validate(length(min = 1, message = "Role name must not be empty"))]
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub permissions: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct RolesRequestCreateDto {
|
||||||
|
#[validate(length(min = 1, message = "Role name must not be empty"))]
|
||||||
|
pub name: String,
|
||||||
|
pub permissions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RolesItemListDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RolesItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub permissions: Vec<PermissionsItemDto>,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RolesItemByIdDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub permissions: Vec<PermissionsItemDto>,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RolesItemByIdDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub permissions: Vec<PermissionsItemDtoRaw>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RolesItemDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub permissions: Vec<PermissionsItemDtoRaw>,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum RolesEnum {
|
||||||
|
Admin,
|
||||||
|
User,
|
||||||
|
Staf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RolesEnum {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let roles_str = match self {
|
||||||
|
RolesEnum::Admin => "Admin",
|
||||||
|
RolesEnum::User => "User",
|
||||||
|
RolesEnum::Staf => "Staf",
|
||||||
|
};
|
||||||
|
write!(f, "{}", roles_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use super::{
|
||||||
|
RolesItemByIdDto, RolesItemByIdDtoRaw, RolesRequestCreateDto,
|
||||||
|
RolesRequestUpdateDto, RolesSchema,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
extract_id, get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto,
|
||||||
|
PermissionsItemDto, ResourceEnum, ResponseListSuccessDto,
|
||||||
|
};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use surrealdb::Uuid;
|
||||||
|
|
||||||
|
pub struct RolesRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RolesRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_raw_role_by_id(&self, id: &str) -> Result<RolesSchema> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let role: Option<RolesSchema> =
|
||||||
|
db.select((ResourceEnum::Roles.to_string(), id)).await?;
|
||||||
|
match role {
|
||||||
|
Some(r) if !r.is_deleted => Ok(r),
|
||||||
|
_ => bail!("Role not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_role_list(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<RolesSchema>>> {
|
||||||
|
let mut conditions = vec!["is_deleted = false".into()];
|
||||||
|
if meta.search.is_some() {
|
||||||
|
conditions.push("string::contains(name, $search)".into());
|
||||||
|
}
|
||||||
|
if meta.filter_by.is_some() && meta.filter.is_some() {
|
||||||
|
let filter_by = meta.filter_by.as_ref().unwrap();
|
||||||
|
conditions.push(format!("{} = $filter", filter_by));
|
||||||
|
}
|
||||||
|
query_list_with_meta(
|
||||||
|
&self.state.surrealdb_ws,
|
||||||
|
&ResourceEnum::Roles.to_string(),
|
||||||
|
&meta,
|
||||||
|
conditions,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_role_by_name(&self, name: String) -> Result<RolesItemByIdDto> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT *, permissions FROM {} WHERE name = $name AND is_deleted = false LIMIT 1 FETCH permissions",
|
||||||
|
ResourceEnum::Roles.to_string()
|
||||||
|
);
|
||||||
|
let mut result = db.query(sql).bind(("name", name.clone())).await?;
|
||||||
|
let role: Option<RolesItemByIdDtoRaw> = result.take(0)?;
|
||||||
|
let role = match role {
|
||||||
|
Some(r) if !r.is_deleted => r,
|
||||||
|
_ => bail!("Role not found"),
|
||||||
|
};
|
||||||
|
let permissions = role
|
||||||
|
.permissions
|
||||||
|
.into_iter()
|
||||||
|
.map(|perm| PermissionsItemDto {
|
||||||
|
id: extract_id(&perm.id),
|
||||||
|
name: perm.name,
|
||||||
|
created_at: perm.created_at,
|
||||||
|
updated_at: perm.updated_at,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(RolesItemByIdDto {
|
||||||
|
id: extract_id(&role.id),
|
||||||
|
name: role.name,
|
||||||
|
is_deleted: role.is_deleted,
|
||||||
|
permissions,
|
||||||
|
created_at: role.created_at,
|
||||||
|
updated_at: role.updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_role_by_id(&self, id: String) -> Result<RolesItemByIdDto> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let query = format!(
|
||||||
|
"SELECT *, permissions.* AS permissions
|
||||||
|
FROM app_roles:⟨{}⟩ WHERE is_deleted = false FETCH permissions",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
let mut result = db.query(query).await?;
|
||||||
|
let role: Option<RolesItemByIdDtoRaw> = result.take(0)?;
|
||||||
|
let role = match role {
|
||||||
|
Some(r) if !r.is_deleted => r,
|
||||||
|
_ => bail!("Role not found"),
|
||||||
|
};
|
||||||
|
let permissions = role
|
||||||
|
.permissions
|
||||||
|
.into_iter()
|
||||||
|
.map(|perm| PermissionsItemDto {
|
||||||
|
id: extract_id(&perm.id),
|
||||||
|
name: perm.name,
|
||||||
|
created_at: perm.created_at,
|
||||||
|
updated_at: perm.updated_at,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(RolesItemByIdDto {
|
||||||
|
id: extract_id(&role.id),
|
||||||
|
name: role.name,
|
||||||
|
is_deleted: role.is_deleted,
|
||||||
|
permissions,
|
||||||
|
created_at: role.created_at,
|
||||||
|
updated_at: role.updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_create_role(
|
||||||
|
&self,
|
||||||
|
payload: RolesRequestCreateDto,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
|
||||||
|
let role_id = Uuid::new_v4().to_string();
|
||||||
|
let permission_things: Vec<Thing> = payload
|
||||||
|
.permissions
|
||||||
|
.iter()
|
||||||
|
.map(|id| make_thing(&ResourceEnum::Permissions.to_string(), id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let role = RolesSchema {
|
||||||
|
id: make_thing(&ResourceEnum::Roles.to_string(), &role_id),
|
||||||
|
name: payload.name,
|
||||||
|
is_deleted: false,
|
||||||
|
permissions: permission_things,
|
||||||
|
created_at: Some(crate::get_iso_date()),
|
||||||
|
updated_at: Some(crate::get_iso_date()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _: Option<RolesSchema> = db
|
||||||
|
.create((&ResourceEnum::Roles.to_string(), role_id))
|
||||||
|
.content(role)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok("Role with permissions created successfully".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_update_role(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
data: RolesRequestUpdateDto,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let thing_id = make_thing(&ResourceEnum::Roles.to_string(), &id);
|
||||||
|
let existing = self.query_raw_role_by_id(&id).await?;
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("Role already deleted");
|
||||||
|
}
|
||||||
|
let permissions: Vec<Thing> = if let Some(permission_ids) = &data.permissions {
|
||||||
|
permission_ids
|
||||||
|
.iter()
|
||||||
|
.map(|id| make_thing(&ResourceEnum::Permissions.to_string(), id))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
existing
|
||||||
|
.permissions
|
||||||
|
.iter()
|
||||||
|
.map(|p| make_thing(&ResourceEnum::Permissions.to_string(), &p.id.to_raw()))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let merged = RolesSchema {
|
||||||
|
id: thing_id,
|
||||||
|
name: data.name.unwrap_or(existing.name),
|
||||||
|
permissions,
|
||||||
|
is_deleted: existing.is_deleted,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: Some(crate::get_iso_date()),
|
||||||
|
};
|
||||||
|
let record: Option<RolesSchema> =
|
||||||
|
db.update(get_id(&merged.id)?).content(merged).await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update role".into()),
|
||||||
|
None => bail!("Failed to update role"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_role(&self, id: String) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let role_id = make_thing(&ResourceEnum::Roles.to_string(), &id);
|
||||||
|
let role = self.query_role_by_id(role_id.id.to_raw()).await?;
|
||||||
|
if role.is_deleted {
|
||||||
|
bail!("Role already deleted");
|
||||||
|
}
|
||||||
|
let record_key = get_id(&role_id)?;
|
||||||
|
let record: Option<RolesSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete role".into()),
|
||||||
|
None => bail!("Failed to delete role"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{sql::Thing, Uuid};
|
||||||
|
|
||||||
|
use crate::{make_thing, ResourceEnum};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RolesSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub name: String,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub permissions: Vec<Thing>,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RolesSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
RolesSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Roles.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
permissions: vec![make_thing(
|
||||||
|
&ResourceEnum::Permissions.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
)],
|
||||||
|
name: String::new(),
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
use super::{RolesRepository, RolesRequestCreateDto, RolesRequestUpdateDto};
|
||||||
|
use crate::{
|
||||||
|
common_response, success_list_response, success_response, validate_request,
|
||||||
|
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
|
||||||
|
pub struct RolesService;
|
||||||
|
|
||||||
|
impl RolesService {
|
||||||
|
pub async fn get_role_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||||
|
let repo = RolesRepository::new(state);
|
||||||
|
match repo.query_role_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_role_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = RolesRepository::new(state);
|
||||||
|
match repo.query_role_by_id(id).await {
|
||||||
|
Ok(role) => success_response(ResponseSuccessDto { data: role }),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_role(
|
||||||
|
state: &AppState,
|
||||||
|
payload: RolesRequestCreateDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = RolesRepository::new(state);
|
||||||
|
match repo.query_role_by_name(payload.name.clone()).await {
|
||||||
|
Ok(_role) => {
|
||||||
|
return common_response(StatusCode::CONFLICT, "Role name already exists");
|
||||||
|
}
|
||||||
|
Err(err) if err.to_string().contains("not found") => {}
|
||||||
|
Err(e) => {
|
||||||
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match repo.query_create_role(payload).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_role(
|
||||||
|
state: &AppState,
|
||||||
|
id: String,
|
||||||
|
payload: RolesRequestUpdateDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&payload) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = RolesRepository::new(state);
|
||||||
|
let existing_role = match repo.query_role_by_id(id.clone()).await {
|
||||||
|
Ok(role) => role,
|
||||||
|
Err(err) if err.to_string().contains("not found") => {
|
||||||
|
return common_response(StatusCode::NOT_FOUND, "Role not found");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(new_name) = payload.name.clone() {
|
||||||
|
match repo.query_role_by_name(new_name.clone()).await {
|
||||||
|
Ok(role_with_same_name) => {
|
||||||
|
if role_with_same_name.id != existing_role.id {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"Role name already exists",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) if err.to_string().contains("not found") => {}
|
||||||
|
Err(e) => {
|
||||||
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match repo.query_update_role(id, payload).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_role(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = RolesRepository::new(state);
|
||||||
|
match repo.query_role_by_id(id.clone()).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) if err.to_string().contains("not found") => {
|
||||||
|
return common_response(StatusCode::NOT_FOUND, "Role not found");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match repo.query_delete_role(id).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod users_controller;
|
||||||
|
pub mod users_dto;
|
||||||
|
pub mod users_repository;
|
||||||
|
pub mod users_schema;
|
||||||
|
pub mod users_service;
|
||||||
|
|
||||||
|
pub use users_controller::*;
|
||||||
|
pub use users_dto::*;
|
||||||
|
pub use users_repository::*;
|
||||||
|
pub use users_schema::*;
|
||||||
|
pub use users_service::*;
|
||||||
|
|
||||||
|
pub fn users_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_user_list))
|
||||||
|
.route("/activate/{id}", put(patch_user_active_status))
|
||||||
|
.route("/create", post(post_create_user))
|
||||||
|
.route("/me", get(get_user_me))
|
||||||
|
.route("/delete/{id}", delete(delete_user))
|
||||||
|
.route("/detail/{id}", get(get_user_by_id))
|
||||||
|
.route("/update/{id}", put(put_update_user))
|
||||||
|
.route("/update/me", put(put_update_user_me))
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
use axum::extract::{Path, Query};
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
permissions_guard, MessageResponseDto, PermissionsEnum, ResponseListSuccessDto,
|
||||||
|
ResponseSuccessDto, UsersActiveInactiveRequestDto, UsersCreateRequestDto,
|
||||||
|
UsersDetailItemDto,
|
||||||
|
};
|
||||||
|
use crate::{v1::users_service::UsersService, AppState, MetaRequestDto};
|
||||||
|
|
||||||
|
use super::{UsersListItemDto, UsersUpdateRequestDto};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users",
|
||||||
|
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 user list", body = ResponseListSuccessDto<Vec<UsersListItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn get_user_list(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(meta): Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadListUsers],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => UsersService::get_user_list(&state, meta).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "User ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get user by ID", body = ResponseSuccessDto<UsersDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn get_user_by_id(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::ReadDetailUsers],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => UsersService::get_user_by_id(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/me",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get user by ID", body = ResponseSuccessDto<UsersDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn get_user_me(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(&headers, state.clone(), vec![]).await {
|
||||||
|
Ok(_) => UsersService::get_user_me(headers, &state).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/create",
|
||||||
|
request_body = UsersCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new user", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_user(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<UsersCreateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::CreateUsers],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => UsersService::create_user(&state, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/update/{id}",
|
||||||
|
request_body = UsersUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update user", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn put_update_user(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<UsersUpdateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::UpdateUsers],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => UsersService::update_user(&state, id, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/update/me",
|
||||||
|
request_body = UsersUpdateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update user me", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn put_update_user_me(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<UsersUpdateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(&headers, state.clone(), vec![]).await {
|
||||||
|
Ok(_) => UsersService::update_user_me(&state, headers, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/activate/{id}",
|
||||||
|
request_body = UsersActiveInactiveRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Set user active/inactive", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn patch_user_active_status(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<UsersActiveInactiveRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::UpdateUsers],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => UsersService::set_user_active_status(&state, id, payload).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/users/delete/{id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Soft delete user", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn delete_user(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
&headers,
|
||||||
|
state.clone(),
|
||||||
|
vec![PermissionsEnum::DeleteUsers],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => UsersService::delete_user(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::{RolesItemDto, RolesItemDtoRaw};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UsersActiveInactiveRequestDto {
|
||||||
|
pub is_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct UsersCreateRequestDto {
|
||||||
|
#[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(regex(
|
||||||
|
path = "PASSWORD_REGEX",
|
||||||
|
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 = 10,
|
||||||
|
message = "Phone number at least have 10 character"
|
||||||
|
))]
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub role_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct UsersUpdateRequestDto {
|
||||||
|
#[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(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||||
|
pub fullname: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 10,
|
||||||
|
message = "Phone number at least have 10 character"
|
||||||
|
))]
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
#[validate(length(min = 1, message = "Gender is required"))]
|
||||||
|
pub gender: Option<String>,
|
||||||
|
#[validate(length(min = 1, message = "Birthdate is required"))]
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
#[validate(length(min = 1, message = "Avatar is required"))]
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub role_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UsersItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub role: RolesItemDto,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
pub password: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UsersDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub role: RolesItemDto,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UsersListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub role: String,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UsersListItemDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub role: Option<String>,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UsersItemDtoRaw {
|
||||||
|
pub id: Thing,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
pub role: RolesItemDtoRaw,
|
||||||
|
pub password: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
use super::{
|
||||||
|
UsersActiveInactiveSchema, UsersItemDto, UsersItemDtoRaw, UsersListItemDto,
|
||||||
|
UsersListItemDtoRaw, UsersSchema, UsersSetNewPasswordSchema,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
extract_id, get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto,
|
||||||
|
PermissionsItemDto, PermissionsItemDtoRaw, ResourceEnum, ResponseListSuccessDto,
|
||||||
|
RolesItemDto, RolesItemDtoRaw,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
|
||||||
|
pub struct UsersRepository<'a> {
|
||||||
|
state: &'a AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UsersRepository<'a> {
|
||||||
|
pub fn new(state: &'a AppState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_user_list(
|
||||||
|
&self,
|
||||||
|
meta: MetaRequestDto,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<UsersListItemDto>>> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let mut conditions = vec!["is_deleted = false".to_string()];
|
||||||
|
if let Some(search) = meta.search.as_deref() {
|
||||||
|
if !search.is_empty() {
|
||||||
|
conditions.push("string::contains(fullname ?? '', $search)".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let (Some(filter_by), Some(_filter)) =
|
||||||
|
(meta.filter_by.as_ref(), meta.filter.as_ref())
|
||||||
|
{
|
||||||
|
conditions.push(format!("{} = $filter", filter_by));
|
||||||
|
}
|
||||||
|
let where_clause = if !conditions.is_empty() {
|
||||||
|
format!("WHERE {}", conditions.join(" AND "))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let limit = meta.per_page.unwrap_or(10);
|
||||||
|
let page = meta.page.unwrap_or(1);
|
||||||
|
if page < 1 || limit < 1 {
|
||||||
|
return Err(anyhow!("Invalid pagination parameters"));
|
||||||
|
}
|
||||||
|
let start = (page - 1) * limit;
|
||||||
|
let select_query = format!(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
role.name AS role,
|
||||||
|
fullname,
|
||||||
|
email,
|
||||||
|
avatar,
|
||||||
|
phone_number,
|
||||||
|
is_active,
|
||||||
|
gender,
|
||||||
|
birthdate,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM {}
|
||||||
|
{}
|
||||||
|
LIMIT {} START {}
|
||||||
|
FETCH role
|
||||||
|
",
|
||||||
|
ResourceEnum::Users.to_string(),
|
||||||
|
where_clause,
|
||||||
|
limit,
|
||||||
|
start
|
||||||
|
);
|
||||||
|
let raw_result = query_list_with_meta::<UsersListItemDtoRaw>(
|
||||||
|
db,
|
||||||
|
&ResourceEnum::Users.to_string(),
|
||||||
|
&meta,
|
||||||
|
vec![],
|
||||||
|
Some(select_query),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let transformed_data = raw_result
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(|user| UsersListItemDto {
|
||||||
|
id: extract_id(&user.id),
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
avatar: user.avatar,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
is_active: user.is_active,
|
||||||
|
role: user.role.unwrap_or_else(|| "-".into()),
|
||||||
|
created_at: user.created_at,
|
||||||
|
updated_at: user.updated_at,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(ResponseListSuccessDto {
|
||||||
|
data: transformed_data,
|
||||||
|
meta: raw_result.meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_user_by_email(&self, email: String) -> Result<UsersItemDtoRaw> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT *, role AS role FROM {} WHERE email = $email AND is_deleted = false LIMIT 1 FETCH role, role.permissions",
|
||||||
|
ResourceEnum::Users.to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let response: Option<UsersItemDtoRaw> = db
|
||||||
|
.query(sql)
|
||||||
|
.bind(("email", email.clone()))
|
||||||
|
.await?
|
||||||
|
.take(0)?;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Some(user) if !user.role.is_deleted => {
|
||||||
|
let permissions = user
|
||||||
|
.role
|
||||||
|
.permissions
|
||||||
|
.into_iter()
|
||||||
|
.map(|perm| PermissionsItemDtoRaw {
|
||||||
|
id: perm.id,
|
||||||
|
name: perm.name,
|
||||||
|
created_at: perm.created_at,
|
||||||
|
updated_at: perm.updated_at,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(UsersItemDtoRaw {
|
||||||
|
id: user.id,
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
avatar: user.avatar,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
is_active: user.is_active,
|
||||||
|
is_deleted: user.is_deleted,
|
||||||
|
gender: user.gender,
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
password: user.password,
|
||||||
|
created_at: user.created_at,
|
||||||
|
updated_at: user.updated_at,
|
||||||
|
role: RolesItemDtoRaw {
|
||||||
|
id: user.role.id,
|
||||||
|
name: user.role.name,
|
||||||
|
permissions,
|
||||||
|
created_at: user.role.created_at,
|
||||||
|
updated_at: user.role.updated_at,
|
||||||
|
is_deleted: user.role.is_deleted,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => bail!("User not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_user_by_id(&self, id: String) -> Result<UsersItemDto> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"SELECT *, role AS role FROM app_users:⟨{}⟩ FETCH role, role.permissions",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut result = db.query(query).await?;
|
||||||
|
|
||||||
|
let response: Option<UsersItemDtoRaw> = result.take(0)?;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Some(user) if !user.role.is_deleted => {
|
||||||
|
let permissions = user
|
||||||
|
.role
|
||||||
|
.permissions
|
||||||
|
.into_iter()
|
||||||
|
.map(|perm| PermissionsItemDto {
|
||||||
|
id: extract_id(&perm.id),
|
||||||
|
name: perm.name,
|
||||||
|
created_at: perm.created_at,
|
||||||
|
updated_at: perm.updated_at,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(UsersItemDto {
|
||||||
|
id: extract_id(&user.id),
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
avatar: user.avatar,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
is_active: user.is_active,
|
||||||
|
is_deleted: user.is_deleted,
|
||||||
|
gender: user.gender,
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
password: user.password,
|
||||||
|
role: RolesItemDto {
|
||||||
|
id: extract_id(&user.role.id),
|
||||||
|
name: user.role.name,
|
||||||
|
permissions,
|
||||||
|
created_at: user.role.created_at,
|
||||||
|
updated_at: user.role.updated_at,
|
||||||
|
},
|
||||||
|
created_at: user.created_at,
|
||||||
|
updated_at: user.updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => bail!("User not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_create_user(&self, data: UsersSchema) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<UsersSchema> = db
|
||||||
|
.create(ResourceEnum::Users.to_string())
|
||||||
|
.content(data)
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success create user".into()),
|
||||||
|
None => bail!("Failed to create user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_update_user(&self, data: UsersSchema) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record_key = get_id(&data.id)?;
|
||||||
|
let existing = self.query_user_by_id(data.id.id.to_raw()).await?;
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("User already deleted");
|
||||||
|
}
|
||||||
|
let merged = UsersSchema {
|
||||||
|
password: existing.password,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
role: make_thing("app_roles", &existing.role.id),
|
||||||
|
..data.clone()
|
||||||
|
};
|
||||||
|
let record: Option<UsersSchema> = db.update(record_key).merge(merged).await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update user".into()),
|
||||||
|
None => bail!("Failed to update user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_active_inactive_user(
|
||||||
|
&self,
|
||||||
|
email: String,
|
||||||
|
data: UsersActiveInactiveSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let user = self.query_user_by_email(email.clone()).await?;
|
||||||
|
if user.is_deleted {
|
||||||
|
bail!("User already deleted");
|
||||||
|
}
|
||||||
|
let record_key = get_id(&user.id)?;
|
||||||
|
let record: Option<UsersSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.merge(UsersActiveInactiveSchema {
|
||||||
|
is_active: data.is_active,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update user".into()),
|
||||||
|
None => bail!("Failed to update user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_active_inactive_user_by_id(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
data: UsersActiveInactiveSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<UsersSchema> = db
|
||||||
|
.update((ResourceEnum::Users.to_string(), id))
|
||||||
|
.merge(UsersActiveInactiveSchema {
|
||||||
|
is_active: data.is_active,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update user".into()),
|
||||||
|
None => bail!("Failed to update user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_update_password_user(
|
||||||
|
&self,
|
||||||
|
email: String,
|
||||||
|
data: UsersSetNewPasswordSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let user = self.query_user_by_email(email).await?;
|
||||||
|
let record: Option<UsersSetNewPasswordSchema> = db
|
||||||
|
.update((ResourceEnum::Users.to_string(), user.id.id.to_raw()))
|
||||||
|
.merge(UsersSetNewPasswordSchema {
|
||||||
|
password: data.password.clone(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
dbg!(record.clone());
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update password user".into()),
|
||||||
|
None => bail!("Failed to update password user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_delete_user(&self, id: String) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let user_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
|
let user = self.query_user_by_id(user_id.id.to_raw()).await?;
|
||||||
|
if user.is_deleted {
|
||||||
|
bail!("User already deleted");
|
||||||
|
}
|
||||||
|
let id = make_thing(&ResourceEnum::Users.to_string(), &user.id);
|
||||||
|
let record_key = get_id(&id)?;
|
||||||
|
let record: Option<UsersSchema> = db
|
||||||
|
.update(record_key)
|
||||||
|
.merge(serde_json::json!({ "is_deleted": true }))
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success delete user".into()),
|
||||||
|
None => bail!("Failed to delete user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::{get_iso_date, make_thing, ResourceEnum};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{sql::Thing, Uuid};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UsersSchema {
|
||||||
|
pub id: Thing,
|
||||||
|
pub fullname: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub phone_number: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
pub role: Thing,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UsersSchema {
|
||||||
|
fn default() -> Self {
|
||||||
|
UsersSchema {
|
||||||
|
id: make_thing(
|
||||||
|
&ResourceEnum::Users.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
fullname: String::new(),
|
||||||
|
email: String::new(),
|
||||||
|
password: String::new(),
|
||||||
|
avatar: None,
|
||||||
|
phone_number: String::new(),
|
||||||
|
is_active: false,
|
||||||
|
is_deleted: false,
|
||||||
|
gender: None,
|
||||||
|
birthdate: None,
|
||||||
|
role: make_thing(
|
||||||
|
&ResourceEnum::Roles.to_string(),
|
||||||
|
&Uuid::new_v4().to_string(),
|
||||||
|
),
|
||||||
|
created_at: get_iso_date(),
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UsersSetNewPasswordSchema {
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UsersActiveInactiveSchema {
|
||||||
|
pub is_active: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
use crate::{
|
||||||
|
common_response, extract_email, get_iso_date, hash_password, make_thing,
|
||||||
|
success_list_response, success_response, validate_request, ResourceEnum,
|
||||||
|
ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
AppState, MetaRequestDto, ResponseListSuccessDto, UsersActiveInactiveSchema,
|
||||||
|
UsersRepository, UsersSchema, UsersSetNewPasswordSchema,
|
||||||
|
};
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
UsersActiveInactiveRequestDto, UsersCreateRequestDto, UsersDetailItemDto,
|
||||||
|
UsersUpdateRequestDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct UsersService;
|
||||||
|
|
||||||
|
impl UsersService {
|
||||||
|
pub async fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
match repo.query_user_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_user_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
match repo.query_user_by_id(id).await {
|
||||||
|
Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto {
|
||||||
|
data: UsersDetailItemDto {
|
||||||
|
id: user.id,
|
||||||
|
role: user.role,
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
avatar: user.avatar,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
is_active: user.is_active,
|
||||||
|
gender: user.gender,
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_me(headers: HeaderMap, state: &AppState) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
let email = extract_email(&headers).unwrap();
|
||||||
|
let user = repo.query_user_by_email(email).await.unwrap();
|
||||||
|
match repo.query_user_by_id(user.id.id.to_raw()).await {
|
||||||
|
Ok(user) => success_response(ResponseSuccessDto {
|
||||||
|
data: UsersDetailItemDto {
|
||||||
|
id: user.id,
|
||||||
|
role: user.role,
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
avatar: user.avatar,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
is_active: user.is_active,
|
||||||
|
gender: user.gender,
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_user(
|
||||||
|
state: &AppState,
|
||||||
|
new_user: UsersCreateRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&new_user) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
if repo
|
||||||
|
.query_user_by_email(new_user.email.clone())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User already exists");
|
||||||
|
}
|
||||||
|
let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &new_user.role_id);
|
||||||
|
match repo
|
||||||
|
.query_create_user(UsersSchema {
|
||||||
|
email: new_user.email.clone(),
|
||||||
|
fullname: new_user.fullname.clone(),
|
||||||
|
password: hash_password(&new_user.password).unwrap(),
|
||||||
|
phone_number: new_user.phone_number.clone(),
|
||||||
|
is_active: new_user.is_active.clone(),
|
||||||
|
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 update_user(
|
||||||
|
state: &AppState,
|
||||||
|
id: String,
|
||||||
|
user: UsersUpdateRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
|
||||||
|
if let Err((status, message)) = validate_request(&user) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
|
let role_id = make_thing(&ResourceEnum::Roles.to_string(), "");
|
||||||
|
|
||||||
|
let updated_user = UsersSchema {
|
||||||
|
id: user_id,
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
is_active: user.is_active,
|
||||||
|
gender: user.gender,
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
avatar: user.avatar,
|
||||||
|
role: role_id,
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match repo.query_update_user(updated_user).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_me(
|
||||||
|
state: &AppState,
|
||||||
|
headers: HeaderMap,
|
||||||
|
user: UsersUpdateRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
let email = extract_email(&headers).unwrap();
|
||||||
|
let user_data = repo.query_user_by_email(email).await.unwrap();
|
||||||
|
if let Err((status, message)) = validate_request(&user) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let user_id =
|
||||||
|
make_thing(&ResourceEnum::Users.to_string(), &user_data.id.id.to_raw());
|
||||||
|
let role_id = make_thing(&ResourceEnum::Roles.to_string(), "");
|
||||||
|
let updated_user = UsersSchema {
|
||||||
|
id: user_id,
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
|
||||||
|
is_active: user.is_active,
|
||||||
|
|
||||||
|
gender: user.gender,
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
avatar: user.avatar,
|
||||||
|
|
||||||
|
role: role_id,
|
||||||
|
updated_at: get_iso_date(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match repo.query_update_user(updated_user).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_user_active_status(
|
||||||
|
state: &AppState,
|
||||||
|
id: String,
|
||||||
|
status: UsersActiveInactiveRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
|
match repo.query_user_by_id(thing_id.id.to_raw()).await {
|
||||||
|
Ok(_) => match repo
|
||||||
|
.query_active_inactive_user_by_id(
|
||||||
|
id,
|
||||||
|
UsersActiveInactiveSchema {
|
||||||
|
is_active: status.is_active,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
},
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_password(
|
||||||
|
state: &AppState,
|
||||||
|
email: String,
|
||||||
|
new_password: UsersSetNewPasswordSchema,
|
||||||
|
) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
match repo.query_update_password_user(email, new_password).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
if repo.query_user_by_id(id.clone()).await.is_err() {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
|
}
|
||||||
|
match repo.query_delete_user(id).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-libs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-entities = { path = "../imphnen-entities" }
|
||||||
|
argon2.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
jsonwebtoken.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
lettre.workspace = true
|
||||||
|
surrealdb = { workspace = true, features = ["kv-mem"] }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use argon2::{
|
||||||
|
password_hash::{
|
||||||
|
rand_core::OsRng, Error, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||||
|
SaltString,
|
||||||
|
},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn hash_password(password: &str) -> Result<String, Error> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let password_hash = argon2
|
||||||
|
.hash_password(password.as_bytes(), &salt)?
|
||||||
|
.to_string();
|
||||||
|
Ok(password_hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_password(password: &str, hash: &str) -> Result<bool, Error> {
|
||||||
|
let parsed_hash = PasswordHash::new(hash)?;
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
match argon2.verify_password(password.as_bytes(), &parsed_hash) {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use crate::{
|
||||||
|
surrealdb_init_mem, surrealdb_init_ws, Env, SurrealMemClient, SurrealWsClient,
|
||||||
|
};
|
||||||
|
use axum::{serve, Router};
|
||||||
|
use std::{future::Future, net::SocketAddr};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
pub async fn axum_init<F, Fut>(router_fn: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(SurrealWsClient, SurrealMemClient) -> Fut,
|
||||||
|
Fut: Future<Output = Router>,
|
||||||
|
{
|
||||||
|
let env = Env::new();
|
||||||
|
let surrealdb_ws = surrealdb_init_ws().await.expect("Failed surrealdb ws");
|
||||||
|
let surrealdb_mem = surrealdb_init_mem().await.expect("Failed surrealdb mem");
|
||||||
|
let router = router_fn(surrealdb_ws, surrealdb_mem).await;
|
||||||
|
let port = env.port;
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||||
|
println!("Listening on http://{}", addr);
|
||||||
|
match serve(listener, router).await {
|
||||||
|
Ok(_) => println!("Server stopped gracefully."),
|
||||||
|
Err(err) => println!("Server encountered an error: {}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
pub struct Env {
|
||||||
|
pub port: u16,
|
||||||
|
pub access_token_secret: String,
|
||||||
|
pub refresh_token_secret: String,
|
||||||
|
pub surrealdb_url: String,
|
||||||
|
pub surrealdb_username: String,
|
||||||
|
pub surrealdb_password: String,
|
||||||
|
pub surrealdb_namespace: String,
|
||||||
|
pub surrealdb_dbname: String,
|
||||||
|
pub smtp_email: String,
|
||||||
|
pub smtp_password: String,
|
||||||
|
pub smtp_name: String,
|
||||||
|
pub smtp_host: String,
|
||||||
|
pub redisdb_url: String,
|
||||||
|
pub fe_url: String,
|
||||||
|
pub rust_env: String,
|
||||||
|
pub minio_endpoint: String,
|
||||||
|
pub minio_bucket_name: String,
|
||||||
|
pub minio_access_key: String,
|
||||||
|
pub minio_secret_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Env {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
port: env::var("PORT")
|
||||||
|
.unwrap_or_else(|_| "3000".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(3000),
|
||||||
|
access_token_secret: env::var("ACCESS_TOKEN_SECRET")
|
||||||
|
.unwrap_or_else(|_| "default_access_secret".to_string()),
|
||||||
|
refresh_token_secret: env::var("REFRESH_TOKEN_SECRET")
|
||||||
|
.unwrap_or_else(|_| "default_refresh_secret".to_string()),
|
||||||
|
surrealdb_url: env::var("SURREALDB_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:8000".to_string()),
|
||||||
|
surrealdb_username: env::var("SURREALDB_USERNAME")
|
||||||
|
.unwrap_or_else(|_| "root".to_string()),
|
||||||
|
surrealdb_password: env::var("SURREALDB_PASSWORD")
|
||||||
|
.unwrap_or_else(|_| "password".to_string()),
|
||||||
|
surrealdb_namespace: env::var("SURREALDB_NAMESPACE")
|
||||||
|
.unwrap_or_else(|_| "namespace".to_string()),
|
||||||
|
surrealdb_dbname: env::var("SURREALDB_DBNAME")
|
||||||
|
.unwrap_or_else(|_| "database".to_string()),
|
||||||
|
smtp_email: env::var("SMTP_EMAIL")
|
||||||
|
.unwrap_or_else(|_| "no-reply@example.com".to_string()),
|
||||||
|
smtp_password: env::var("SMTP_PASSWORD")
|
||||||
|
.unwrap_or_else(|_| "default_smtp_password".to_string()),
|
||||||
|
smtp_name: env::var("SMTP_NAME").unwrap_or_else(|_| "MyApp SMTP".to_string()),
|
||||||
|
smtp_host: env::var("SMTP_HOST")
|
||||||
|
.unwrap_or_else(|_| "smtp.gmail.com".to_string()),
|
||||||
|
redisdb_url: env::var("REDISDB_URL")
|
||||||
|
.unwrap_or_else(|_| "localhost".to_string()),
|
||||||
|
fe_url: env::var("FE_URL").unwrap_or_else(|_| "http://localhost".to_string()),
|
||||||
|
rust_env: env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()),
|
||||||
|
minio_endpoint: env::var("MINIO_ENDPOINT")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:9000".to_string()),
|
||||||
|
minio_bucket_name: env::var("MINIO_BUCKET_NAME")
|
||||||
|
.unwrap_or_else(|_| "default_bucket".to_string()),
|
||||||
|
minio_access_key: env::var("MINIO_ACCESS_KEY")
|
||||||
|
.unwrap_or_else(|_| "minio_access".to_string()),
|
||||||
|
minio_secret_key: env::var("MINIO_SECRET_KEY")
|
||||||
|
.unwrap_or_else(|_| "minio_secret".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use super::Env;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use chrono::{Duration, TimeDelta, Utc};
|
||||||
|
use jsonwebtoken::{
|
||||||
|
decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Claims {
|
||||||
|
pub exp: usize,
|
||||||
|
pub iat: usize,
|
||||||
|
pub sub: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_access_token(sub: String) -> Result<String, StatusCode> {
|
||||||
|
let env = Env::new();
|
||||||
|
let secret: String = env.access_token_secret;
|
||||||
|
let now = Utc::now();
|
||||||
|
let expire: TimeDelta = Duration::minutes(15);
|
||||||
|
let exp: usize = (now + expire).timestamp() as usize;
|
||||||
|
let iat: usize = now.timestamp() as usize;
|
||||||
|
let claim = Claims { iat, exp, sub };
|
||||||
|
encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claim,
|
||||||
|
&EncodingKey::from_secret(secret.as_ref()),
|
||||||
|
)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_reset_password_token(sub: String) -> Result<String, StatusCode> {
|
||||||
|
let env = Env::new();
|
||||||
|
let secret: String = env.access_token_secret;
|
||||||
|
let now = Utc::now();
|
||||||
|
let expire: TimeDelta = Duration::minutes(5);
|
||||||
|
let exp: usize = (now + expire).timestamp() as usize;
|
||||||
|
let iat: usize = now.timestamp() as usize;
|
||||||
|
let claim = Claims { iat, exp, sub };
|
||||||
|
encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claim,
|
||||||
|
&EncodingKey::from_secret(secret.as_ref()),
|
||||||
|
)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_access_token(
|
||||||
|
jwt_token: &str,
|
||||||
|
) -> Result<TokenData<Claims>, StatusCode> {
|
||||||
|
let env = Env::new();
|
||||||
|
let secret: String = env.access_token_secret;
|
||||||
|
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
||||||
|
&jwt_token,
|
||||||
|
&DecodingKey::from_secret(secret.as_ref()),
|
||||||
|
&Validation::default(),
|
||||||
|
)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_refresh_token(sub: String) -> Result<String, StatusCode> {
|
||||||
|
let env = Env::new();
|
||||||
|
let secret: String = env.refresh_token_secret;
|
||||||
|
let now = Utc::now();
|
||||||
|
let expire: TimeDelta = Duration::days(1);
|
||||||
|
let exp: usize = (now + expire).timestamp() as usize;
|
||||||
|
let iat: usize = now.timestamp() as usize;
|
||||||
|
let claim = Claims { iat, exp, sub };
|
||||||
|
encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claim,
|
||||||
|
&EncodingKey::from_secret(secret.as_ref()),
|
||||||
|
)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_refresh_token(
|
||||||
|
jwt_token: &str,
|
||||||
|
) -> Result<TokenData<Claims>, StatusCode> {
|
||||||
|
let env = Env::new();
|
||||||
|
let secret: String = env.refresh_token_secret;
|
||||||
|
let result: Result<TokenData<Claims>, StatusCode> = decode(
|
||||||
|
&jwt_token,
|
||||||
|
&DecodingKey::from_secret(secret.as_ref()),
|
||||||
|
&Validation::default(),
|
||||||
|
)
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
result
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use super::Env;
|
||||||
|
use lettre::message::Mailbox;
|
||||||
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
pub fn send_email(
|
||||||
|
to: &str,
|
||||||
|
subject: &str,
|
||||||
|
body: &str,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let env = Env::new();
|
||||||
|
let host = env.smtp_host;
|
||||||
|
let sender_email = env.smtp_email;
|
||||||
|
let sender_name = env.smtp_name;
|
||||||
|
let sender_password = env.smtp_password;
|
||||||
|
let recipient_email = to;
|
||||||
|
let email = Message::builder()
|
||||||
|
.from(Mailbox::new(
|
||||||
|
Some(sender_name.replace("-", " ")),
|
||||||
|
sender_email.parse()?,
|
||||||
|
))
|
||||||
|
.to(recipient_email.parse()?)
|
||||||
|
.subject(subject)
|
||||||
|
.body(body.to_string())?;
|
||||||
|
let smtp_credentials =
|
||||||
|
Credentials::new(sender_email, sender_password.replace("-", " "));
|
||||||
|
let mailer = SmtpTransport::relay(&host)?
|
||||||
|
.credentials(smtp_credentials)
|
||||||
|
.build();
|
||||||
|
match mailer.send(&email) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Email sent successfully to {}", to);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to send email: {}", e);
|
||||||
|
Err(Box::new(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use imphnen_entities::*;
|
||||||
|
|
||||||
|
pub mod argon;
|
||||||
|
pub mod axum;
|
||||||
|
pub mod enviroment;
|
||||||
|
pub mod jsonwebtoken;
|
||||||
|
pub mod lettre;
|
||||||
|
pub mod surrealdb;
|
||||||
|
|
||||||
|
pub use argon::*;
|
||||||
|
pub use axum::*;
|
||||||
|
pub use enviroment::*;
|
||||||
|
pub use imphnen_entities::*;
|
||||||
|
pub use jsonwebtoken::*;
|
||||||
|
pub use lettre::*;
|
||||||
|
pub use surrealdb::*;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use super::Env;
|
||||||
|
use crate::{SurrealMemClient, SurrealWsClient};
|
||||||
|
use surrealdb::engine::local::Mem;
|
||||||
|
use surrealdb::engine::remote::ws::{Client, Ws};
|
||||||
|
use surrealdb::opt::auth::Root;
|
||||||
|
use surrealdb::{Result, Surreal};
|
||||||
|
|
||||||
|
pub mod resource;
|
||||||
|
pub use resource::*;
|
||||||
|
|
||||||
|
pub async fn surrealdb_init_ws() -> Result<SurrealWsClient> {
|
||||||
|
let env = Env::new();
|
||||||
|
let db = Surreal::<Client>::init();
|
||||||
|
db.connect::<Ws>(env.surrealdb_url.clone()).await?;
|
||||||
|
db.signin(Root {
|
||||||
|
username: &env.surrealdb_username,
|
||||||
|
password: &env.surrealdb_password,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
db.use_ns(env.surrealdb_namespace.clone())
|
||||||
|
.use_db(env.surrealdb_dbname.clone())
|
||||||
|
.await?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn surrealdb_init_mem() -> Result<SurrealMemClient> {
|
||||||
|
let env = Env::new();
|
||||||
|
let db = Surreal::new::<Mem>(()).await?;
|
||||||
|
db.use_ns(&env.surrealdb_namespace)
|
||||||
|
.use_db(&env.surrealdb_dbname)
|
||||||
|
.await?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ResourceEnum {
|
||||||
|
OtpCache,
|
||||||
|
UsersCache,
|
||||||
|
GachaItems,
|
||||||
|
GachaClaims,
|
||||||
|
GachaRolls,
|
||||||
|
Users,
|
||||||
|
Roles,
|
||||||
|
Permissions,
|
||||||
|
RolesPermissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ResourceEnum {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let str = match self {
|
||||||
|
ResourceEnum::Users => "app_users",
|
||||||
|
ResourceEnum::UsersCache => "app_users_cache",
|
||||||
|
ResourceEnum::OtpCache => "app_otp_cache",
|
||||||
|
ResourceEnum::Roles => "app_roles",
|
||||||
|
ResourceEnum::Permissions => "app_permissions",
|
||||||
|
ResourceEnum::RolesPermissions => "app_roles_permissions",
|
||||||
|
ResourceEnum::GachaItems => "app_gacha_items",
|
||||||
|
ResourceEnum::GachaClaims => "app_gacha_claims",
|
||||||
|
ResourceEnum::GachaRolls => "app_gacha_rolls",
|
||||||
|
};
|
||||||
|
write!(f, "{}", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "imphnen-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
imphnen-libs = { path = "../imphnen-libs"}
|
||||||
|
imphnen-entities = { path = "../imphnen-entities" }
|
||||||
|
surrealdb.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
axum-test.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use surrealdb::{engine::remote::ws::Client, method::Query};
|
||||||
|
|
||||||
|
pub fn bind_filter_value(
|
||||||
|
query: Query<'_, Client>,
|
||||||
|
val: String,
|
||||||
|
) -> Query<'_, Client> {
|
||||||
|
if let Ok(b) = val.parse::<bool>() {
|
||||||
|
query.bind(("filter", b))
|
||||||
|
} else if let Ok(i) = val.parse::<i64>() {
|
||||||
|
query.bind(("filter", i))
|
||||||
|
} else {
|
||||||
|
query.bind(("filter", val))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use crate::decode_access_token;
|
||||||
|
use axum::http::{header::AUTHORIZATION, HeaderMap};
|
||||||
|
|
||||||
|
pub fn extract_email(headers: &HeaderMap) -> Option<String> {
|
||||||
|
println!("📥 Received headers: {:?}", headers);
|
||||||
|
|
||||||
|
let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?;
|
||||||
|
println!("🔍 Authorization Header: {}", auth_header);
|
||||||
|
|
||||||
|
let token = auth_header.strip_prefix("Bearer ")?;
|
||||||
|
println!("🧪 Token: {}", token);
|
||||||
|
|
||||||
|
match decode_access_token(token) {
|
||||||
|
Ok(data) => {
|
||||||
|
println!("✅ Token claims: {:?}", data.claims);
|
||||||
|
Some(data.claims.sub)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ Failed to decode token: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_email_token(token: String) -> Option<String> {
|
||||||
|
let token_data = decode_access_token(&token).ok()?;
|
||||||
|
Some(token_data.claims.sub)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
pub fn get_iso_date() -> String {
|
||||||
|
let now: DateTime<Utc> = Utc::now();
|
||||||
|
now.to_rfc3339()
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use rand::{rng, Rng};
|
||||||
|
|
||||||
|
pub struct OtpManager;
|
||||||
|
|
||||||
|
impl OtpManager {
|
||||||
|
pub fn generate_otp() -> u32 {
|
||||||
|
rng().random_range(100_000..1_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_otp(stored_otp: u32, user_otp: u32) -> bool {
|
||||||
|
stored_otp == user_otp
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use anyhow::{bail, Result};
|
||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
pub fn get_id(thing: &Thing) -> Result<(&str, &str)> {
|
||||||
|
let table = thing.tb.as_str();
|
||||||
|
let id = match &thing.id {
|
||||||
|
surrealdb::sql::Id::String(s) => s.as_str(),
|
||||||
|
_ => bail!("Unsupported ID type"),
|
||||||
|
};
|
||||||
|
Ok((table, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_id(thing: &Thing) -> String {
|
||||||
|
let id = thing.id.to_raw();
|
||||||
|
id
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use imphnen_entities::*;
|
||||||
|
use imphnen_libs::*;
|
||||||
|
|
||||||
|
pub mod bind_filter;
|
||||||
|
pub mod extract_email;
|
||||||
|
pub mod generate_date;
|
||||||
|
pub mod generate_otp;
|
||||||
|
pub mod get_id;
|
||||||
|
pub mod make_thing;
|
||||||
|
pub mod mock_test;
|
||||||
|
pub mod query_list;
|
||||||
|
pub mod response_format;
|
||||||
|
pub mod validator;
|
||||||
|
|
||||||
|
pub use bind_filter::*;
|
||||||
|
pub use extract_email::*;
|
||||||
|
pub use generate_date::*;
|
||||||
|
pub use generate_otp::*;
|
||||||
|
pub use get_id::*;
|
||||||
|
pub use imphnen_entities::*;
|
||||||
|
pub use imphnen_libs::*;
|
||||||
|
pub use make_thing::*;
|
||||||
|
pub use mock_test::*;
|
||||||
|
pub use query_list::*;
|
||||||
|
pub use response_format::*;
|
||||||
|
pub use validator::*;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
use surrealdb::sql::Thing;
|
||||||
|
|
||||||
|
pub fn make_thing(table: &str, id: &str) -> Thing {
|
||||||
|
Thing::from((table, id))
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use surrealdb::{
|
||||||
|
Surreal,
|
||||||
|
engine::{local::Mem, remote::ws::Ws},
|
||||||
|
opt::auth::Root,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn create_mock_app_state() -> AppState {
|
||||||
|
let db_mem = Surreal::new::<Mem>(()).await.unwrap();
|
||||||
|
let db_ws = Surreal::new::<Ws>("localhost:8000").await.unwrap();
|
||||||
|
db_mem.use_ns("test").use_db("test").await.unwrap();
|
||||||
|
db_ws
|
||||||
|
.signin(Root {
|
||||||
|
username: "root",
|
||||||
|
password: "root",
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db_ws.use_ns("test").use_db("test").await.unwrap();
|
||||||
|
|
||||||
|
AppState {
|
||||||
|
surrealdb_mem: db_mem,
|
||||||
|
surrealdb_ws: db_ws,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cleanup_db() {
|
||||||
|
let app_state = create_mock_app_state().await;
|
||||||
|
let _ = app_state
|
||||||
|
.surrealdb_mem
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
REMOVE TABLE app_users;
|
||||||
|
REMOVE TABLE app_roles;
|
||||||
|
REMOVE TABLE app_users_cache;
|
||||||
|
REMOVE TABLE app_otp_cache;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = app_state
|
||||||
|
.surrealdb_ws
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
REMOVE TABLE app_users;
|
||||||
|
REMOVE TABLE app_roles;
|
||||||
|
REMOVE TABLE app_users_cache;
|
||||||
|
REMOVE TABLE app_otp_cache;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use super::bind_filter_value;
|
||||||
|
use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use surrealdb::{engine::remote::ws::Client, Surreal};
|
||||||
|
|
||||||
|
pub async fn query_list_with_meta<T>(
|
||||||
|
db: &Surreal<Client>,
|
||||||
|
table: &str,
|
||||||
|
meta: &MetaRequestDto,
|
||||||
|
conditions: Vec<String>,
|
||||||
|
custom_select: Option<String>,
|
||||||
|
) -> Result<ResponseListSuccessDto<Vec<T>>>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Serialize,
|
||||||
|
{
|
||||||
|
let page = meta.page.unwrap_or(1);
|
||||||
|
let per_page = meta.per_page.unwrap_or(10);
|
||||||
|
if page < 1 || per_page < 1 {
|
||||||
|
bail!("Invalid pagination: page and per_page must be greater than 0");
|
||||||
|
}
|
||||||
|
let start = (page - 1) * per_page;
|
||||||
|
let sql = custom_select.unwrap_or_else(|| {
|
||||||
|
let mut s = format!("SELECT * FROM {}", table);
|
||||||
|
if !conditions.is_empty() {
|
||||||
|
s.push_str(" WHERE ");
|
||||||
|
s.push_str(&conditions.join(" AND "));
|
||||||
|
}
|
||||||
|
if let Some(sort_by) = &meta.sort_by {
|
||||||
|
let order = match meta
|
||||||
|
.order
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
|
"DESC" => "DESC",
|
||||||
|
_ => "ASC",
|
||||||
|
};
|
||||||
|
s.push_str(&format!(" ORDER BY {} {}", sort_by, order));
|
||||||
|
}
|
||||||
|
s.push_str(" LIMIT $per_page START $start");
|
||||||
|
s
|
||||||
|
});
|
||||||
|
let mut query_exec = db.query(sql);
|
||||||
|
if let Some(search) = &meta.search {
|
||||||
|
if !search.is_empty() {
|
||||||
|
query_exec = query_exec.bind(("search", search.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(filter_val) = meta.filter.clone() {
|
||||||
|
query_exec = bind_filter_value(query_exec, filter_val);
|
||||||
|
}
|
||||||
|
query_exec = query_exec
|
||||||
|
.bind(("per_page", per_page))
|
||||||
|
.bind(("start", start));
|
||||||
|
let raw: Vec<T> = query_exec.await?.take(0)?;
|
||||||
|
let mut count_sql = format!("SELECT count() FROM {}", table);
|
||||||
|
if !conditions.is_empty() {
|
||||||
|
count_sql.push_str(" WHERE ");
|
||||||
|
count_sql.push_str(&conditions.join(" AND "));
|
||||||
|
}
|
||||||
|
let mut count_query = db.query(count_sql);
|
||||||
|
if let Some(search) = &meta.search {
|
||||||
|
if !search.is_empty() {
|
||||||
|
count_query = count_query.bind(("search", search.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(filter_val) = meta.filter.clone() {
|
||||||
|
count_query = bind_filter_value(count_query, filter_val);
|
||||||
|
}
|
||||||
|
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
|
||||||
|
let total = count_result.first().map(|c| c.count);
|
||||||
|
|
||||||
|
let meta = MetaResponseDto {
|
||||||
|
page: Some(page),
|
||||||
|
per_page: Some(per_page),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
Ok(ResponseListSuccessDto {
|
||||||
|
data: raw,
|
||||||
|
meta: Some(meta),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::{ResponseListSuccessDto, ResponseSuccessDto};
|
||||||
|
|
||||||
|
pub fn success_response<T: Serialize>(params: ResponseSuccessDto<T>) -> Response {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(json!({
|
||||||
|
"data": params.data,
|
||||||
|
"version": "0.1.0",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success_list_response<T: Serialize>(
|
||||||
|
params: ResponseListSuccessDto<T>,
|
||||||
|
) -> Response {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(json!({
|
||||||
|
"data": params.data,
|
||||||
|
"meta": params.meta,
|
||||||
|
"version": "0.1.0",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn common_response(status: StatusCode, message: &str) -> Response {
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
Json(json!({
|
||||||
|
"message": message,
|
||||||
|
"version": "0.1.0",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use axum::http::StatusCode;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
pub fn validate_request<T: Validate>(
|
||||||
|
payload: &T,
|
||||||
|
) -> Result<(), (StatusCode, String)> {
|
||||||
|
if let Err(validation_errors) = payload.validate() {
|
||||||
|
let error_messages: Vec<String> = validation_errors
|
||||||
|
.field_errors()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(_, errors)| {
|
||||||
|
errors.iter().map(move |error| {
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
error
|
||||||
|
.message
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "Invalid value".into())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return Err((StatusCode::BAD_REQUEST, error_messages.join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
hard_tabs = true
|
hard_tabs = true
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
max_width = 85
|
max_width = 85
|
||||||
|
tab_spaces = 2
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pkgs.mkShell {
|
|||||||
rustfmt
|
rustfmt
|
||||||
crate2nix
|
crate2nix
|
||||||
clippy
|
clippy
|
||||||
|
surrealdb
|
||||||
|
|
||||||
(writeScriptBin "helpme" ''
|
(writeScriptBin "helpme" ''
|
||||||
__usage="
|
__usage="
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
use axum::{routing::get, Router};
|
|
||||||
use v1::auth_router;
|
|
||||||
|
|
||||||
pub mod v1;
|
|
||||||
pub mod v2;
|
|
||||||
|
|
||||||
pub async fn apps() -> Router {
|
|
||||||
let v1_routes = Router::new()
|
|
||||||
.nest("/auth", auth_router())
|
|
||||||
.route("/", get(|| async { "Comming Soon v1" }));
|
|
||||||
|
|
||||||
let v2_routes = Router::new().route("/", get(|| async { "Comming Soon v2" }));
|
|
||||||
|
|
||||||
Router::new().nest("/v1", v1_routes).nest("/v2", v2_routes)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
use super::{mutation_login, AuthLoginRequestDto};
|
|
||||||
use axum::{response::Response, Json};
|
|
||||||
|
|
||||||
pub async fn post_login(Json(payload): Json<AuthLoginRequestDto>) -> Response {
|
|
||||||
mutation_login(payload).await
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use utoipa::ToSchema;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
|
||||||
pub struct AuthLoginRequestDto {
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
use super::auth_dto::AuthLoginRequestDto;
|
|
||||||
use crate::{success_response, ResponseSuccessDto};
|
|
||||||
use axum::response::Response;
|
|
||||||
|
|
||||||
pub async fn mutation_login(params: AuthLoginRequestDto) -> Response {
|
|
||||||
let response = ResponseSuccessDto {
|
|
||||||
data: AuthLoginRequestDto {
|
|
||||||
email: params.email,
|
|
||||||
password: params.password,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
success_response(response)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
use axum::{routing::post, Router};
|
|
||||||
|
|
||||||
pub mod auth_controller;
|
|
||||||
pub mod auth_dto;
|
|
||||||
pub mod auth_middleware;
|
|
||||||
pub mod auth_repository;
|
|
||||||
|
|
||||||
pub use auth_dto::*;
|
|
||||||
pub use auth_repository::*;
|
|
||||||
|
|
||||||
pub fn auth_router() -> Router {
|
|
||||||
Router::new().route("/login", post(auth_controller::post_login))
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub mod auth;
|
|
||||||
|
|
||||||
pub use auth::*;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user