From 7ab036f5f74444b72ec1e999eb81882da607537b Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Fri, 14 Mar 2025 17:50:37 +0700 Subject: [PATCH] feat: user relation --- Cargo.lock | 16 +++++++ Cargo.toml | 3 +- src/apps/mod.rs | 5 ++- src/apps/v1/auth/auth_controller.rs | 24 ++++++++++- src/apps/v1/auth/auth_dto.rs | 7 +++ src/apps/v1/auth/auth_repository.rs | 49 +++++++++++++++++---- src/apps/v1/auth/auth_service.rs | 30 ++++++------- src/apps/v1/docs/docs_controller.rs | 61 +++++++++++++++++++++++++++ src/apps/v1/docs/mod.rs | 8 ++++ src/apps/v1/gacha/gacha_controller.rs | 20 +++++++++ src/apps/v1/gacha/gacha_dto.rs | 17 ++++++++ src/apps/v1/gacha/gacha_repository.rs | 55 ++++++++++++++++++++++++ src/apps/v1/gacha/gacha_schema.rs | 8 ++++ src/apps/v1/gacha/gacha_service.rs | 19 +++++++++ src/apps/v1/gacha/mod.rs | 16 +++++++ src/apps/v1/mod.rs | 8 +++- src/apps/v1/users/mod.rs | 1 + src/apps/v1/users/users_schema.rs | 10 +++++ src/libs/enviroment/mod.rs | 5 +++ src/libs/redisdb/key.rs | 19 +++++++++ src/libs/redisdb/mod.rs | 3 ++ src/libs/surrealdb/resource.rs | 2 + 22 files changed, 358 insertions(+), 28 deletions(-) create mode 100644 src/apps/v1/docs/docs_controller.rs create mode 100644 src/apps/v1/docs/mod.rs create mode 100644 src/apps/v1/gacha/gacha_controller.rs create mode 100644 src/apps/v1/gacha/gacha_dto.rs create mode 100644 src/apps/v1/gacha/gacha_repository.rs create mode 100644 src/apps/v1/gacha/gacha_schema.rs create mode 100644 src/apps/v1/gacha/gacha_service.rs create mode 100644 src/apps/v1/gacha/mod.rs create mode 100644 src/apps/v1/users/users_schema.rs create mode 100644 src/libs/redisdb/key.rs diff --git a/Cargo.lock b/Cargo.lock index b54f849..5d55948 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea50b14b7a4b9343f8c627a7a53c52076482bd4bdad0a24fd3ec533ed616cc2c" +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + [[package]] name = "approx" version = "0.4.0" @@ -740,7 +746,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", + "futures-core", "memchr", + "pin-project-lite", + "tokio", + "tokio-util", ] [[package]] @@ -1736,6 +1746,7 @@ dependencies = [ name = "imphnen-cms-be" version = "0.1.0" dependencies = [ + "anyhow", "argon2", "axum", "chrono", @@ -2822,13 +2833,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37ec3fd44bea2ec947ba6cc7634d7999a6590aca7c35827c250bc0de502bda6" dependencies = [ "arc-swap", + "bytes", "combine", + "futures-util", "itoa", "num-bigint", "percent-encoding", + "pin-project-lite", "ryu", "sha1_smol", "socket2", + "tokio", + "tokio-util", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 77e0a16..333a7c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,11 @@ jsonwebtoken = "9.3.1" chrono = "0.4.39" utoipa = { version = "5.3.1", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] } -redis = "0.28.2" +redis = { version = "0.28.2", features = ["tokio-comp"] } lettre = { version = "0.11.12", features = ["tokio1-native-tls"] } surrealdb = { version = "2.2.1", features = ["protocol-http"] } thiserror = "2.0.11" +anyhow = "1.0.97" [profile.release] lto = "fat" diff --git a/src/apps/mod.rs b/src/apps/mod.rs index e49a135..646ad6e 100644 --- a/src/apps/mod.rs +++ b/src/apps/mod.rs @@ -1,5 +1,6 @@ use crate::{AppState, RedisClient, SurrealClient}; -use axum::{Extension, Router}; +use axum::{response::Redirect, routing::get, Extension, Router}; +use utoipa_swagger_ui::SwaggerUi; pub mod v1; pub mod v2; @@ -7,7 +8,9 @@ pub mod v2; pub async fn apps(surrealdb: SurrealClient, redisdb: RedisClient) -> Router { let state = AppState { surrealdb, redisdb }; Router::new() + .route("/", get(Redirect::to("/docs"))) .nest("/v1", v1::routes().await) .nest("/v2", v2::routes().await) + .merge(SwaggerUi::new("/docs").url("/openapi.json", v1::docs_router())) .layer(Extension(state)) } diff --git a/src/apps/v1/auth/auth_controller.rs b/src/apps/v1/auth/auth_controller.rs index 663aa22..86cdae3 100644 --- a/src/apps/v1/auth/auth_controller.rs +++ b/src/apps/v1/auth/auth_controller.rs @@ -1,7 +1,19 @@ use super::{AuthLoginRequestDto, AuthRegisterRequestDto, AuthService}; -use crate::AppState; +use crate::{v1::AuthLoginResponsetDto, AppState}; use axum::{response::IntoResponse, Extension, Json}; +use crate::{MessageResponseDto, ResponseSuccessDto}; + +#[utoipa::path( + post, + path = "/v1/auth/login", + request_body = AuthLoginRequestDto, + responses( + (status = 200, description = "Login successful", body = ResponseSuccessDto), + (status = 401, description = "Unauthorized", body = MessageResponseDto) + ), + tag = "Authentication" +)] pub async fn post_login( Extension(state): Extension, Json(payload): Json, @@ -9,6 +21,16 @@ pub async fn post_login( AuthService::mutation_login(payload, &state).await } +#[utoipa::path( + post, + path = "/v1/auth/register", + request_body = AuthRegisterRequestDto, + responses( + (status = 200, description = "Login successful", body = MessageResponseDto), + (status = 401, description = "Unauthorized", body = MessageResponseDto) + ), + tag = "Authentication" +)] pub async fn post_register( Extension(state): Extension, Json(payload): Json, diff --git a/src/apps/v1/auth/auth_dto.rs b/src/apps/v1/auth/auth_dto.rs index 71c180b..cdba5ff 100644 --- a/src/apps/v1/auth/auth_dto.rs +++ b/src/apps/v1/auth/auth_dto.rs @@ -27,3 +27,10 @@ pub struct AuthRegisterRequestDto { pub password: String, pub fullname: String, } + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct AuthQueryByEmailResponse { + pub email: String, + pub fullname: String, + pub password: String, +} diff --git a/src/apps/v1/auth/auth_repository.rs b/src/apps/v1/auth/auth_repository.rs index 34dfcfd..bbd8a39 100644 --- a/src/apps/v1/auth/auth_repository.rs +++ b/src/apps/v1/auth/auth_repository.rs @@ -1,7 +1,8 @@ -use crate::{v1::UsersItemDto, AppState, ResourceEnum}; -use std::error::Error; +use crate::{v1::UsersItemDto, AppState, RedisKeyEnum, ResourceEnum}; +use anyhow::{bail, Result}; +use redis::Commands; -use super::AuthRegisterRequestDto; +use super::{AuthQueryByEmailResponse, AuthRegisterRequestDto}; pub struct AuthRepository<'a> { state: &'a AppState, @@ -12,24 +13,56 @@ impl<'a> AuthRepository<'a> { Self { state } } + pub fn query_store_user_data( + &self, + user: AuthRegisterRequestDto, + ) -> Result { + let redis_key = format!("{}:{}", RedisKeyEnum::User, user.email.clone()); + match &self.state.redisdb.get_connection().and_then(|mut conn| { + conn.set_ex::<_, String, ()>( + &redis_key, + serde_json::to_string(&user).unwrap_or_default(), + 86400, + ) + }) { + Ok(_) => Ok("Success store user data".to_string()), + Err(err) => Ok(format!("Redis storage failed: {}", err)), + } + } + + pub fn query_get_stored_user(&self, email: String) -> Result { + let redis_key = format!("{}:{}", RedisKeyEnum::User, email); + let mut conn = self.state.redisdb.get_connection()?; + + let data: Option = conn.get(&redis_key)?; + + match data { + Some(user_json) => { + let user: UsersItemDto = serde_json::from_str(&user_json)?; + Ok(user) + } + None => bail!("No stored user data found for email"), + } + } + pub async fn query_user_by_email( &self, email: String, - ) -> Result> { + ) -> Result { let db = &self.state.surrealdb; let result = db.select((ResourceEnum::Users.to_string(), email)).await?; match result { - Some(user) => Ok(user), - None => Err("User not found for email".into()), + Some(response) => Ok(response), + None => bail!("User not found"), } } pub async fn query_create_user( &self, data: AuthRegisterRequestDto, - ) -> Result> { + ) -> Result { let db = &self.state.surrealdb; let record: Option = db @@ -39,7 +72,7 @@ impl<'a> AuthRepository<'a> { match record { Some(_) => Ok("Success create user".into()), - None => Err("Failed to create user".into()), + None => bail!("Failed to create user"), } } } diff --git a/src/apps/v1/auth/auth_service.rs b/src/apps/v1/auth/auth_service.rs index 151374f..57ce684 100644 --- a/src/apps/v1/auth/auth_service.rs +++ b/src/apps/v1/auth/auth_service.rs @@ -1,5 +1,4 @@ use axum::{http::StatusCode, response::Response}; -use redis::Commands; use super::{ AuthLoginRequestDto, AuthLoginResponsetDto, AuthRegisterRequestDto, @@ -55,22 +54,21 @@ impl AuthService { }, }; - let redis_key = - format!("authenticated_users_data:{}", payload.email.clone()); - - match state.redisdb.get_connection().and_then(|mut conn| { - conn.set_ex::<_, String, ()>( - &redis_key, - serde_json::to_string(&user).unwrap_or_default(), - 86400, - ) - }) { - Ok(_) => success_response(response), - Err(err) => common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &format!("Redis storage failed: {}", err), - ), + if !repository + .query_store_user_data(AuthRegisterRequestDto { + fullname: user.fullname, + password: user.password, + email: user.email, + }) + .is_ok() + { + return common_response( + StatusCode::BAD_REQUEST, + "Failed to store data", + ); } + + success_response(response) } Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()), } diff --git a/src/apps/v1/docs/docs_controller.rs b/src/apps/v1/docs/docs_controller.rs new file mode 100644 index 0000000..b93bd37 --- /dev/null +++ b/src/apps/v1/docs/docs_controller.rs @@ -0,0 +1,61 @@ +use crate::{ + v1::{auth, AuthLoginRequestDto, AuthLoginResponsetDto}, + MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseSuccessDto, +}; + +use utoipa::{ + openapi::security::{Http, HttpAuthScheme, SecurityScheme}, + Modify, OpenApi, +}; + +#[derive(OpenApi)] +#[openapi( + paths( + auth::auth_controller::post_login, + auth::auth_controller::post_register + ), + components( + schemas( + MetaRequestDto, + MetaResponseDto, + MessageResponseDto, + + AuthLoginRequestDto, + AuthLoginResponsetDto, + ResponseSuccessDto, + ) + ), + info( + title = "IMPHNEN API", + description = "IMPHNEN API Documentation", + 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"), + (name = "Users", description = "List of Users 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)), + ); + } + } +} diff --git a/src/apps/v1/docs/mod.rs b/src/apps/v1/docs/mod.rs new file mode 100644 index 0000000..62ced02 --- /dev/null +++ b/src/apps/v1/docs/mod.rs @@ -0,0 +1,8 @@ +use utoipa::OpenApi; + +pub mod docs_controller; +pub use docs_controller::*; + +pub fn docs_router() -> utoipa::openapi::OpenApi { + ApiDoc::openapi() +} diff --git a/src/apps/v1/gacha/gacha_controller.rs b/src/apps/v1/gacha/gacha_controller.rs new file mode 100644 index 0000000..a7a8d70 --- /dev/null +++ b/src/apps/v1/gacha/gacha_controller.rs @@ -0,0 +1,20 @@ +use super::{GachaRequestDto, GachaService}; +use crate::{AppState, MessageResponseDto}; +use axum::{response::IntoResponse, Extension, Json}; + +#[utoipa::path( + post, + path = "/v1/gacha/create", + request_body = GachaRequestDto, + responses( + (status = 200, description = "Create gacha successful", body = MessageResponseDto), + (status = 401, description = "Create gacha failed", body = MessageResponseDto) + ), + tag = "Gacha" +)] +pub async fn post_create_gacha( + Extension(state): Extension, + Json(payload): Json, +) -> impl IntoResponse { + GachaService::mutation_create_gacha(payload, &state).await +} diff --git a/src/apps/v1/gacha/gacha_dto.rs b/src/apps/v1/gacha/gacha_dto.rs new file mode 100644 index 0000000..a1ddad2 --- /dev/null +++ b/src/apps/v1/gacha/gacha_dto.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::v1::UsersItemDto; + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct GachaRequestDto { + pub email: String, + pub fullname: String, + pub transaction_number: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct GachaResponseDto { + pub transaction_number: String, + pub user: UsersItemDto, +} diff --git a/src/apps/v1/gacha/gacha_repository.rs b/src/apps/v1/gacha/gacha_repository.rs new file mode 100644 index 0000000..18c83a9 --- /dev/null +++ b/src/apps/v1/gacha/gacha_repository.rs @@ -0,0 +1,55 @@ +use super::{GachaRequestDto, GachaResponseDto, GachaSchema}; +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_by_transaction_number( + &self, + transaction_number: String, + ) -> Result { + let db = &self.state.surrealdb; + + let result = db + .select((ResourceEnum::Gacha.to_string(), transaction_number)) + .await?; + + match result { + Some(response) => Ok(response), + None => bail!("Gacha not found"), + } + } + + pub async fn query_create_gacha(&self, data: GachaRequestDto) -> Result { + let auth_repository = AuthRepository::new(self.state); + let db = &self.state.surrealdb; + + let user = auth_repository + .query_user_by_email(data.email.clone()) + .await?; + + let user_thing = + Thing::from((ResourceEnum::Users.to_string(), Id::String(user.email))); + + let record: Option = db + .create((ResourceEnum::Gacha.to_string(), &data.transaction_number)) + .content(GachaSchema { + transaction_number: data.transaction_number.clone(), + user: user_thing, + }) + .await?; + + match record { + Some(_) => Ok("Gacha successfully created".to_string()), + None => bail!("Failed to create gacha record"), + } + } +} diff --git a/src/apps/v1/gacha/gacha_schema.rs b/src/apps/v1/gacha/gacha_schema.rs new file mode 100644 index 0000000..57c22d0 --- /dev/null +++ b/src/apps/v1/gacha/gacha_schema.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GachaSchema { + pub transaction_number: String, + pub user: Thing, +} diff --git a/src/apps/v1/gacha/gacha_service.rs b/src/apps/v1/gacha/gacha_service.rs new file mode 100644 index 0000000..a28fa78 --- /dev/null +++ b/src/apps/v1/gacha/gacha_service.rs @@ -0,0 +1,19 @@ +use super::{GachaRepository, GachaRequestDto}; +use crate::{common_response, AppState}; +use axum::{http::StatusCode, response::Response}; + +pub struct GachaService; + +impl GachaService { + pub async fn mutation_create_gacha( + payload: GachaRequestDto, + state: &AppState, + ) -> Response { + let repository = GachaRepository::new(state); + + match repository.query_create_gacha(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 new file mode 100644 index 0000000..ca39215 --- /dev/null +++ b/src/apps/v1/gacha/mod.rs @@ -0,0 +1,16 @@ +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("/create", post(gacha_controller::post_create_gacha)) +} diff --git a/src/apps/v1/mod.rs b/src/apps/v1/mod.rs index b9b4e93..b8365df 100644 --- a/src/apps/v1/mod.rs +++ b/src/apps/v1/mod.rs @@ -1,11 +1,17 @@ use axum::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 { - Router::new().nest("/auth", auth::auth_router()) + Router::new() + .nest("/auth", auth_router()) + .nest("/gacha", gacha_router()) } diff --git a/src/apps/v1/users/mod.rs b/src/apps/v1/users/mod.rs index 7b39807..169dbdc 100644 --- a/src/apps/v1/users/mod.rs +++ b/src/apps/v1/users/mod.rs @@ -1,3 +1,4 @@ pub mod users_dto; +pub mod users_schema; pub use users_dto::*; diff --git a/src/apps/v1/users/users_schema.rs b/src/apps/v1/users/users_schema.rs new file mode 100644 index 0000000..74ba992 --- /dev/null +++ b/src/apps/v1/users/users_schema.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct UsersSchema { + pub id: String, + pub email: String, + pub fullname: String, + pub password: String, +} diff --git a/src/libs/enviroment/mod.rs b/src/libs/enviroment/mod.rs index 953c9be..dc6fa7c 100644 --- a/src/libs/enviroment/mod.rs +++ b/src/libs/enviroment/mod.rs @@ -11,6 +11,7 @@ pub struct Env { pub smtp_name: String, pub smpt_host: String, pub redis_hostname: String, + pub redis_port: u16, pub fe_url: String, pub rust_env: String, pub minio_endpoint: String, @@ -26,6 +27,10 @@ impl Env { .unwrap_or("3000".to_string()) .parse() .unwrap_or(3000), + redis_port: env::var("REDIS_PORT") + .unwrap_or("5436".to_string()) + .parse() + .unwrap_or(5436), access_token_secret: env::var("ACCESS_TOKEN_SECRET") .unwrap_or("default_access_secret".to_string()), refresh_token_secret: env::var("REFRESH_TOKEN_SECRET") diff --git a/src/libs/redisdb/key.rs b/src/libs/redisdb/key.rs new file mode 100644 index 0000000..c073ca0 --- /dev/null +++ b/src/libs/redisdb/key.rs @@ -0,0 +1,19 @@ +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RedisKeyEnum { + User, + Token, + Otp, +} + +impl fmt::Display for RedisKeyEnum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + RedisKeyEnum::User => "user", + RedisKeyEnum::Token => "token", + RedisKeyEnum::Otp => "otp", + }; + write!(f, "{}", str) + } +} diff --git a/src/libs/redisdb/mod.rs b/src/libs/redisdb/mod.rs index 5c18d75..03ff721 100644 --- a/src/libs/redisdb/mod.rs +++ b/src/libs/redisdb/mod.rs @@ -2,6 +2,9 @@ use super::Env; use redis::Client; use redis::RedisResult; +pub mod key; +pub use key::*; + pub async fn redisdb_init() -> RedisResult { let env = Env::new(); let host_name = env.redis_hostname; diff --git a/src/libs/surrealdb/resource.rs b/src/libs/surrealdb/resource.rs index a4f8c4e..9a16d1a 100644 --- a/src/libs/surrealdb/resource.rs +++ b/src/libs/surrealdb/resource.rs @@ -2,6 +2,7 @@ use std::fmt; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResourceEnum { + Gacha, Users, Roles, Permissions, @@ -13,6 +14,7 @@ impl fmt::Display for ResourceEnum { ResourceEnum::Users => "app_users", ResourceEnum::Roles => "app_roles", ResourceEnum::Permissions => "app_permissions", + ResourceEnum::Gacha => "app_gacha", }; write!(f, "{}", str) }