mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
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:
@ -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>,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
))
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user