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

@ -346,7 +346,6 @@ wildcard_origin = false # If true, allows any origin to make req
[email]
sender_email = "example@example.com" # Sender email
aws_region = "" # AWS region used by AWS SES
base_url = "" # Base url used when adding links that should redirect to self
allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email
active_email_client = "SES" # The currently active email client
@ -359,6 +358,7 @@ sts_role_session_name = "" # An identifier for the assumed role session, used to
password_validity_in_days = 90 # Number of days after which password should be updated
two_factor_auth_expiry_in_secs = 300 # Number of seconds after which 2FA should be done again if doing update/change from inside
totp_issuer_name = "Hyperswitch" # Name of the issuer for TOTP
base_url = "" # Base url used for user specific redirects and emails
#tokenization configuration which describe token lifetime and payment method for specific connector
[tokenization]

View File

@ -58,7 +58,6 @@ wildcard_origin = false # If true, allows any origin to make req
[email]
sender_email = "example@example.com" # Sender email
aws_region = "" # AWS region used by AWS SES
base_url = "" # Dashboard base url used when adding links that should redirect to self, say https://app.hyperswitch.io for example
allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email
active_email_client = "SES" # The currently active email client

View File

@ -118,6 +118,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
password_validity_in_days = 90
two_factor_auth_expiry_in_secs = 300
totp_issuer_name = "Hyperswitch Integ"
base_url = "https://integ.hyperswitch.io"
[frm]
enabled = true

View File

@ -125,6 +125,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
password_validity_in_days = 90
two_factor_auth_expiry_in_secs = 300
totp_issuer_name = "Hyperswitch Production"
base_url = "https://live.hyperswitch.io"
[frm]
enabled = false

View File

@ -125,6 +125,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
password_validity_in_days = 90
two_factor_auth_expiry_in_secs = 300
totp_issuer_name = "Hyperswitch Sandbox"
base_url = "https://app.hyperswitch.io"
[frm]
enabled = true

View File

@ -264,7 +264,6 @@ wildcard_origin = true
[email]
sender_email = "example@example.com"
aws_region = ""
base_url = "http://localhost:8080"
allowed_unverified_days = 1
active_email_client = "SES"
@ -276,6 +275,7 @@ sts_role_session_name = ""
password_validity_in_days = 90
two_factor_auth_expiry_in_secs = 300
totp_issuer_name = "Hyperswitch Dev"
base_url = "http://localhost:8080"
[bank_config.eps]
stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }

View File

@ -56,6 +56,7 @@ recon_admin_api_key = "recon_test_admin"
password_validity_in_days = 90
two_factor_auth_expiry_in_secs = 300
totp_issuer_name = "Hyperswitch"
base_url = "http://localhost:8080"
[locker]
host = ""

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,