From 585e00980c43797f326efb809df9ffd497d1dd26 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:16:03 +0530 Subject: [PATCH] feat(user): Add `verify_email` API (#3076) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 5 +-- crates/api_models/src/user.rs | 7 ++++ crates/router/src/core/user.rs | 39 ++++++++++++++++++++--- crates/router/src/routes/app.rs | 15 +++++---- crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/user.rs | 21 +++++++++++- crates/router/src/services/email/types.rs | 14 ++++---- crates/router_env/src/logger/types.rs | 2 ++ 8 files changed, 84 insertions(+), 22 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index ca29327253..92b6757239 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -9,7 +9,7 @@ use crate::user::{ AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, InviteUserResponse, ResetPasswordRequest, SignUpRequest, SignUpWithMerchantIdRequest, - SwitchMerchantIdRequest, UserMerchantCreate, + SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest, }; impl ApiEventMetric for DashboardEntryResponse { @@ -38,7 +38,8 @@ common_utils::impl_misc_api_event_type!( ForgotPasswordRequest, ResetPasswordRequest, InviteUserRequest, - InviteUserResponse + InviteUserResponse, + VerifyEmailRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index e5f06fdbfa..10d8411f8e 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -121,3 +121,10 @@ pub struct UserDetails { #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: time::PrimitiveDateTime, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct VerifyEmailRequest { + pub token: Secret, +} + +pub type VerifyEmailResponse = DashboardEntryResponse; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 01947d08d1..f44042a2dc 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -11,7 +11,7 @@ use router_env::logger; use super::errors::{UserErrors, UserResponse}; #[cfg(feature = "email")] -use crate::services::email::{types as email_types, types::EmailToken}; +use crate::services::email::types as email_types; use crate::{ consts, db::user::UserInterface, @@ -296,9 +296,10 @@ pub async fn reset_password( state: AppState, request: user_api::ResetPasswordRequest, ) -> UserResponse<()> { - let token = auth::decode_jwt::(request.token.expose().as_str(), &state) - .await - .change_context(UserErrors::LinkInvalid)?; + let token = + auth::decode_jwt::(request.token.expose().as_str(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; let password = domain::UserPassword::new(request.password)?; @@ -630,3 +631,33 @@ pub async fn get_users_for_merchant_account( Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users))) } + +#[cfg(feature = "email")] +pub async fn verify_email( + state: AppState, + req: user_api::VerifyEmailRequest, +) -> UserResponse { + let token = auth::decode_jwt::(&req.token.clone().expose(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + + let user = state + .store + .find_user_by_email(token.get_email()) + .await + .change_context(UserErrors::InternalServerError)?; + + let user = state + .store + .update_user_by_user_id(user.user_id.as_str(), storage_user::UserUpdate::VerifyUser) + .await + .change_context(UserErrors::InternalServerError)?; + + let user_from_db: domain::UserFromStorage = user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let jwt_token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, jwt_token), + )) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a7c394b7b6..34ae6fa3ec 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -863,11 +863,6 @@ impl User { route = route .service(web::resource("/signin").route(web::post().to(user_signin))) .service(web::resource("/change_password").route(web::post().to(change_password))) - .service( - web::resource("/data/merchant") - .route(web::post().to(set_merchant_scoped_dashboard_metadata)), - ) - .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) .service( @@ -879,7 +874,12 @@ impl User { .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) - .service(web::resource("/role/{role_id}").route(web::get().to(get_role))); + .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) + .service( + web::resource("/data") + .route(web::get().to(get_multiple_dashboard_metadata)) + .route(web::post().to(set_dashboard_metadata)), + ); #[cfg(feature = "dummy_connector")] { @@ -901,7 +901,8 @@ impl User { .service( web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_id)), - ); + ) + .service(web::resource("/verify_email").route(web::post().to(verify_email))) } #[cfg(not(feature = "email"))] { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 533d1d3a62..f5519b9603 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -170,7 +170,8 @@ impl From for ApiIdentifier { | Flow::ForgotPassword | Flow::ResetPassword | Flow::InviteUser - | Flow::UserSignUpWithMerchantId => Self::User, + | Flow::UserSignUpWithMerchantId + | Flow::VerifyEmail => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index c4476d6ed7..594da67aa0 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -115,7 +115,7 @@ pub async fn change_password( .await } -pub async fn set_merchant_scoped_dashboard_metadata( +pub async fn set_dashboard_metadata( state: web::Data, req: HttpRequest, json_payload: web::Json, @@ -351,3 +351,22 @@ pub async fn invite_user( )) .await } + +#[cfg(feature = "email")] +pub async fn verify_email( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyEmail; + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + json_payload.into_inner(), + |state, _, req_payload| user_core::verify_email(state, req_payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index ad91edd8c3..9e26c45ba6 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -50,7 +50,7 @@ pub mod html { #[derive(serde::Serialize, serde::Deserialize)] pub struct EmailToken { email: String, - expiration: u64, + exp: u64, } impl EmailToken { @@ -59,10 +59,10 @@ impl EmailToken { settings: &configs::settings::Settings, ) -> CustomResult { let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); - let expiration = jwt::generate_exp(expiration_duration)?.as_secs(); + let exp = jwt::generate_exp(expiration_duration)?.as_secs(); let token_payload = Self { email: email.get_secret().expose(), - expiration, + exp, }; jwt::generate_jwt(&token_payload, settings).await } @@ -95,7 +95,7 @@ impl EmailData for VerifyEmail { .change_context(EmailError::TokenGenerationFailure)?; let verify_email_link = - get_link_with_token(&self.settings.server.base_url, token, "verify_email"); + get_link_with_token(&self.settings.email.base_url, token, "verify_email"); let body = html::get_html_body(EmailBody::Verify { link: verify_email_link, @@ -124,7 +124,7 @@ impl EmailData for ResetPassword { .change_context(EmailError::TokenGenerationFailure)?; let reset_password_link = - get_link_with_token(&self.settings.server.base_url, token, "set_password"); + get_link_with_token(&self.settings.email.base_url, token, "set_password"); let body = html::get_html_body(EmailBody::Reset { link: reset_password_link, @@ -153,7 +153,7 @@ impl EmailData for MagicLink { .await .change_context(EmailError::TokenGenerationFailure)?; - let magic_link_login = get_link_with_token(&self.settings.server.base_url, token, "login"); + let magic_link_login = get_link_with_token(&self.settings.email.base_url, token, "login"); let body = html::get_html_body(EmailBody::MagicLink { link: magic_link_login, @@ -183,7 +183,7 @@ impl EmailData for InviteUser { .change_context(EmailError::TokenGenerationFailure)?; let invite_user_link = - get_link_with_token(&self.settings.server.base_url, token, "set_password"); + get_link_with_token(&self.settings.email.base_url, token, "set_password"); let body = html::get_html_body(EmailBody::MagicLink { link: invite_user_link, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index b682bcb12e..6344c21f5f 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -311,6 +311,8 @@ pub enum Flow { GetActionUrl, /// Sync connector onboarding status SyncOnboardingStatus, + /// Verify email Token + VerifyEmail, } ///