feat(email): Add auth_id in email types and send auth_id in email URLs (#5120)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Mani Chandra
2024-06-26 15:18:23 +05:30
committed by GitHub
parent e69a7bda52
commit 4ccd25d0dc
13 changed files with 120 additions and 28 deletions

View File

@ -355,3 +355,8 @@ pub struct AuthMethodDetails {
pub auth_type: common_enums::UserAuthType,
pub name: Option<OpenIdProvider>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct AuthIdQueryParam {
pub auth_id: Option<String>,
}

View File

@ -126,9 +126,6 @@ pub struct EmailSettings {
/// The AWS region to send SES requests to.
pub aws_region: String,
/// Base-url used when adding links that should redirect to self
pub base_url: String,
/// Number of days for verification of the email
pub allowed_unverified_days: i64,

View File

@ -484,6 +484,7 @@ pub struct UserSettings {
pub password_validity_in_days: u16,
pub two_factor_auth_expiry_in_secs: i64,
pub totp_issuer_name: String,
pub base_url: String,
}
#[derive(Debug, Deserialize, Clone)]

View File

@ -39,6 +39,7 @@ pub mod sample_data;
pub async fn signup_with_merchant_id(
state: SessionState,
request: user_api::SignUpWithMerchantIdRequest,
auth_id: Option<String>,
) -> UserResponse<user_api::SignUpWithMerchantIdResponse> {
let new_user = domain::NewUser::try_from(request.clone())?;
new_user
@ -64,6 +65,7 @@ pub async fn signup_with_merchant_id(
user_name: domain::UserName::new(user_from_db.get_name())?,
settings: state.conf.clone(),
subject: "Get back to Hyperswitch - Reset Your Password Now",
auth_id,
};
let send_email_result = state
@ -241,6 +243,7 @@ pub async fn signin_token_only_flow(
pub async fn connect_account(
state: SessionState,
request: user_api::ConnectAccountRequest,
auth_id: Option<String>,
) -> UserResponse<user_api::ConnectAccountResponse> {
let find_user = state.global_store.find_user_by_email(&request.email).await;
@ -253,6 +256,7 @@ pub async fn connect_account(
settings: state.conf.clone(),
user_name: domain::UserName::new(user_from_db.get_name())?,
subject: "Unlock Hyperswitch: Use Your Magic Link to Sign In",
auth_id,
};
let send_email_result = state
@ -303,6 +307,7 @@ pub async fn connect_account(
recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
settings: state.conf.clone(),
subject: "Welcome to the Hyperswitch community!",
auth_id,
};
let send_email_result = state
@ -401,6 +406,7 @@ pub async fn change_password(
pub async fn forgot_password(
state: SessionState,
request: user_api::ForgotPasswordRequest,
auth_id: Option<String>,
) -> UserResponse<()> {
let user_email = domain::UserEmail::from_pii_email(request.email)?;
@ -422,6 +428,7 @@ pub async fn forgot_password(
settings: state.conf.clone(),
user_name: domain::UserName::new(user_from_db.get_name())?,
subject: "Get back to Hyperswitch - Reset Your Password Now",
auth_id,
};
state
@ -596,6 +603,7 @@ pub async fn invite_multiple_user(
requests: Vec<user_api::InviteUserRequest>,
req_state: ReqState,
is_token_only: Option<bool>,
auth_id: Option<String>,
) -> UserResponse<Vec<InviteMultipleUserResponse>> {
if requests.len() > 10 {
return Err(report!(UserErrors::MaxInvitationsError))
@ -603,7 +611,15 @@ pub async fn invite_multiple_user(
}
let responses = futures::future::join_all(requests.iter().map(|request| async {
match handle_invitation(&state, &user_from_token, request, &req_state, is_token_only).await
match handle_invitation(
&state,
&user_from_token,
request,
&req_state,
is_token_only,
&auth_id,
)
.await
{
Ok(response) => response,
Err(error) => InviteMultipleUserResponse {
@ -625,6 +641,7 @@ async fn handle_invitation(
request: &user_api::InviteUserRequest,
req_state: &ReqState,
is_token_only: Option<bool>,
auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> {
let inviter_user = user_from_token.get_user_from_db(state).await?;
@ -656,7 +673,14 @@ async fn handle_invitation(
.await;
if let Ok(invitee_user) = invitee_user {
handle_existing_user_invitation(state, user_from_token, request, invitee_user.into()).await
handle_existing_user_invitation(
state,
user_from_token,
request,
invitee_user.into(),
auth_id,
)
.await
} else if invitee_user
.as_ref()
.map_err(|e| e.current_context().is_db_not_found())
@ -669,6 +693,7 @@ async fn handle_invitation(
request,
req_state.clone(),
is_token_only,
auth_id,
)
.await
} else {
@ -676,12 +701,13 @@ async fn handle_invitation(
}
}
//TODO: send email
#[allow(unused_variables)]
async fn handle_existing_user_invitation(
state: &SessionState,
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
invitee_user_from_db: domain::UserFromStorage,
auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> {
let now = common_utils::date_time::now();
state
@ -722,6 +748,7 @@ async fn handle_existing_user_invitation(
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
auth_id: auth_id.clone(),
};
is_email_sent = state
@ -748,12 +775,14 @@ async fn handle_existing_user_invitation(
})
}
#[allow(unused_variables)]
async fn handle_new_user_invitation(
state: &SessionState,
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
req_state: ReqState,
is_token_only: Option<bool>,
auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> {
let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?;
@ -809,6 +838,7 @@ async fn handle_new_user_invitation(
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
auth_id: auth_id.clone(),
})
} else {
Box::new(email_types::InviteUser {
@ -817,6 +847,7 @@ async fn handle_new_user_invitation(
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
auth_id: auth_id.clone(),
})
};
let send_email_result = state
@ -862,7 +893,7 @@ pub async fn resend_invite(
state: SessionState,
user_from_token: auth::UserFromToken,
request: user_api::ReInviteUserRequest,
_req_state: ReqState,
auth_id: Option<String>,
) -> UserResponse<()> {
let invitee_email = domain::UserEmail::from_pii_email(request.email)?;
let user: domain::UserFromStorage = state
@ -906,6 +937,7 @@ pub async fn resend_invite(
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id,
auth_id,
};
state
.email_client
@ -1518,6 +1550,7 @@ pub async fn verify_email_token_only_flow(
pub async fn send_verification_mail(
state: SessionState,
req: user_api::SendVerifyEmailRequest,
auth_id: Option<String>,
) -> UserResponse<()> {
let user_email = domain::UserEmail::try_from(req.email)?;
let user = state
@ -1540,6 +1573,7 @@ pub async fn send_verification_mail(
recipient_email: domain::UserEmail::from_pii_email(user.email)?,
settings: state.conf.clone(),
subject: "Welcome to the Hyperswitch community!",
auth_id,
};
state

View File

@ -39,15 +39,19 @@ pub async fn user_signup_with_merchant_id(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SignUpWithMerchantIdRequest>,
query: web::Query<user_api::AuthIdQueryParam>,
) -> HttpResponse {
let flow = Flow::UserSignUpWithMerchantId;
let req_payload = json_payload.into_inner();
let auth_id = query.into_inner().auth_id;
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
req_payload.clone(),
|state, _, req_body, _| user_core::signup_with_merchant_id(state, req_body),
|state, _, req_body, _| {
user_core::signup_with_merchant_id(state, req_body, auth_id.clone())
},
&auth::AdminApiAuth,
api_locking::LockAction::NotApplicable,
))
@ -113,15 +117,17 @@ pub async fn user_connect_account(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::ConnectAccountRequest>,
query: web::Query<user_api::AuthIdQueryParam>,
) -> HttpResponse {
let flow = Flow::UserConnectAccount;
let req_payload = json_payload.into_inner();
let auth_id = query.into_inner().auth_id;
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
req_payload.clone(),
|state, _, req_body, _| user_core::connect_account(state, req_body),
|state, _, req_body, _| user_core::connect_account(state, req_body, auth_id.clone()),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
@ -382,14 +388,16 @@ pub async fn forgot_password(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<user_api::ForgotPasswordRequest>,
query: web::Query<user_api::AuthIdQueryParam>,
) -> HttpResponse {
let flow = Flow::ForgotPassword;
let auth_id = query.into_inner().auth_id;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
|state, _, payload, _| user_core::forgot_password(state, payload),
|state, _, payload, _| user_core::forgot_password(state, payload, auth_id.clone()),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
@ -431,21 +439,31 @@ pub async fn reset_password(
.await
}
}
pub async fn invite_multiple_user(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<Vec<user_api::InviteUserRequest>>,
query: web::Query<user_api::TokenOnlyQueryParam>,
token_only_query_param: web::Query<user_api::TokenOnlyQueryParam>,
auth_id_query_param: web::Query<user_api::AuthIdQueryParam>,
) -> HttpResponse {
let flow = Flow::InviteMultipleUser;
let is_token_only = query.into_inner().token_only;
let is_token_only = token_only_query_param.into_inner().token_only;
let auth_id = auth_id_query_param.into_inner().auth_id;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
|state, user, payload, req_state| {
user_core::invite_multiple_user(state, user, payload, req_state, is_token_only)
user_core::invite_multiple_user(
state,
user,
payload,
req_state,
is_token_only,
auth_id.clone(),
)
},
&auth::JWTAuth(Permission::UsersWrite),
api_locking::LockAction::NotApplicable,
@ -458,14 +476,18 @@ pub async fn resend_invite(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<user_api::ReInviteUserRequest>,
query: web::Query<user_api::AuthIdQueryParam>,
) -> HttpResponse {
let flow = Flow::ReInviteUser;
let auth_id = query.into_inner().auth_id;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
user_core::resend_invite,
|state, user, req_payload, _| {
user_core::resend_invite(state, user, req_payload, auth_id.clone())
},
&auth::JWTAuth(Permission::UsersWrite),
api_locking::LockAction::NotApplicable,
))
@ -551,14 +573,16 @@ pub async fn verify_email_request(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SendVerifyEmailRequest>,
query: web::Query<user_api::AuthIdQueryParam>,
) -> HttpResponse {
let flow = Flow::VerifyEmailRequest;
let auth_id = query.into_inner().auth_id;
Box::pin(api::server_wrap(
flow,
state.clone(),
&http_req,
json_payload.into_inner(),
|state, _, req_body, _| user_core::send_verification_mail(state, req_body),
|state, _, req_body, _| user_core::send_verification_mail(state, req_body, auth_id.clone()),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))

View File

@ -190,14 +190,21 @@ pub fn get_link_with_token(
base_url: impl std::fmt::Display,
token: impl std::fmt::Display,
action: impl std::fmt::Display,
auth_id: &Option<impl std::fmt::Display>,
) -> String {
format!("{base_url}/user/{action}?token={token}")
let email_url = format!("{base_url}/user/{action}?token={token}");
if let Some(auth_id) = auth_id {
format!("{email_url}&auth_id={auth_id}")
} else {
email_url
}
}
pub struct VerifyEmail {
pub recipient_email: domain::UserEmail,
pub settings: std::sync::Arc<configs::Settings>,
pub subject: &'static str,
pub auth_id: Option<String>,
}
/// Currently only HTML is supported
@ -213,8 +220,12 @@ impl EmailData for VerifyEmail {
.await
.change_context(EmailError::TokenGenerationFailure)?;
let verify_email_link =
get_link_with_token(&self.settings.email.base_url, token, "verify_email");
let verify_email_link = get_link_with_token(
&self.settings.user.base_url,
token,
"verify_email",
&self.auth_id,
);
let body = html::get_html_body(EmailBody::Verify {
link: verify_email_link,
@ -233,6 +244,7 @@ pub struct ResetPassword {
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::Settings>,
pub subject: &'static str,
pub auth_id: Option<String>,
}
#[async_trait::async_trait]
@ -247,8 +259,12 @@ impl EmailData for ResetPassword {
.await
.change_context(EmailError::TokenGenerationFailure)?;
let reset_password_link =
get_link_with_token(&self.settings.email.base_url, token, "set_password");
let reset_password_link = get_link_with_token(
&self.settings.user.base_url,
token,
"set_password",
&self.auth_id,
);
let body = html::get_html_body(EmailBody::Reset {
link: reset_password_link,
@ -268,6 +284,7 @@ pub struct MagicLink {
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::Settings>,
pub subject: &'static str,
pub auth_id: Option<String>,
}
#[async_trait::async_trait]
@ -282,8 +299,12 @@ impl EmailData for MagicLink {
.await
.change_context(EmailError::TokenGenerationFailure)?;
let magic_link_login =
get_link_with_token(&self.settings.email.base_url, token, "verify_email");
let magic_link_login = get_link_with_token(
&self.settings.user.base_url,
token,
"verify_email",
&self.auth_id,
);
let body = html::get_html_body(EmailBody::MagicLink {
link: magic_link_login,
@ -305,6 +326,7 @@ pub struct InviteUser {
pub settings: std::sync::Arc<configs::Settings>,
pub subject: &'static str,
pub merchant_id: String,
pub auth_id: Option<String>,
}
#[async_trait::async_trait]
@ -319,8 +341,12 @@ impl EmailData for InviteUser {
.await
.change_context(EmailError::TokenGenerationFailure)?;
let invite_user_link =
get_link_with_token(&self.settings.email.base_url, token, "set_password");
let invite_user_link = get_link_with_token(
&self.settings.user.base_url,
token,
"set_password",
&self.auth_id,
);
let body = html::get_html_body(EmailBody::InviteUser {
link: invite_user_link,
@ -341,6 +367,7 @@ pub struct InviteRegisteredUser {
pub settings: std::sync::Arc<configs::Settings>,
pub subject: &'static str,
pub merchant_id: String,
pub auth_id: Option<String>,
}
#[async_trait::async_trait]
@ -356,9 +383,10 @@ impl EmailData for InviteRegisteredUser {
.change_context(EmailError::TokenGenerationFailure)?;
let invite_user_link = get_link_with_token(
&self.settings.email.base_url,
&self.settings.user.base_url,
token,
"accept_invite_from_email",
&self.auth_id,
);
let body = html::get_html_body(EmailBody::AcceptInviteFromEmail {
link: invite_user_link,