From acd15002ca9884bfdd7d3e1a362acfda1f9e40c2 Mon Sep 17 00:00:00 2001 From: Rajdeep Sengupta Date: Thu, 25 Jul 2024 14:17:23 +0530 Subject: [PATCH 1/3] Block user functionality --- makefile | 8 +- src/core/user.rs | 189 +++++++++++++++++++++++-------- src/errors.rs | 6 + src/handlers/password_handler.rs | 45 +++++--- src/handlers/user_handler.rs | 85 +++++++++++++- src/main.rs | 3 +- src/models/password_model.rs | 2 +- src/models/user_model.rs | 23 ++++ src/routes/user_routes.rs | 3 +- 9 files changed, 283 insertions(+), 81 deletions(-) diff --git a/makefile b/makefile index 4371256..b680c6a 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ # Define the list of required environment variables for the root .env file -REQUIRED_ENV_VARS = PORT SERVER_KEK EMAIL_PASSWORD EMAIL MAIL_NAME SMTP_DOMAIN SMTP_PORT MONGO_INITDB_ROOT_USERNAME MONGO_INITDB_ROOT_PASSWORD +REQUIRED_ENV_VARS = PORT SERVER_KEK EMAIL_PASSWORD EMAIL MAIL_NAME SMTP_DOMAIN SMTP_PORT MONGO_INITDB_ROOT_USERNAME MONGO_INITDB_ROOT_PASSWORD HOST # Default target to check and update .env file .PHONY: setup @@ -62,14 +62,14 @@ check-private-key: fi # Target to run the server using Docker Compose with --build option -.PHONY: run-server +.PHONY: build-run-server build-run-server: setup - docker-compose up --build + docker compose up --build # Target to run the server using Docker Compose without --build option .PHONY: run-server run-server: setup - docker-compose up + docker compose up # Target to build the ui / next app using npm i .PHONY: build-ui diff --git a/src/core/user.rs b/src/core/user.rs index cda499d..16cfa17 100644 --- a/src/core/user.rs +++ b/src/core/user.rs @@ -2,7 +2,7 @@ use std::env; use crate::{ errors::{Error, Result}, - models::{password_model::ForgetPasswordRequest, user_model::{EmailVerificationRequest, UserResponse}}, + models::{password_model::ForgetPasswordRequest, user_model::{EmailVerificationRequest, UserBlockRequest, UserResponse}}, traits::{decryption::Decrypt, encryption::Encrypt}, utils::{ email_utils::Email, encryption_utils::Encryption, password_utils::Password @@ -48,7 +48,6 @@ impl User { updated_at: Some(DateTime::now()), } } - pub async fn encrypt_and_add(&self, mongo_client: &Client, dek: &str) -> Result { let db = mongo_client.database("auth"); let mut user = self.clone(); @@ -63,7 +62,6 @@ impl User { } } } - pub async fn get_from_email(mongo_client: &Client, email: &str) -> Result { let user_collection: Collection = mongo_client.database("auth").collection("users"); @@ -97,7 +95,6 @@ impl User { }), } } - pub async fn get_from_uid(mongo_client: &Client, uid: &str) -> Result { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); @@ -128,7 +125,6 @@ impl User { }), } } - pub async fn get_all(mongo_client: &Client) -> Result> { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); @@ -176,11 +172,7 @@ impl User { Ok(users) } - - pub async fn get_recent( - mongo_client: &Client, - limit: i64, - ) -> Result> { + pub async fn get_recent(mongo_client: &Client, limit: i64) -> Result> { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); @@ -240,12 +232,7 @@ impl User { Ok(recent_users) } - - pub async fn update_role( - mongo_client: &Client, - email: &str, - role: &str, - ) -> Result { + pub async fn update_role(mongo_client: &Client,email: &str,role: &str) -> Result { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); @@ -291,12 +278,7 @@ impl User { } } } - - pub async fn toggle_account_activation( - mongo_client: &Client, - email: &str, - is_active: &bool, - ) -> Result { + pub async fn toggle_account_activation(mongo_client: &Client, email: &str, is_active: &bool) -> Result { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); let dek_data = match Dek::get(&mongo_client, email).await { @@ -341,11 +323,7 @@ impl User { } } } - - pub async fn increase_failed_login_attempt( - mongo_client: &Client, - email: &str, - ) -> Result { + pub async fn increase_failed_login_attempt(mongo_client: &Client, email: &str) -> Result { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); let dek_data = match Dek::get(&mongo_client, email).await { @@ -535,11 +513,7 @@ impl User { } } } - - pub async fn reset_failed_login_attempt( - mongo_client: &Client, - email: &str, - ) -> Result { + pub async fn reset_failed_login_attempt(mongo_client: &Client,email: &str ) -> Result { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); let dek_data = match Dek::get(&mongo_client, email).await { @@ -584,7 +558,6 @@ impl User { } } } - pub async fn change_password(mongo_client: &Client, email: &str, old_password: &str, new_password: &str) -> Result { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); @@ -649,7 +622,6 @@ impl User { } } } - pub async fn forget_password_request(mongo_client: &Client, email: &str) -> Result { // check if the user exists let db = mongo_client.database("auth"); @@ -669,7 +641,7 @@ impl User { // create a new doc in forget_password_requests collection let new_doc = ForgetPasswordRequest { _id: ObjectId::new(), - id: uuid::Uuid::new().to_string(), + req_id: uuid::Uuid::new().to_string(), email: Encryption::encrypt_data(&email, &dek_data.dek), is_used: false, valid_till: ten_minutes_from_now, @@ -687,18 +659,12 @@ impl User { &user.name, &user.email, &"Reset Password", - &format!("Please click on the link to reset your password: http://localhost:8080/forget-reset/{}", new_doc.id), + &format!("Please click on the link to reset your password: {}/forget-reset/{}", dotenv::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()), new_doc.req_id), ).send().await; Ok("Forget password request sent to email successfully".to_string()) } - - pub async fn forget_password_reset( - mongo_client: &Client, - req_id: &str, - email: &str, - new_password: &str - ) -> Result { + pub async fn forget_password_reset(mongo_client: &Client,req_id: &str, email: &str,new_password: &str) -> Result { let db = mongo_client.database("auth"); let user_collection: Collection = db.collection("users"); let forget_password_requests_collection: Collection = @@ -714,7 +680,7 @@ impl User { // check if forget password request exists let forget_password_request = forget_password_requests_collection - .find_one(doc! { "id": &req_id }, None) + .find_one(doc! { "req_id": &req_id }, None) .await .unwrap() .unwrap(); @@ -772,7 +738,7 @@ impl User { // update the forget password request as used forget_password_requests_collection .find_one_and_update( - doc! { "id": &req_id }, + doc! { "req_id": &req_id }, doc! { "$set": { "is_used": true, @@ -783,19 +749,24 @@ impl User { ) .await .unwrap(); + + let block_req_id = match User::block_request(&mongo_client, &email, &user.uid).await { + Ok(req_id) => req_id, + Err(e) => { + return Err(e); + } + }; // send a email to the user that the password has been updated Email::new( &user.name, &email, &"Password Updated", - &"Your password has been updated successfully. If it was not you please take action as soon as possible", + &format!("Your password has been updated successfully. If it was not you please take action as soon as possible. Click on the link to block your account temporarily: {}/block-account/{} . If you want to re-activate your account then please contact us by simply replying to this email.", dotenv::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()), block_req_id) ).send().await; Ok("Password updated successfully".to_string()) - } - pub async fn verify_email_request(mongo_client: &Client, email: &str) -> Result { // make a new request in the email_verification_requests collection let db = mongo_client.database("auth"); @@ -830,12 +801,11 @@ impl User { &"FlexAuth Team", &email, &"Verify Email", - &format!("Please click on the link to verify your email: http://localhost:8080/verify-email/{}", new_doc.req_id), + &format!("Please click on the link to verify your email: {}/verify-email/{}", dotenv::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()), new_doc.req_id), ).send().await; Ok(new_doc) } - pub async fn verify_email(mongo_client: &Client, req_id: &str) -> Result { // check if the email_verification_request exists let db = mongo_client.database("auth"); @@ -907,7 +877,126 @@ impl User { ).send().await; Ok(req_id.to_string()) } + pub async fn block_request(mongo_client: &Client, email: &str, uid: &str) -> Result { + let db = mongo_client.database("auth"); + let collection: Collection = db.collection("users_block_requests"); + + let dek_data = match Dek::get(&mongo_client, uid).await { + Ok(dek) => dek, + Err(e) => { + return Err(e); + } + }; + + // get a time 24h from now + let twenty_four_hours_from_now_millis = DateTime::now().timestamp_millis() + 86400000; + let twenty_four_hours_from_now = DateTime::from_millis(twenty_four_hours_from_now_millis); + + let req_id = uuid::Uuid::new().to_string(); + let block_request = UserBlockRequest { + _id: ObjectId::new(), + req_id: req_id.to_string(), + uid: uid.to_string(), + email: Encryption::encrypt_data(&email, &dek_data.dek), + is_used: false, + expires_at: twenty_four_hours_from_now, + created_at: Some(DateTime::now()), + updated_at: Some(DateTime::now()), + }; + + match collection.insert_one(&block_request, None).await { + Ok(_) => { + Ok(req_id) + } + Err(_) => { + return Err(Error::ServerError { + message: "Failed to insert block request".to_string(), + }); + } + } + } + pub async fn block(mongo_client: &Client, req_id: &str) -> Result { + let db = mongo_client.database("auth"); + let collection: Collection = db.collection("users"); + let collection_block_requests: Collection = db.collection("users_block_requests"); + + // check if the block request exists + let block_request = match collection_block_requests + .find_one(doc! { "req_id": req_id }, None) + .await + .unwrap() { + Some(data) => data, + None => { + return Err(Error::UserNotFound { + message: "Block request not found. Please request a new link.".to_string(), + }); + } + }; + + // check if the request is expired + if block_request.expires_at.timestamp_millis() < DateTime::now().timestamp_millis() { + return Err(Error::BlockRequestLinkExpired { + message: "The link has expired. Please request a new link.".to_string(), + }); + } + + // check if the request is used + if block_request.is_used { + return Err(Error::BlockRequestLinkExpired { + message: "The link has already been used. Please request a new link.".to_string(), + }); + } + + // find the user in the users collection using the uid + match collection + .update_one( + doc! { + "uid": &block_request.uid, + }, + doc! { + "$set": { + "is_active": false, + "updated_at": DateTime::now(), + } + }, + None, + ) + .await + { + Ok(cursor) => { + let modified_count = cursor.modified_count; + + // Return Error if User is not there + if modified_count == 0 { + // send back a 404 to + return Err(Error::UserNotFound { + message: "User not found".to_string(), + }); + } + // Send a email to the user that the account has been blocked + let user = match User::get_from_uid(&mongo_client, &block_request.uid).await { + Ok(user) => user, + Err(e) => { + return Err(e); + } + }; + + Email::new( + &user.name, + &user.email, + &"Account Blocked", + &("Your account has been blocked. If it was not you please take action as soon as possible. If you want to re-activate your account then please contact us by simply replying to this email."), + ).send().await; + return Ok("User blocked successfully".to_string()); + } + Err(_) => { + return Err(Error::ServerError { + message: "Failed to update User".to_string(), + }) + } + } + } pub async fn delete(mongo_client: &Client, email: &str) -> Result { let db = mongo_client.database("auth"); let collection: Collection = db.collection("users"); diff --git a/src/errors.rs b/src/errors.rs index 11da9d8..b223a7b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -36,6 +36,7 @@ pub enum Error { // -- Email erros EmailVerificationLinkExpired { message: String }, + BlockRequestLinkExpired { message: String }, // -- Validation Errors InvalidEmail { message: String }, @@ -166,6 +167,10 @@ impl Error { (StatusCode::UNAUTHORIZED, ClientError::EMAIL_VERIFICATION_LINK_EXPIRED) } + Self::BlockRequestLinkExpired { message: _ } => { + (StatusCode::UNAUTHORIZED, ClientError::BLOCK_REQUEST_LINK_EXPIRED) + } + _ => ( StatusCode::INTERNAL_SERVER_ERROR, ClientError::SERVICE_ERROR, @@ -192,6 +197,7 @@ pub enum ClientError { ACTIVE_SESSION_EXISTS, SESSION_NOT_FOUND, EMAIL_VERIFICATION_LINK_EXPIRED, + BLOCK_REQUEST_LINK_EXPIRED, } // region: --- Error Boilerplate diff --git a/src/handlers/password_handler.rs b/src/handlers/password_handler.rs index e9e0546..e79aa86 100644 --- a/src/handlers/password_handler.rs +++ b/src/handlers/password_handler.rs @@ -97,7 +97,7 @@ pub async fn forget_password_reset_handler( } } -#[debug_handler] //forget_password_form +#[debug_handler] pub async fn forget_password_form(Path(id): Path) -> impl IntoResponse { Html(format!(r#" @@ -107,11 +107,10 @@ pub async fn forget_password_form(Path(id): Path) -> impl IntoResponse { Reset Password @@ -127,18 +127,17 @@ pub async fn forget_password_form(Path(id): Path) -> impl IntoResponse {

