feat: initial commit

This commit is contained in:
Maulana Sodiqin
2025-03-20 17:33:57 +07:00
parent 8ff532bb67
commit ea5ece9ab2
12 changed files with 61 additions and 461 deletions
Generated
+23 -23
View File
@@ -388,6 +388,29 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-surrealdb-boilerplate"
version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"axum",
"chrono",
"jsonwebtoken",
"lettre",
"log",
"rand 0.9.0",
"redis",
"serde",
"serde_json",
"surrealdb",
"thiserror 2.0.12",
"tokio",
"tower-http",
"utoipa",
"utoipa-swagger-ui",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.74" version = "0.3.74"
@@ -1754,29 +1777,6 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "imphnen-backend-service"
version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"axum",
"chrono",
"jsonwebtoken",
"lettre",
"log",
"rand 0.9.0",
"redis",
"serde",
"serde_json",
"surrealdb",
"thiserror 2.0.12",
"tokio",
"tower-http",
"utoipa",
"utoipa-swagger-ui",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
+1 -1
View File
@@ -1,5 +1,5 @@
[package] [package]
name = "imphnen-backend-service" name = "axum-surrealdb-boilerplate"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
+29 -70
View File
@@ -1,113 +1,72 @@
# IMPHNEN CMS API # Axum SurrealDB Boilerplate
This project is a CMS API for [IMPHNEN Website](https://imphnen.dev)
## Features ## Features
- **RESTful API**: Provides endpoints for managing wedding reservations. - **Authentication Ready**: Preconfigured authentication and middleware for secure API access.
- **Database Integration**: Employs SeaORM for seamless database interactions. - **Database Integration**: SurrealDB seamlessly integrated as an Axum Extension.
- **Authentication**: Authentication and Middleware - **CORS Handling**: Fine-tuned CORS management with Tower HTTP `CorsLayer`.
- **CORS Handling**: Handling CORS with Tower HTTP CorsLayer - **API Documentation**: Fully documented with OpenAPI and Swagger UI.
- **API Docs**: OpenAPI Swager Ready - **Optimized for Performance**: Asynchronous, lightweight, and scalable architecture.
## Prerequisites ## Prerequisites
- **Rust**: Ensure that Rust is installed on your system. You can install it from [rust-lang.org](https://www.rust-lang.org/). - **Rust**: Install Rust from [rust-lang.org](https://www.rust-lang.org/).
- **Database**: Set up a Postgresql database and note the connection details - **Database**: Set up a SurrealDB instance and configure connection details.
- **Docker**: if you want build this project using docker, you need docker, You can install it from [docker.com](https://www.docker.com/) - **Docker**: Required for containerized deployment, install from [docker.com](https://www.docker.com/).
- **Nix**: if you want build this project using nix, you need nix, You can install it from [nixos.org](https://nixos.org/) - **Nix (Optional)**: For reproducible builds, install from [nixos.org](https://nixos.org/).
## Getting Started ## Getting Started
1. **Clone the Repository**: 1. **Clone the Repository**:
```bash - `git clone https://github.com/maulanasdqn/axum-surrealdb-boilerplate`
git clone https://github.com/IMPHNEN/imphnen-cms-api.git
cd imphnen-cms-api
```
2. **Set Up Environment Variables**: 2. **Set Up Environment Variables**:
Copy a `.env.exanple` file: - Copy `.env.example` and rename it to `.env`
```env - **Windows**: Run the script: `./apply-env.ps1`
cp .env.example .env - **Unix-based systems (Linux, macOS, BSD)**: Run the script: `./apply-env.sh`
```
3. **Install Dependencies**: 3. **Install Dependencies**:
```bash - `cargo install .`
cargo check
```
```bash 4. **Setup Database**:
cargo build --release
```
```bash
cargo install sea-orm-cli
```
5. **Run Database Migrations**: - 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`
```bash 5. **Start the Server**:
sea-orm-cli migrate up
```
6. **Start the Server**: - Install Cargo Watch `cargo install cargo-watch`
- Run it with cargo watch `cargo watch -x run`
```bash The API will be available at `http://localhost:3000/docs`.
cargo run -q
```
The API will be accessible at `http://localhost:3000/v1/docs`.
## Docker ## Docker
1. **Build the Docker Image**: 1. **Build the Docker Image**:
```bash
docker build -t cms-api .
```
2. **Run the Docker Container**: 2. **Run the Docker Container**:
```bash The API will be accessible at `http://localhost:3000/docs`.
docker run -p 3000:3000 --env-file .env cms-api cms-api:latest
```
The API will be accessible at `http://localhost:3000/v1/docs`. ## Using Nix as Builder (Optional)
## Using Nix as Builder
1. **Install Nix**: 1. **Install Nix**:
```bash 2. **Enter Nix Shell or Use Nix Flakes**:
curl -L https://nixos.org/nix/install | sh
```
2. **Switch to Nix Shell or Nix Flake**:
```bash
nix develop
```
3. **Build the Project**: 3. **Build the Project**:
```bash
nix build
```
4. **Run the Server**: 4. **Run the Server**:
```bash
nix run
```
## Contributing ## Contributing
Contributions are welcome! Please fork the repository and create a pull request with your changes. Contributions are welcome! Fork the repository and create a pull request with your improvements.
## License ## License
@@ -116,4 +75,4 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
## Acknowledgements ## Acknowledgements
- [Axum](https://github.com/tokio-rs/axum) - [Axum](https://github.com/tokio-rs/axum)
- [SeaORM](https://github.com/SeaQL/sea-orm) - [SurrealDB](https://github.com/surrealdb/surrealdb)
+5 -12
View File
@@ -1,8 +1,7 @@
use crate::{ use crate::{
v1::{ v1::{
auth, gacha, AuthLoginRequestDto, AuthLoginResponsetDto, auth, AuthLoginRequestDto, AuthLoginResponsetDto, AuthResendOtpRequestDto,
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, GachaCreateClaimRequestDto, AuthVerifyEmailRequestDto,
GachaCreateItemRequestDto, GachaCreateRollRequestDto,
}, },
MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseSuccessDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseSuccessDto,
}; };
@@ -18,10 +17,7 @@ use utoipa::{
auth::auth_controller::post_login, auth::auth_controller::post_login,
auth::auth_controller::post_register, auth::auth_controller::post_register,
auth::auth_controller::post_verify_email, auth::auth_controller::post_verify_email,
auth::auth_controller::post_resend_otp, auth::auth_controller::post_resend_otp
gacha::gacha_controller::post_create_gacha_claim,
gacha::gacha_controller::post_create_gacha_item,
gacha::gacha_controller::post_create_gacha_roll
), ),
components( components(
schemas( schemas(
@@ -33,14 +29,11 @@ use utoipa::{
AuthVerifyEmailRequestDto, AuthVerifyEmailRequestDto,
AuthResendOtpRequestDto, AuthResendOtpRequestDto,
ResponseSuccessDto<AuthLoginResponsetDto>, ResponseSuccessDto<AuthLoginResponsetDto>,
GachaCreateClaimRequestDto,
GachaCreateItemRequestDto,
GachaCreateRollRequestDto
) )
), ),
info( info(
title = "IMPHNEN API", title = "Axum SurrealDB Boilerplate",
description = "IMPHNEN API Documentation", description = "Axum SurrealDB Documentation",
version = "0.1.0", version = "0.1.0",
contact( contact(
name = "Maulana Sodiqin", name = "Maulana Sodiqin",
-55
View File
@@ -1,55 +0,0 @@
use super::{GachaCreateClaimRequestDto, GachaCreateRollRequestDto, GachaService};
use crate::{v1::GachaCreateItemRequestDto, AppState, MessageResponseDto};
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
#[utoipa::path(
post,
path = "/v1/gacha/claims/create",
request_body = GachaCreateClaimRequestDto,
responses(
(status = 200, description = "Create gacha claim successful", body = MessageResponseDto),
(status = 401, description = "Create gacha claim failed", body = MessageResponseDto)
),
tag = "Gacha"
)]
pub async fn post_create_gacha_claim(
header: HeaderMap,
Extension(state): Extension<AppState>,
Json(payload): Json<GachaCreateClaimRequestDto>,
) -> impl IntoResponse {
GachaService::mutation_create_gacha_claim(payload, &state, header).await
}
#[utoipa::path(
post,
path = "/v1/gacha/items/create",
request_body = GachaCreateItemRequestDto,
responses(
(status = 200, description = "Create gacha item successful", body = MessageResponseDto),
(status = 401, description = "Create gacha item failed", body = MessageResponseDto)
),
tag = "Gacha"
)]
pub async fn post_create_gacha_item(
Extension(state): Extension<AppState>,
Json(payload): Json<GachaCreateItemRequestDto>,
) -> impl IntoResponse {
GachaService::mutation_create_gacha_item(payload, &state).await
}
#[utoipa::path(
post,
path = "/v1/gacha/rolls/create",
request_body = GachaCreateRollRequestDto,
responses(
(status = 200, description = "Create gacha roll successful", body = MessageResponseDto),
(status = 401, description = "Create gacha roll failed", body = MessageResponseDto)
),
tag = "Gacha"
)]
pub async fn post_create_gacha_roll(
Extension(state): Extension<AppState>,
Json(payload): Json<GachaCreateRollRequestDto>,
) -> impl IntoResponse {
GachaService::mutation_create_gacha_roll(payload, &state).await
}
-39
View File
@@ -1,39 +0,0 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::v1::UsersItemDto;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaCreateClaimRequestDto {
pub item_name: String,
pub transaction_number: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaCreateItemRequestDto {
pub item_name: String,
pub item_image: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaCreateRollRequestDto {
pub item_name: String,
pub weight: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaItemResponseDto {
pub item_name: String,
pub item_image: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaClaimResponseDto {
pub transaction_number: String,
pub user: UsersItemDto,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaRollResponseDto {
pub item: GachaItemResponseDto,
}
-146
View File
@@ -1,146 +0,0 @@
use super::{
GachaClaimResponseDto, GachaClaimSchema, GachaCreateClaimRequestDto,
GachaCreateItemRequestDto, GachaCreateRollRequestDto, GachaItemResponseDto,
GachaItemSchema, GachaRollSchema,
};
use crate::{v1::AuthRepository, AppState, ResourceEnum};
use anyhow::{bail, Result};
use surrealdb::sql::{Id, Thing};
pub struct GachaRepository<'a> {
pub state: &'a AppState,
}
impl<'a> GachaRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
pub async fn query_gacha_claim_by_transaction_number(
&self,
transaction_number: String,
) -> Result<GachaClaimResponseDto> {
let db = &self.state.surrealdb;
let result = db
.select((ResourceEnum::GachaClaims.to_string(), transaction_number))
.await?;
match result {
Some(response) => Ok(response),
None => bail!("Gacha claim not found"),
}
}
pub async fn query_gacha_item_by_name(
&self,
name: String,
) -> Result<GachaItemResponseDto> {
let db = &self.state.surrealdb;
let result = db
.select((ResourceEnum::GachaItems.to_string(), name))
.await?;
match result {
Some(response) => Ok(response),
None => bail!("Gacha item not found"),
}
}
pub async fn query_gacha_claim_by_name(
&self,
name: String,
) -> Result<GachaItemResponseDto> {
let db = &self.state.surrealdb;
let result = db
.select((ResourceEnum::GachaClaims.to_string(), name))
.await?;
match result {
Some(response) => Ok(response),
None => bail!("Gacha item not found"),
}
}
pub async fn query_create_gacha_claim(
&self,
data: GachaCreateClaimRequestDto,
email: String,
) -> Result<String> {
let auth_repository = AuthRepository::new(self.state);
let db = &self.state.surrealdb;
let user = auth_repository.query_user_by_email(email).await?;
let user_thing =
Thing::from((ResourceEnum::Users.to_string(), Id::String(user.email)));
let item_thing = Thing::from((
ResourceEnum::GachaItems.to_string(),
Id::String(data.item_name.clone()),
));
let record: Option<GachaClaimSchema> = db
.create((
ResourceEnum::GachaClaims.to_string(),
&data.transaction_number,
))
.content(GachaClaimSchema {
transaction_number: data.transaction_number.clone(),
item: item_thing,
user: user_thing,
})
.await?;
match record {
Some(_) => Ok("Gacha claims successfully created".to_string()),
None => bail!("Failed to create gacha claims"),
}
}
pub async fn query_create_gacha_item(
&self,
data: GachaCreateItemRequestDto,
) -> Result<String> {
let db = &self.state.surrealdb;
let record: Option<GachaItemSchema> = db
.create((ResourceEnum::GachaItems.to_string(), data.item_name.clone()))
.content(GachaItemSchema {
item_name: data.item_name.clone(),
item_image: data.item_image.clone(),
})
.await?;
match record {
Some(_) => Ok("Gacha item successfully created".to_string()),
None => bail!("Failed to create gacha item"),
}
}
pub async fn query_create_gacha_roll(
&self,
data: GachaCreateRollRequestDto,
) -> Result<String> {
let db = &self.state.surrealdb;
let item_thing = Thing::from((
ResourceEnum::GachaItems.to_string(),
Id::String(data.item_name.clone()),
));
let record: Option<GachaRollSchema> = db
.create((ResourceEnum::GachaRolls.to_string(), data.item_name.clone()))
.content(GachaRollSchema {
weight: data.weight.clone(),
item: item_thing,
})
.await?;
match record {
Some(_) => Ok("Gacha roll successfully created".to_string()),
None => bail!("Failed to create gacha roll"),
}
}
}
-21
View File
@@ -1,21 +0,0 @@
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GachaClaimSchema {
pub transaction_number: String,
pub item: Thing,
pub user: Thing,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GachaRollSchema {
pub weight: String,
pub item: Thing,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GachaItemSchema {
pub item_image: String,
pub item_name: String,
}
-60
View File
@@ -1,60 +0,0 @@
use super::{
GachaCreateClaimRequestDto, GachaCreateItemRequestDto, GachaCreateRollRequestDto,
GachaRepository,
};
use crate::{common_response, extract_email, AppState};
use axum::{
http::{HeaderMap, StatusCode},
response::Response,
};
pub struct GachaService;
impl GachaService {
pub async fn mutation_create_gacha_claim(
payload: GachaCreateClaimRequestDto,
state: &AppState,
header: HeaderMap,
) -> Response {
let repository = GachaRepository::new(state);
let email = match extract_email(&header) {
Some(email) => email,
None => {
return common_response(
StatusCode::UNAUTHORIZED,
"Invalid or expired token",
);
}
};
match repository.query_create_gacha_claim(payload, email).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
}
}
pub async fn mutation_create_gacha_item(
payload: GachaCreateItemRequestDto,
state: &AppState,
) -> Response {
let repository = GachaRepository::new(state);
match repository.query_create_gacha_item(payload).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
}
}
pub async fn mutation_create_gacha_roll(
payload: GachaCreateRollRequestDto,
state: &AppState,
) -> Response {
let repository = GachaRepository::new(state);
match repository.query_create_gacha_roll(payload).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
}
}
}
-28
View File
@@ -1,28 +0,0 @@
use axum::{routing::post, Router};
pub mod gacha_controller;
pub mod gacha_dto;
pub mod gacha_repository;
pub mod gacha_schema;
pub mod gacha_service;
pub use gacha_dto::*;
pub use gacha_repository::*;
pub use gacha_schema::*;
pub use gacha_service::*;
pub fn gacha_router() -> Router {
Router::new()
.route(
"/claims/create",
post(gacha_controller::post_create_gacha_claim),
)
.route(
"/items/create",
post(gacha_controller::post_create_gacha_item),
)
.route(
"/rolls/create",
post(gacha_controller::post_create_gacha_roll),
)
}
+2 -5
View File
@@ -1,20 +1,17 @@
use axum::{middleware::from_fn, Router}; use axum::{middleware::from_fn, Router};
pub mod auth; pub mod auth;
pub mod docs; pub mod docs;
pub mod gacha;
pub mod users; pub mod users;
pub use auth::*; pub use auth::*;
pub use docs::*; pub use docs::*;
pub use gacha::*;
pub use users::*; pub use users::*;
pub async fn routes() -> Router { pub async fn routes() -> Router {
let public_routes = Router::new().nest("/auth", auth_router()); let public_routes = Router::new().nest("/auth", auth_router());
let protected_routes = Router::new() let protected_routes =
.nest("/gacha", gacha_router()) Router::new().layer(from_fn(auth::auth_middleware::auth_middleware));
.layer(from_fn(auth::auth_middleware::auth_middleware));
Router::new().merge(public_routes).merge(protected_routes) Router::new().merge(public_routes).merge(protected_routes)
} }
+1 -1
View File
@@ -1,4 +1,4 @@
use imphnen_backend_service::{apps, axum_init}; use axum_surrealdb_boilerplate::{apps, axum_init};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {