diff --git a/Cargo.lock b/Cargo.lock index 498fdc1..c3d03ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,6 +388,29 @@ dependencies = [ "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]] name = "backtrace" version = "0.3.74" @@ -1754,29 +1777,6 @@ dependencies = [ "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]] name = "indexmap" version = "1.9.3" diff --git a/Cargo.toml b/Cargo.toml index 7dcce22..4ea2f5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "imphnen-backend-service" +name = "axum-surrealdb-boilerplate" version = "0.1.0" edition = "2021" diff --git a/README.md b/README.md index c6c219e..615b232 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,72 @@ -# IMPHNEN CMS API - -This project is a CMS API for [IMPHNEN Website](https://imphnen.dev) +# Axum SurrealDB Boilerplate ## Features -- **RESTful API**: Provides endpoints for managing wedding reservations. -- **Database Integration**: Employs SeaORM for seamless database interactions. -- **Authentication**: Authentication and Middleware -- **CORS Handling**: Handling CORS with Tower HTTP CorsLayer -- **API Docs**: OpenAPI Swager Ready +- **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**: Ensure that Rust is installed on your system. You can install it from [rust-lang.org](https://www.rust-lang.org/). -- **Database**: Set up a Postgresql database and note the 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/) -- **Nix**: if you want build this project using nix, you need nix, You can install it from [nixos.org](https://nixos.org/) +- **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**: - ```bash - git clone https://github.com/IMPHNEN/imphnen-cms-api.git - cd imphnen-cms-api - ``` + - `git clone https://github.com/maulanasdqn/axum-surrealdb-boilerplate` 2. **Set Up Environment Variables**: - Copy a `.env.exanple` file: + - Copy `.env.example` and rename it to `.env` - ```env - cp .env.example .env - ``` + - **Windows**: Run the script: `./apply-env.ps1` + - **Unix-based systems (Linux, macOS, BSD)**: Run the script: `./apply-env.sh` 3. **Install Dependencies**: - ```bash - cargo check - ``` + - `cargo install .` - ```bash - cargo build --release - ``` - ```bash - cargo install sea-orm-cli - ``` +4. **Setup Database**: -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 - sea-orm-cli migrate up - ``` +5. **Start the Server**: -6. **Start the Server**: + - Install Cargo Watch `cargo install cargo-watch` + - Run it with cargo watch `cargo watch -x run` - ```bash - cargo run -q - ``` - - The API will be accessible at `http://localhost:3000/v1/docs`. + The API will be available at `http://localhost:3000/docs`. ## Docker 1. **Build the Docker Image**: - ```bash - docker build -t cms-api . - ``` - 2. **Run the Docker Container**: - ```bash - docker run -p 3000:3000 --env-file .env cms-api cms-api:latest - ``` + The API will be accessible at `http://localhost:3000/docs`. - The API will be accessible at `http://localhost:3000/v1/docs`. - - -## Using Nix as Builder +## Using Nix as Builder (Optional) 1. **Install Nix**: - ```bash - curl -L https://nixos.org/nix/install | sh - ``` - -2. **Switch to Nix Shell or Nix Flake**: - - ```bash - nix develop - ``` +2. **Enter Nix Shell or Use Nix Flakes**: 3. **Build the Project**: - ```bash - nix build - ``` - 4. **Run the Server**: - ```bash - nix run - ``` - - ## 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 @@ -116,4 +75,4 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file ## Acknowledgements - [Axum](https://github.com/tokio-rs/axum) -- [SeaORM](https://github.com/SeaQL/sea-orm) +- [SurrealDB](https://github.com/surrealdb/surrealdb) diff --git a/src/apps/v1/docs/docs_controller.rs b/src/apps/v1/docs/docs_controller.rs index 84998c4..03ed350 100644 --- a/src/apps/v1/docs/docs_controller.rs +++ b/src/apps/v1/docs/docs_controller.rs @@ -1,8 +1,7 @@ use crate::{ v1::{ - auth, gacha, AuthLoginRequestDto, AuthLoginResponsetDto, - AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, GachaCreateClaimRequestDto, - GachaCreateItemRequestDto, GachaCreateRollRequestDto, + auth, AuthLoginRequestDto, AuthLoginResponsetDto, AuthResendOtpRequestDto, + AuthVerifyEmailRequestDto, }, MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseSuccessDto, }; @@ -18,10 +17,7 @@ use utoipa::{ auth::auth_controller::post_login, auth::auth_controller::post_register, auth::auth_controller::post_verify_email, - 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 + auth::auth_controller::post_resend_otp ), components( schemas( @@ -33,14 +29,11 @@ use utoipa::{ AuthVerifyEmailRequestDto, AuthResendOtpRequestDto, ResponseSuccessDto, - GachaCreateClaimRequestDto, - GachaCreateItemRequestDto, - GachaCreateRollRequestDto ) ), info( - title = "IMPHNEN API", - description = "IMPHNEN API Documentation", + title = "Axum SurrealDB Boilerplate", + description = "Axum SurrealDB Documentation", version = "0.1.0", contact( name = "Maulana Sodiqin", diff --git a/src/apps/v1/gacha/gacha_controller.rs b/src/apps/v1/gacha/gacha_controller.rs deleted file mode 100644 index b29e5ba..0000000 --- a/src/apps/v1/gacha/gacha_controller.rs +++ /dev/null @@ -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, - Json(payload): Json, -) -> 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, - Json(payload): Json, -) -> 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, - Json(payload): Json, -) -> impl IntoResponse { - GachaService::mutation_create_gacha_roll(payload, &state).await -} diff --git a/src/apps/v1/gacha/gacha_dto.rs b/src/apps/v1/gacha/gacha_dto.rs deleted file mode 100644 index 28d7bd9..0000000 --- a/src/apps/v1/gacha/gacha_dto.rs +++ /dev/null @@ -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, -} diff --git a/src/apps/v1/gacha/gacha_repository.rs b/src/apps/v1/gacha/gacha_repository.rs deleted file mode 100644 index 69562b0..0000000 --- a/src/apps/v1/gacha/gacha_repository.rs +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 = 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 { - let db = &self.state.surrealdb; - - let record: Option = 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 { - let db = &self.state.surrealdb; - let item_thing = Thing::from(( - ResourceEnum::GachaItems.to_string(), - Id::String(data.item_name.clone()), - )); - - let record: Option = 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"), - } - } -} diff --git a/src/apps/v1/gacha/gacha_schema.rs b/src/apps/v1/gacha/gacha_schema.rs deleted file mode 100644 index 12a2e6b..0000000 --- a/src/apps/v1/gacha/gacha_schema.rs +++ /dev/null @@ -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, -} diff --git a/src/apps/v1/gacha/gacha_service.rs b/src/apps/v1/gacha/gacha_service.rs deleted file mode 100644 index f993d43..0000000 --- a/src/apps/v1/gacha/gacha_service.rs +++ /dev/null @@ -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()), - } - } -} diff --git a/src/apps/v1/gacha/mod.rs b/src/apps/v1/gacha/mod.rs deleted file mode 100644 index 9e06958..0000000 --- a/src/apps/v1/gacha/mod.rs +++ /dev/null @@ -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), - ) -} diff --git a/src/apps/v1/mod.rs b/src/apps/v1/mod.rs index 12fce28..ba45ebd 100644 --- a/src/apps/v1/mod.rs +++ b/src/apps/v1/mod.rs @@ -1,20 +1,17 @@ use axum::{middleware::from_fn, Router}; pub mod auth; pub mod docs; -pub mod gacha; pub mod users; pub use auth::*; pub use docs::*; -pub use gacha::*; pub use users::*; pub async fn routes() -> Router { let public_routes = Router::new().nest("/auth", auth_router()); - let protected_routes = Router::new() - .nest("/gacha", gacha_router()) - .layer(from_fn(auth::auth_middleware::auth_middleware)); + let protected_routes = + Router::new().layer(from_fn(auth::auth_middleware::auth_middleware)); Router::new().merge(public_routes).merge(protected_routes) } diff --git a/src/main.rs b/src/main.rs index 95451bd..ca3b8db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use imphnen_backend_service::{apps, axum_init}; +use axum_surrealdb_boilerplate::{apps, axum_init}; #[tokio::main] async fn main() {