diff --git a/src/core/user.rs b/src/core/user.rs index c8fc6dc..cda499d 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::UserResponse}, + models::{password_model::ForgetPasswordRequest, user_model::{EmailVerificationRequest, UserResponse}}, traits::{decryption::Decrypt, encryption::Encrypt}, utils::{ email_utils::Email, encryption_utils::Encryption, password_utils::Password @@ -704,10 +704,6 @@ impl User { let forget_password_requests_collection: Collection = db.collection("forget_password_requests"); - println!("Forget Password Request ID {:?}", req_id); - println!("Forget Password Request Email {:?}", email); - println!("Forget Password Request New Password {:?}", new_password); - // find the dek with the email let dek_data = match Dek::get(&mongo_client, &email).await { Ok(dek) => dek, @@ -800,6 +796,118 @@ impl User { } + 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"); + let collection: Collection = db.collection("email_verification_requests"); + + let dek_data = match Dek::get(&mongo_client, email).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 new_doc = EmailVerificationRequest { + _id: ObjectId::new(), + uid: dek_data.uid, + req_id: uuid::Uuid::new().to_string(), + email: Encryption::encrypt_data(&email, &dek_data.dek), + // expires in 24 hours + expires_at: twenty_four_hours_from_now, + created_at: Some(DateTime::now()), + updated_at: Some(DateTime::now()), + }; + + collection.insert_one(&new_doc, None).await.unwrap(); + + // send a email to the user with the link having id of the new doc + Email::new( + &"FlexAuth Team", + &email, + &"Verify Email", + &format!("Please click on the link to verify your email: http://localhost:8080/verify-email/{}", 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"); + let collection: Collection = db.collection("email_verification_requests"); + + let email_verification_request = match collection + .find_one(doc! { "req_id": req_id }, None) + .await + .unwrap() { + Some(data) => data, + None => { + return Err(Error::UserNotFound { + message: "Email verification request not found. Please request a new link.".to_string(), + }); + } + }; + + // check if the request exists + if email_verification_request.email.is_empty() { + return Err(Error::UserNotFound { + message: "Email verification request not found. Please request a new link.".to_string(), + }); + } + + if email_verification_request.expires_at.timestamp_millis() < DateTime::now().timestamp_millis() { + return Err(Error::EmailVerificationLinkExpired { + message: "The link has expired. Please request a new link.".to_string(), + }); + } + + // update the user with verified email + let user_collection: Collection = db.collection("users"); + + user_collection + .find_one_and_update( + doc! { "uid": &email_verification_request.uid }, + doc! { + "$set": { + "email_verified": true, + "updated_at": DateTime::now(), + } + }, + None, + ) + .await + .unwrap(); + + // delete the email_verification_request + collection + .delete_one(doc! { "req_id": req_id }, None) + .await + .unwrap(); + + let dek_data = match Dek::get(&mongo_client, &email_verification_request.uid).await { + Ok(dek) => dek, + Err(e) => { + return Err(e); + } + }; + + let decrypted_email = Encryption::decrypt_data(&email_verification_request.email, &dek_data.dek); + + // send a email to the user that the email has been verified + Email::new( + &"FlexAuth Team", + &decrypted_email, + &"Email Verified", + &"Your email has been verified successfully. If it was not you please take action as soon as possible", + ).send().await; + Ok(req_id.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 1af781f..11da9d8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -34,6 +34,9 @@ pub enum Error { ActiveSessionExists { message: String }, SessionNotFound { message: String }, + // -- Email erros + EmailVerificationLinkExpired { message: String }, + // -- Validation Errors InvalidEmail { message: String }, InvalidUserAgent { message: String }, @@ -158,6 +161,11 @@ impl Error { (StatusCode::NOT_FOUND, ClientError::SESSION_NOT_FOUND) } + // -- Email errors + Self::EmailVerificationLinkExpired { message: _ } => { + (StatusCode::UNAUTHORIZED, ClientError::EMAIL_VERIFICATION_LINK_EXPIRED) + } + _ => ( StatusCode::INTERNAL_SERVER_ERROR, ClientError::SERVICE_ERROR, @@ -183,6 +191,7 @@ pub enum ClientError { SESSION_EXPIRED, ACTIVE_SESSION_EXISTS, SESSION_NOT_FOUND, + EMAIL_VERIFICATION_LINK_EXPIRED, } // region: --- Error Boilerplate diff --git a/src/handlers/user_handler.rs b/src/handlers/user_handler.rs index feda68e..3fbe99a 100644 --- a/src/handlers/user_handler.rs +++ b/src/handlers/user_handler.rs @@ -2,14 +2,19 @@ use crate::{ core::{dek::Dek, session::Session, user::User}, errors::{Error, Result}, models::user_model::{ - RecentUserPayload, ToggleUserActivationStatusPayload, ToggleUserActivationStatusResponse, - UpdateUserPayload, UpdateUserResponse, UpdateUserRolePayload, UpdateUserRoleResponse, - UserEmailPayload, UserEmailResponse, UserIdPayload, UserResponse, + EmailVerificationResponse, RecentUserPayload, ToggleUserActivationStatusPayload, + ToggleUserActivationStatusResponse, UpdateUserPayload, UpdateUserResponse, + UpdateUserRolePayload, UpdateUserRoleResponse, UserEmailPayload, UserEmailResponse, + UserIdPayload, UserResponse, }, utils::{encryption_utils::Encryption, validation_utils::Validation}, AppState, }; -use axum::{extract::State, Json}; +use axum::{ + extract::{Path, State}, + response::{Html, IntoResponse}, + Json, +}; use axum_macros::debug_handler; use bson::{doc, DateTime}; use mongodb::Collection; @@ -228,6 +233,48 @@ pub async fn get_user_id_handler( } } +#[debug_handler] +pub async fn verify_email_request_handler( + State(state): State, + payload: Json, +) -> Result> { + println!(">> HANDLER: verify_email_request_handler called"); + + if !Validation::email(&payload.email) { + return Err(Error::InvalidPayload { + message: "Invalid Email".to_string(), + }); + } + + match User::verify_email_request(&State(&state).mongo_client, &payload.email).await { + Ok(_) => { + return Ok(Json(UserEmailResponse { + message: "Verification email sent".to_string(), + email: payload.email.to_owned(), + })); + } + Err(e) => return Err(e), + } +} + +#[debug_handler] +pub async fn verify_email_handler( + State(state): State, + Path(id): Path, +) -> Result> { + println!(">> HANDLER: verify_email_handler called"); + + match User::verify_email(&State(&state).mongo_client, &id).await { + Ok(req_id) => { + return Ok(Json(EmailVerificationResponse { + message: "Email verified successfully".to_string(), + req_id: req_id.to_owned(), + })); + } + Err(e) => return Err(e), + } +} + pub async fn delete_user_handler( State(state): State, payload: Json, @@ -254,3 +301,59 @@ pub async fn delete_user_handler( Err(e) => return Err(e), } } + +#[debug_handler] +pub async fn show_verification_page_email(Path(id): Path) -> impl IntoResponse { + Html(format!( + r#" + + + + + + Verify Email + + + + +
+
+

Verifying...

+
+
+ + + + "#, + id = id, + api_key = dotenv::var("X_API_KEY").unwrap() + )) +} diff --git a/src/main.rs b/src/main.rs index 7485fb7..496fa4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +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 middlewares::res_log::main_response_mapper; use middlewares::with_api_key::with_api_key; use mongodb::Client; @@ -46,6 +47,7 @@ async fn main() -> Result<(), Box> { let public_routes = Router::new() .route("/", get(root_handler)) .route("/forget-reset/:id", get(forget_password_form)) + .route("/verify-email/:id", get(show_verification_page_email)) .merge(routes::health_check_routes::routes()) .layer(middleware::map_response(main_response_mapper)); diff --git a/src/models/user_model.rs b/src/models/user_model.rs index b19ddea..3b142ac 100644 --- a/src/models/user_model.rs +++ b/src/models/user_model.rs @@ -1,6 +1,6 @@ use core::str; -use bson::DateTime; +use bson::{oid::ObjectId, DateTime}; use serde::{Deserialize, Serialize}; use crate::core::user::User; @@ -85,3 +85,24 @@ pub struct UserResponse { pub struct RecentUserPayload { pub limit: i64, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EmailVerificationRequest { + pub _id: ObjectId, + pub req_id: String, + pub uid: String, + pub email: String, + pub expires_at: DateTime, + pub created_at: Option, + pub updated_at: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EmailVerificationPayload { + pub req_id: String, +} +#[derive(Serialize, Debug, Clone)] +pub struct EmailVerificationResponse { + pub message: String, + pub req_id: String, +} diff --git a/src/routes/user_routes.rs b/src/routes/user_routes.rs index 2b59f0c..18b444f 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 + 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 }; @@ -15,6 +15,8 @@ pub fn routes(State(state): State) -> Router { .route("/get-from-email", post(get_user_email_handler)) .route("/get-from-id", post(get_user_id_handler)) .route("/update", post(update_user_handler)) + .route("/verify-email-request", post(verify_email_request_handler)) + .route("/verify-email/:id", get(verify_email_handler)) .route( "/toggle-account-active-status", post(toggle_user_activation_status),