Skip to content

Commit

Permalink
Synced with Main
Browse files Browse the repository at this point in the history
  • Loading branch information
Debajyoti14 committed Jul 24, 2024
2 parents 6cc04e5 + 53befac commit c18465d
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 11 deletions.
118 changes: 113 additions & 5 deletions src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -704,10 +704,6 @@ impl User {
let forget_password_requests_collection: Collection<ForgetPasswordRequest> =
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,
Expand Down Expand Up @@ -800,6 +796,118 @@ impl User {

}

pub async fn verify_email_request(mongo_client: &Client, email: &str) -> Result<EmailVerificationRequest> {
// make a new request in the email_verification_requests collection
let db = mongo_client.database("auth");
let collection: Collection<EmailVerificationRequest> = 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<String> {
// check if the email_verification_request exists
let db = mongo_client.database("auth");
let collection: Collection<EmailVerificationRequest> = 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<User> = 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<String> {
let db = mongo_client.database("auth");
let collection: Collection<User> = db.collection("users");
Expand Down
9 changes: 9 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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,
Expand All @@ -183,6 +191,7 @@ pub enum ClientError {
SESSION_EXPIRED,
ACTIVE_SESSION_EXISTS,
SESSION_NOT_FOUND,
EMAIL_VERIFICATION_LINK_EXPIRED,
}

// region: --- Error Boilerplate
Expand Down
111 changes: 107 additions & 4 deletions src/handlers/user_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -228,6 +233,48 @@ pub async fn get_user_id_handler(
}
}

#[debug_handler]
pub async fn verify_email_request_handler(
State(state): State<AppState>,
payload: Json<UserEmailPayload>,
) -> Result<Json<UserEmailResponse>> {
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<AppState>,
Path(id): Path<String>,
) -> Result<Json<EmailVerificationResponse>> {
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<AppState>,
payload: Json<UserEmailPayload>,
Expand All @@ -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<String>) -> impl IntoResponse {
Html(format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify Email</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 0; background-color: #060A13; color: #f2f2f2; }}
.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 h2 {{ color: #f2f2f2; }}
</style>
</head>
<body>
<div class='navbar'>
<h1>FlexAuth</h1>
</div>
<div class="content">
<div class="message">
<h2 id="message">Verifying...</h2>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {{
fetch('http://localhost:8080/api/user/verify-email/{id}', {{
headers: {{
'Content-Type': 'application/json',
'x-api-key': '{api_key}'
}},
}})
.then(response => {{
if (response.ok) {{
document.getElementById('message').textContent = 'Email Verified 🎉';
}} else {{
document.getElementById('message').textContent = 'Verification Link Expired';
}}
}})
.catch(error => {{
document.getElementById('message').textContent = 'An error occurred: ' + error.message;
}});
}});
</script>
</body>
</html>
"#,
id = id,
api_key = dotenv::var("X_API_KEY").unwrap()
))
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,6 +47,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
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));

Expand Down
23 changes: 22 additions & 1 deletion src/models/user_model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use core::str;

use bson::DateTime;
use bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize};

use crate::core::user::User;
Expand Down Expand Up @@ -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<DateTime>,
pub updated_at: Option<DateTime>,
}

#[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,
}
4 changes: 3 additions & 1 deletion src/routes/user_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand All @@ -15,6 +15,8 @@ pub fn routes(State(state): State<AppState>) -> 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),
Expand Down

0 comments on commit c18465d

Please sign in to comment.