diff --git a/.github/workflows/merge-ci.yml b/.github/workflows/merge-ci.yml index 3def846..802ebf0 100644 --- a/.github/workflows/merge-ci.yml +++ b/.github/workflows/merge-ci.yml @@ -32,25 +32,34 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - args: --release --all-features + args: --verbose --release --all-features - name: Run tests uses: actions-rs/cargo@v1 with: command: test - args: --all-features + args: --verbose --all-features - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: | + cargo fmt --all -- --check + if [ $? -ne 0 ]; then + echo "Formatting issues found. Applying automatic fixes..." + cargo fmt --all + git config --global user.name 'Lzyct-Bot' + git config --global user.email 'lazycatlabs@users.noreply.github.com' + git add . + git commit -m "style: apply automatic formatting" + git push + echo "Formatting issues have been automatically fixed. Please review the changes." + exit 1 + fi - name: Run clippy uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: -- -W warnings - name: Get branch name id: branch-name @@ -72,6 +81,7 @@ jobs: git push - name: Delete branch if merged + if: github.event.pull_request.merged == true uses: actions/github-script@v5 with: script: | diff --git a/src/core/middlewares/general.rs b/src/core/middlewares/general.rs index 8a10b09..431d366 100644 --- a/src/core/middlewares/general.rs +++ b/src/core/middlewares/general.rs @@ -25,10 +25,7 @@ impl FromRequest for GeneralMiddleware { // act as auth middleware fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future { // clone the request headers to avoids lifetime issues - let auth_header = request - .headers() - .get(AUTHORIZATION) - .cloned(); + let auth_header = request.headers().get(AUTHORIZATION).cloned(); Box::pin(async move { let auth_header = auth_header.ok_or_else(|| APIError::UnauthorizedMessage { @@ -41,12 +38,11 @@ impl FromRequest for GeneralMiddleware { }); } - let auth_str = - auth_header - .to_str() - .map_err(|_| APIError::UnauthorizedMessage { - message: "Invalid authorization headers".to_string(), - })?; + let auth_str = auth_header + .to_str() + .map_err(|_| APIError::UnauthorizedMessage { + message: "Invalid authorization headers".to_string(), + })?; let token = token_extractor(auth_str); let token_data = decode_token(&token).map_err(|_| APIError::Unauthorized)?; diff --git a/src/features/auth/data/repository/auth.rs b/src/features/auth/data/repository/auth.rs index 9232b79..d236ab7 100644 --- a/src/features/auth/data/repository/auth.rs +++ b/src/features/auth/data/repository/auth.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + use bcrypt::{verify, DEFAULT_COST}; use chrono::Utc; use diesel::prelude::*; @@ -45,10 +47,10 @@ impl IAuthRepository for AuthRepository { let login_history_params = LoginHistoryParams { user_id: user, ip_addr: login_params.ip_addr.unwrap(), - os_info: login_params.os_info, - device_info: login_params.device_info, + os_info: login_params.os_info.unwrap(), + device_info: login_params.device_info.unwrap(), login_timestamp: now, - fcm_token: login_params.fcm_token, + fcm_token: login_params.fcm_token.unwrap(), }; diesel::insert_into(login_history::table) @@ -80,11 +82,15 @@ impl IAuthRepository for AuthRepository { } fn login(&self, params: LoginParams) -> AppResult { + let param = params.clone(); + let email_param = param.email.as_deref().unwrap_or(""); + let password_param = param.password.as_deref().unwrap_or(""); + users::table - .filter(email.eq(¶ms.email)) + .filter(email.eq(&email_param)) .get_result::(&mut self.source.get().unwrap()) .map(|user| { - (!user.password.is_empty() && verify(¶ms.password, &user.password).unwrap()) + (!user.password.is_empty() && verify(&password_param, &user.password).unwrap()) .then(|| { self.add_user_session(user.id, params) .map(|login_session| { @@ -122,10 +128,13 @@ impl IAuthRepository for AuthRepository { .filter(user_id.eq(user)) .get_result::(&mut self.source.get().unwrap()) .map(|user| { - if !params.old_password.is_empty() - && verify(¶ms.old_password, &user.password).unwrap() + let old_password_param = ¶ms.old_password.unwrap_or("".to_string()); + let new_password_param = ¶ms.new_password.unwrap_or("".to_string()); + + if !&old_password_param.is_empty() + && verify(&old_password_param, &user.password).unwrap() { - let new_password = bcrypt::hash(¶ms.new_password, DEFAULT_COST).unwrap(); + let new_password = bcrypt::hash(&new_password_param, DEFAULT_COST).unwrap(); diesel::update(users::table) .filter(user_id.eq(&user.id)) .set(password.eq(&new_password)) diff --git a/src/features/auth/domain/usecase/dto.rs b/src/features/auth/domain/usecase/dto.rs index 2ed16af..0a27428 100644 --- a/src/features/auth/domain/usecase/dto.rs +++ b/src/features/auth/domain/usecase/dto.rs @@ -17,24 +17,47 @@ pub struct LoginHistoryParams { } camel_case_struct!(LoginParams { - #[validate(email(message = "Invalid email"))] - email: String, - #[validate(length(min = 3, max = 20))] - password: String, - #[validate(length(min = 0, message = "Can't be empty"))] + #[validate( + required(message = "field is required"), + email(message = "Invalid email"), + )] + email: Option, + #[validate( + required(message = "field is required"), + length(min = 3, max = 20), + )] + password: Option, + #[validate( + length(min = 1, message = "Can't be empty"), + )] ip_addr: Option, - #[validate(length(min = 0, message = "Can't be empty"))] - device_info: String, - #[validate(length(min = 0, message = "Can't be empty"))] - os_info: String, - #[validate(length(min = 0, message = "Can't be empty"))] - fcm_token: String + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] + device_info: Option, + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] + os_info: Option, + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] + fcm_token: Option }); camel_case_struct!(GeneralTokenParams { - #[validate(length(min = 0, message = "Can't be empty"))] + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] client_id: Option, - #[validate(length(min = 0, message = "Can't be empty"))] + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] client_secret: Option }); @@ -47,16 +70,21 @@ impl GeneralTokenParams { } camel_case_struct!(UpdatePasswordParams { - #[validate(length(min = 1, message = "Can't be empty"))] - old_password: String, #[validate( - length(min = 6, message = "Must be at least 6 characters"), - must_match(other = "confirm_password", message = "Password not match") + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), )] - new_password: String, + old_password: Option, #[validate( - length(min = 6, message = "Must be at least 6 characters"), - must_match(other = "new_password", message = "Password not match") + required(message = "field is required"), + length(min = 6, message = "Must be at least 6 characters"), + must_match(other = "confirm_password", message = "Password not match") )] - confirm_password: String + new_password: Option, + #[validate( + required(message = "field is required"), + length(min = 6, message = "Must be at least 6 characters"), + must_match(other = "new_password", message = "Password not match") + )] + confirm_password: Option }); diff --git a/src/features/general/domain/mod.rs b/src/features/general/domain/mod.rs index ed46e91..581feeb 100644 --- a/src/features/general/domain/mod.rs +++ b/src/features/general/domain/mod.rs @@ -1 +1 @@ -pub mod usecase; \ No newline at end of file +pub mod usecase; diff --git a/src/features/general/domain/usecase/dto.rs b/src/features/general/domain/usecase/dto.rs index 147e33c..1d4626b 100644 --- a/src/features/general/domain/usecase/dto.rs +++ b/src/features/general/domain/usecase/dto.rs @@ -1,12 +1,21 @@ use crate::camel_case_struct; camel_case_struct!(SendEmailParams { - #[validate(length(min = 0, message = "Can't be empty"))] - email: String, - #[validate(length(min = 0, message = "Can't be empty"))] - name: String, - #[validate(length(min = 0, message = "Can't be empty"))] - subject: String, + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] + email: Option, + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] + name: Option, + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] + subject: Option, text_content: Option, html_content: Option }); diff --git a/src/features/general/domain/usecase/mod.rs b/src/features/general/domain/usecase/mod.rs index 6c29114..a07dce5 100644 --- a/src/features/general/domain/usecase/mod.rs +++ b/src/features/general/domain/usecase/mod.rs @@ -1 +1 @@ -pub mod dto; \ No newline at end of file +pub mod dto; diff --git a/src/features/general/general_controller.rs b/src/features/general/general_controller.rs index 22d7e81..70065ed 100644 --- a/src/features/general/general_controller.rs +++ b/src/features/general/general_controller.rs @@ -2,15 +2,26 @@ use actix_web::{ web::{self, Json}, HttpResponse, }; +use validator::Validate; -use crate::utils::mail_sender::send_email; - -use crate::core::{middlewares::state::AppState, response::ResponseBody, types::AppResult}; -use super::domain::usecase::dto::SendEmailParams; +use crate::{ + core::{error::APIError, middlewares::general::GeneralMiddleware}, + features::general::domain::usecase::dto::SendEmailParams, +}; +use crate::{ + core::{middlewares::state::AppState, response::ResponseBody, types::AppResult}, + utils::mail_sender::send_email, +}; pub async fn test_email( - _: web::Data, + _: GeneralMiddleware, + __: web::Data, params: Json, ) -> AppResult { - send_email(params.0).await.map(|_| ResponseBody::<()>::success(None).into()) + params.validate().map_err(|e| APIError::BadRequest { + message: e.to_string(), + })?; + send_email(params.0) + .await + .map(|_| ResponseBody::<()>::success(None).into()) } diff --git a/src/features/general/mod.rs b/src/features/general/mod.rs index e012b7f..3e27b69 100644 --- a/src/features/general/mod.rs +++ b/src/features/general/mod.rs @@ -1,2 +1,2 @@ -pub mod general_controller; -pub mod domain; \ No newline at end of file +pub mod domain; +pub mod general_controller; \ No newline at end of file diff --git a/src/features/mod.rs b/src/features/mod.rs index 87cfef9..85b89df 100644 --- a/src/features/mod.rs +++ b/src/features/mod.rs @@ -1,3 +1,3 @@ pub mod auth; +pub mod general; pub mod user; -pub mod general; \ No newline at end of file diff --git a/src/features/user/domain/usecase/dto.rs b/src/features/user/domain/usecase/dto.rs index 4092221..ce250af 100644 --- a/src/features/user/domain/usecase/dto.rs +++ b/src/features/user/domain/usecase/dto.rs @@ -6,25 +6,30 @@ use uuid::Uuid; use crate::{camel_case_struct, features::user::data::models::user::User, schema::users}; camel_case_struct!(RegisterParams { - #[validate(email(message = "Invalid email"))] - email: String, - #[validate(length(min = 0, message = "Can't be empty"))] - name: String, - #[validate(length( - min = 3, - max = 20, - message = "Password must be between 3 and 20 characters" - ))] - password: String + #[validate( + required(message = "field is required"), + email(message = "Invalid email"), + )] + email: Option, + #[validate( + required(message = "field is required"), + length(min = 1, message = "Can't be empty"), + )] + name: Option, + #[validate( + required(message = "field is required"), + length(min = 3,max = 20,message = "Password must be between 3 and 20 characters"), + )] + password: Option }); impl From for User { fn from(params: RegisterParams) -> Self { User { id: Uuid::new_v4(), - email: params.email, - name: params.name, - password: params.password, + email: params.email.unwrap(), + name: params.name.unwrap(), + password: params.password.unwrap(), role: String::from("user"), created_at: Utc::now().naive_utc(), updated_at: Utc::now().naive_utc(), diff --git a/src/features/user/user_controller.rs b/src/features/user/user_controller.rs index cd68a8c..f599867 100644 --- a/src/features/user/user_controller.rs +++ b/src/features/user/user_controller.rs @@ -1,7 +1,6 @@ use actix_web::web::Json; use actix_web::{web, HttpResponse}; -use crate::core::error::APIError; use crate::core::middlewares::general::GeneralMiddleware; use crate::{ core::{ diff --git a/src/utils/macros.rs b/src/utils/macros.rs index 59c9eaa..1dcdb1b 100644 --- a/src/utils/macros.rs +++ b/src/utils/macros.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! camel_case_struct { ($name:ident { $( $field:ident: $type:ty ),* }) => { - #[derive(serde::Serialize, serde::Deserialize, Debug)] + #[derive(serde::Serialize, serde::Deserialize, Debug,Clone)] #[serde(rename_all = "camelCase")] pub struct $name { $( pub $field: $type ),* @@ -16,7 +16,7 @@ macro_rules! camel_case_struct { ),+ $(,)? } ) => { - #[derive(serde::Serialize, serde::Deserialize, validator::Validate, Debug)] + #[derive(serde::Serialize, serde::Deserialize, validator::Validate, Debug,Clone)] #[serde(rename_all = "camelCase")] pub struct $name { $( diff --git a/src/utils/mail_sender.rs b/src/utils/mail_sender.rs index af798fe..472c3e8 100644 --- a/src/utils/mail_sender.rs +++ b/src/utils/mail_sender.rs @@ -1,12 +1,11 @@ -use actix_web::HttpResponse; use dotenv_codegen::dotenv; use reqwest::Client; use serde_json::json; -use crate::core::{error::APIError, response::ResponseBody, types::AppResult}; +use crate::core::{error::APIError, types::AppResult}; use crate::features::general::domain::usecase::dto::SendEmailParams; -pub async fn send_email(params: SendEmailParams) -> AppResult { +pub async fn send_email(params: SendEmailParams) -> AppResult { let client = Client::new(); let mut message = json!({ @@ -46,14 +45,15 @@ pub async fn send_email(params: SendEmailParams) -> AppResult { .await .map_err(|_| APIError::InternalError)?; - println!("Email sent to {}", ¶ms.email); - println!("Body: {:?}", body); - println!("Response: {:?}", result); + let email = ¶ms.email.unwrap_or("Unknown".to_string()); + println!("Email sent to {}", &email); + println!("Body: {:#?}", body); + println!("Response: {:#?}", result); if result.status().is_client_error() { return Err(APIError::BadRequest { - message: format!("Failed to send email to {}", ¶ms.email).to_string(), + message: format!("Failed to send email to {}", &email).to_string(), }); } - Ok(ResponseBody::<()>::success(None).into()) -} \ No newline at end of file + Ok("Email sent successfully".to_string()) +}