Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify Email Functionality Added #64

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -702,10 +702,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 @@ -798,6 +794,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: 108 additions & 3 deletions src/handlers/user_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +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 All @@ -25,7 +32,7 @@ pub async fn get_all_users_handler(

pub async fn get_recent_users_handler(
State(state): State<AppState>,
payload: Json<RecentUserPayload>
payload: Json<RecentUserPayload>,
) -> Result<Json<Vec<UserResponse>>> {
println!(">> HANDLER: get_recent_users_handler called");

Expand Down Expand Up @@ -224,6 +231,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 @@ -250,3 +299,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 @@ -45,6 +46,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 @@ -83,4 +83,25 @@ pub struct UserResponse {
#[derive(Deserialize, Debug, Clone)]
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