mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat: Add decision starter API for email flows (#4533)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -58,7 +58,9 @@ crates/router/src/core/payments/routing.rs @juspay/hyperswitch-routing
|
||||
crates/api_models/src/connector_onboarding.rs @juspay/hyperswitch-dashboard
|
||||
crates/api_models/src/user @juspay/hyperswitch-dashboard
|
||||
crates/api_models/src/user.rs @juspay/hyperswitch-dashboard
|
||||
crates/api_models/src/events/user.rs @juspay/hyperswitch-dashboard
|
||||
crates/api_models/src/user_role.rs @juspay/hyperswitch-dashboard
|
||||
crates/api_models/src/events/user_role.rs @juspay/hyperswitch-dashboard
|
||||
crates/api_models/src/verify_connector.rs @juspay/hyperswitch-dashboard
|
||||
crates/api_models/src/connector_onboarding.rs @juspay/hyperswitch-dashboard
|
||||
crates/diesel_models/src/query/dashboard_metadata.rs @juspay/hyperswitch-dashboard
|
||||
|
||||
@ -15,8 +15,8 @@ use crate::user::{
|
||||
GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
|
||||
InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest,
|
||||
SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest,
|
||||
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest,
|
||||
UserMerchantCreate, VerifyEmailRequest,
|
||||
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenResponse,
|
||||
UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest,
|
||||
};
|
||||
|
||||
impl ApiEventMetric for DashboardEntryResponse {
|
||||
@ -64,7 +64,9 @@ common_utils::impl_misc_api_event_type!(
|
||||
GetUserDetailsResponse,
|
||||
SignInWithTokenResponse,
|
||||
GetUserRoleDetailsRequest,
|
||||
GetUserRoleDetailsResponse
|
||||
GetUserRoleDetailsResponse,
|
||||
TokenResponse,
|
||||
UserFromEmailRequest
|
||||
);
|
||||
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
|
||||
@ -231,3 +231,8 @@ pub enum SignInWithTokenResponse {
|
||||
Token(TokenResponse),
|
||||
SignInResponse(SignInResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct UserFromEmailRequest {
|
||||
pub token: Secret<String>,
|
||||
}
|
||||
|
||||
@ -183,21 +183,7 @@ pub async fn signin_token_only_flow(
|
||||
let next_flow =
|
||||
domain::NextFlow::from_origin(domain::Origin::SignIn, user_from_db.clone(), &state).await?;
|
||||
|
||||
let token = match next_flow.get_flow() {
|
||||
domain::UserFlow::SPTFlow(spt_flow) => spt_flow.generate_spt(&state, &next_flow).await,
|
||||
domain::UserFlow::JWTFlow(jwt_flow) => {
|
||||
#[cfg(feature = "email")]
|
||||
{
|
||||
user_from_db.get_verification_days_left(&state)?;
|
||||
}
|
||||
|
||||
let user_role = user_from_db
|
||||
.get_preferred_or_active_user_role_from_db(&state)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InternalServerError)?;
|
||||
jwt_flow.generate_jwt(&state, &next_flow, &user_role).await
|
||||
}
|
||||
}?;
|
||||
let token = next_flow.get_token(&state).await?;
|
||||
|
||||
let response = user_api::SignInWithTokenResponse::Token(user_api::TokenResponse {
|
||||
token: token.clone(),
|
||||
@ -1323,3 +1309,38 @@ pub async fn update_user_details(
|
||||
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
pub async fn user_from_email(
|
||||
state: AppState,
|
||||
req: user_api::UserFromEmailRequest,
|
||||
) -> UserResponse<user_api::TokenResponse> {
|
||||
let token = req.token.expose();
|
||||
let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
|
||||
.await
|
||||
.change_context(UserErrors::LinkInvalid)?;
|
||||
|
||||
auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
|
||||
|
||||
let user_from_db: domain::UserFromStorage = state
|
||||
.store
|
||||
.find_user_by_email(
|
||||
&email_token
|
||||
.get_email()
|
||||
.change_context(UserErrors::InternalServerError)?,
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?
|
||||
.into();
|
||||
|
||||
let next_flow =
|
||||
domain::NextFlow::from_origin(email_token.get_flow(), user_from_db.clone(), &state).await?;
|
||||
|
||||
let token = next_flow.get_token(&state).await?;
|
||||
|
||||
let response = user_api::TokenResponse {
|
||||
token: token.clone(),
|
||||
token_type: next_flow.get_flow().into(),
|
||||
};
|
||||
auth::cookies::set_cookie_response(response, token)
|
||||
}
|
||||
|
||||
@ -1201,6 +1201,7 @@ impl User {
|
||||
#[cfg(feature = "email")]
|
||||
{
|
||||
route = route
|
||||
.service(web::resource("/from_email").route(web::post().to(user_from_email)))
|
||||
.service(
|
||||
web::resource("/connect_account").route(web::post().to(user_connect_account)),
|
||||
)
|
||||
|
||||
@ -219,7 +219,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::DeleteUserRole
|
||||
| Flow::TransferOrgOwnership
|
||||
| Flow::CreateRole
|
||||
| Flow::UpdateRole => Self::UserRole,
|
||||
| Flow::UpdateRole
|
||||
| Flow::UserFromEmail => Self::UserRole,
|
||||
|
||||
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
||||
Self::ConnectorOnboarding
|
||||
|
||||
@ -512,3 +512,22 @@ pub async fn update_user_account_details(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
pub async fn user_from_email(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
json_payload: web::Json<user_api::UserFromEmailRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::UserFromEmail;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, _: (), req_body, _| user_core::user_from_email(state, req_body),
|
||||
&auth::NoAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
@ -151,6 +151,7 @@ Email : {user_email}
|
||||
pub struct EmailToken {
|
||||
email: String,
|
||||
merchant_id: Option<String>,
|
||||
flow: domain::Origin,
|
||||
exp: u64,
|
||||
}
|
||||
|
||||
@ -158,6 +159,7 @@ impl EmailToken {
|
||||
pub async fn new_token(
|
||||
email: domain::UserEmail,
|
||||
merchant_id: Option<String>,
|
||||
flow: domain::Origin,
|
||||
settings: &configs::Settings,
|
||||
) -> CustomResult<String, UserErrors> {
|
||||
let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS);
|
||||
@ -165,6 +167,7 @@ impl EmailToken {
|
||||
let token_payload = Self {
|
||||
email: email.get_secret().expose(),
|
||||
merchant_id,
|
||||
flow,
|
||||
exp,
|
||||
};
|
||||
jwt::generate_jwt(&token_payload, settings).await
|
||||
@ -177,6 +180,10 @@ impl EmailToken {
|
||||
pub fn get_merchant_id(&self) -> Option<&str> {
|
||||
self.merchant_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_flow(&self) -> domain::Origin {
|
||||
self.flow.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_link_with_token(
|
||||
@ -197,9 +204,14 @@ pub struct VerifyEmail {
|
||||
#[async_trait::async_trait]
|
||||
impl EmailData for VerifyEmail {
|
||||
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
|
||||
let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
|
||||
.await
|
||||
.change_context(EmailError::TokenGenerationFailure)?;
|
||||
let token = EmailToken::new_token(
|
||||
self.recipient_email.clone(),
|
||||
None,
|
||||
domain::Origin::VerifyEmail,
|
||||
&self.settings,
|
||||
)
|
||||
.await
|
||||
.change_context(EmailError::TokenGenerationFailure)?;
|
||||
|
||||
let verify_email_link =
|
||||
get_link_with_token(&self.settings.email.base_url, token, "verify_email");
|
||||
@ -226,9 +238,14 @@ pub struct ResetPassword {
|
||||
#[async_trait::async_trait]
|
||||
impl EmailData for ResetPassword {
|
||||
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
|
||||
let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
|
||||
.await
|
||||
.change_context(EmailError::TokenGenerationFailure)?;
|
||||
let token = EmailToken::new_token(
|
||||
self.recipient_email.clone(),
|
||||
None,
|
||||
domain::Origin::ResetPassword,
|
||||
&self.settings,
|
||||
)
|
||||
.await
|
||||
.change_context(EmailError::TokenGenerationFailure)?;
|
||||
|
||||
let reset_password_link =
|
||||
get_link_with_token(&self.settings.email.base_url, token, "set_password");
|
||||
@ -256,9 +273,14 @@ pub struct MagicLink {
|
||||
#[async_trait::async_trait]
|
||||
impl EmailData for MagicLink {
|
||||
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
|
||||
let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
|
||||
.await
|
||||
.change_context(EmailError::TokenGenerationFailure)?;
|
||||
let token = EmailToken::new_token(
|
||||
self.recipient_email.clone(),
|
||||
None,
|
||||
domain::Origin::MagicLink,
|
||||
&self.settings,
|
||||
)
|
||||
.await
|
||||
.change_context(EmailError::TokenGenerationFailure)?;
|
||||
|
||||
let magic_link_login =
|
||||
get_link_with_token(&self.settings.email.base_url, token, "verify_email");
|
||||
@ -276,6 +298,7 @@ impl EmailData for MagicLink {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Deprecate this and use InviteRegisteredUser for new invites
|
||||
pub struct InviteUser {
|
||||
pub recipient_email: domain::UserEmail,
|
||||
pub user_name: domain::UserName,
|
||||
@ -290,6 +313,7 @@ impl EmailData for InviteUser {
|
||||
let token = EmailToken::new_token(
|
||||
self.recipient_email.clone(),
|
||||
Some(self.merchant_id.clone()),
|
||||
domain::Origin::ResetPassword,
|
||||
&self.settings,
|
||||
)
|
||||
.await
|
||||
@ -310,6 +334,7 @@ impl EmailData for InviteUser {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InviteRegisteredUser {
|
||||
pub recipient_email: domain::UserEmail,
|
||||
pub user_name: domain::UserName,
|
||||
@ -324,6 +349,7 @@ impl EmailData for InviteRegisteredUser {
|
||||
let token = EmailToken::new_token(
|
||||
self.recipient_email.clone(),
|
||||
Some(self.merchant_id.clone()),
|
||||
domain::Origin::AcceptInvitationFromEmail,
|
||||
&self.settings,
|
||||
)
|
||||
.await
|
||||
|
||||
@ -4,7 +4,7 @@ use masking::Secret;
|
||||
|
||||
use super::UserFromStorage;
|
||||
use crate::{
|
||||
core::errors::{UserErrors, UserResult},
|
||||
core::errors::{StorageErrorExt, UserErrors, UserResult},
|
||||
routes::AppState,
|
||||
services::authentication as auth,
|
||||
};
|
||||
@ -225,6 +225,25 @@ impl NextFlow {
|
||||
pub fn get_flow(&self) -> UserFlow {
|
||||
self.next_flow
|
||||
}
|
||||
|
||||
pub async fn get_token(&self, state: &AppState) -> UserResult<Secret<String>> {
|
||||
match self.next_flow {
|
||||
UserFlow::SPTFlow(spt_flow) => spt_flow.generate_spt(state, self).await,
|
||||
UserFlow::JWTFlow(jwt_flow) => {
|
||||
#[cfg(feature = "email")]
|
||||
{
|
||||
self.user.get_verification_days_left(state)?;
|
||||
}
|
||||
|
||||
let user_role = self
|
||||
.user
|
||||
.get_preferred_or_active_user_role_from_db(state)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InternalServerError)?;
|
||||
jwt_flow.generate_jwt(state, self, &user_role).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserFlow> for TokenPurpose {
|
||||
|
||||
@ -394,6 +394,8 @@ pub enum Flow {
|
||||
CreateRole,
|
||||
/// Update Role
|
||||
UpdateRole,
|
||||
/// User email flow start
|
||||
UserFromEmail,
|
||||
/// List initial webhook delivery attempts
|
||||
WebhookEventInitialDeliveryAttemptList,
|
||||
/// List delivery attempts for a webhook event
|
||||
|
||||
Reference in New Issue
Block a user