Reset Password

-
- - - - - - -
- -

Note: Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one number, and one special character.

-
- +
+ + + + + + +
+ +

Note: Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one number, and one special character.

+
+ + + "#, + id = id, + api_key = dotenv::var("X_API_KEY").expect("X_API_KEY is not set") )) } + diff --git a/src/main.rs b/src/main.rs index 496fa4b..d4dd95a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use axum::routing::get; use axum::{middleware, Router}; use dotenv::dotenv; use handlers::password_handler::forget_password_form; -use handlers::user_handler::show_verification_page_email; +use handlers::user_handler::{show_block_user_page, show_verification_page_email}; use middlewares::res_log::main_response_mapper; use middlewares::with_api_key::with_api_key; use mongodb::Client; @@ -48,6 +48,7 @@ async fn main() -> Result<(), Box> { .route("/", get(root_handler)) .route("/forget-reset/:id", get(forget_password_form)) .route("/verify-email/:id", get(show_verification_page_email)) + .route("/block-account/:id", get(show_block_user_page)) .merge(routes::health_check_routes::routes()) .layer(middleware::map_response(main_response_mapper)); diff --git a/src/models/password_model.rs b/src/models/password_model.rs index 2bbd41b..77a2b61 100644 --- a/src/models/password_model.rs +++ b/src/models/password_model.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; pub struct ForgetPasswordRequest { pub _id: ObjectId, pub email: String, - pub id: String, + pub req_id: String, pub is_used: bool, pub valid_till: DateTime, pub created_at: DateTime, diff --git a/src/models/user_model.rs b/src/models/user_model.rs index 3b142ac..2c813b9 100644 --- a/src/models/user_model.rs +++ b/src/models/user_model.rs @@ -106,3 +106,26 @@ pub struct EmailVerificationResponse { pub message: String, pub req_id: String, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UserBlockRequest { + pub _id: ObjectId, + pub req_id: String, + pub uid: String, + pub email: String, + pub is_used: bool, + pub expires_at: DateTime, + pub created_at: Option, + pub updated_at: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BlockUserPayload { + pub req_id: String, +} + +#[derive(Serialize, Debug, Clone)] +pub struct BlockUserResponse { + pub message: String, + pub req_id: String, +} diff --git a/src/routes/user_routes.rs b/src/routes/user_routes.rs index 18b444f..d8fd605 100644 --- a/src/routes/user_routes.rs +++ b/src/routes/user_routes.rs @@ -4,7 +4,7 @@ use axum::{ use crate::{ handlers::user_handler::{ - delete_user_handler, get_all_users_handler, get_recent_users_handler, get_user_email_handler, get_user_id_handler, toggle_user_activation_status, update_user_handler, update_user_role_handler, verify_email_handler, verify_email_request_handler + block_user_handler, delete_user_handler, get_all_users_handler, get_recent_users_handler, get_user_email_handler, get_user_id_handler, toggle_user_activation_status, update_user_handler, update_user_role_handler, verify_email_handler, verify_email_request_handler }, AppState }; @@ -17,6 +17,7 @@ pub fn routes(State(state): State) -> Router { .route("/update", post(update_user_handler)) .route("/verify-email-request", post(verify_email_request_handler)) .route("/verify-email/:id", get(verify_email_handler)) + .route("/block/:id", get(block_user_handler)) .route( "/toggle-account-active-status", post(toggle_user_activation_status), From 5da3889e9afbfcd9c61971cb85eacd59851f4813 Mon Sep 17 00:00:00 2001 From: Rajdeep Sengupta Date: Thu, 25 Jul 2024 15:02:30 +0530 Subject: [PATCH 2/3] Fix server pages style --- src/handlers/password_handler.rs | 2 +- src/handlers/user_handler.rs | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/handlers/password_handler.rs b/src/handlers/password_handler.rs index e79aa86..4a7adf7 100644 --- a/src/handlers/password_handler.rs +++ b/src/handlers/password_handler.rs @@ -118,7 +118,7 @@ pub async fn forget_password_form(Path(id): Path) -> impl IntoResponse { h1 {{ color: #f2f2f2; text-align: center; padding: 14px 0px; }} h2 {{ text-align: center; color: #f2f2f2; }} p {{ color: #f2f2f2; margin-top: 30px; }} - .success {{ display: flex; justify-content: center; align-items: center; height: 100vh; }} + .success {{ display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; }} diff --git a/src/handlers/user_handler.rs b/src/handlers/user_handler.rs index 57e8aa9..a34226e 100644 --- a/src/handlers/user_handler.rs +++ b/src/handlers/user_handler.rs @@ -2,7 +2,7 @@ use crate::{ core::{dek::Dek, session::Session, user::User}, errors::{Error, Result}, models::user_model::{ - BlockUserPayload, BlockUserResponse, EmailVerificationResponse, RecentUserPayload, ToggleUserActivationStatusPayload, ToggleUserActivationStatusResponse, UpdateUserPayload, UpdateUserResponse, UpdateUserRolePayload, UpdateUserRoleResponse, UserEmailPayload, UserEmailResponse, UserIdPayload, UserResponse + BlockUserResponse, EmailVerificationResponse, RecentUserPayload, ToggleUserActivationStatusPayload, ToggleUserActivationStatusResponse, UpdateUserPayload, UpdateUserResponse, UpdateUserRolePayload, UpdateUserRoleResponse, UserEmailPayload, UserEmailResponse, UserIdPayload, UserResponse }, utils::{encryption_utils::Encryption, validation_utils::Validation}, AppState, @@ -332,7 +332,7 @@ pub async fn show_verification_page_email(Path(id): Path) -> impl IntoRe .navbar {{ background-color: #060A13; overflow: hidden; border-bottom: 0.5px solid #1E293B; }} .navbar h1 {{ color: #f2f2f2; text-align: center; padding: 14px 0px; margin: 0; }} .content {{ display: flex; justify-content: center; align-items: center; height: 80vh; }} - .message {{ text-align: center; }} + .message {{ text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; }} .message h2 {{ color: #f2f2f2; }} @@ -343,6 +343,7 @@ pub async fn show_verification_page_email(Path(id): Path) -> impl IntoRe

Verifying...

+