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:
Mani Chandra
2024-05-06 12:39:36 +05:30
committed by GitHub
parent 8d337bf34f
commit 1335554f51
10 changed files with 127 additions and 29 deletions

2
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -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")]

View File

@ -231,3 +231,8 @@ pub enum SignInWithTokenResponse {
Token(TokenResponse),
SignInResponse(SignInResponse),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UserFromEmailRequest {
pub token: Secret<String>,
}

View File

@ -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)
}

View File

@ -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)),
)

View File

@ -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

View File

@ -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
}

View File

@ -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,7 +204,12 @@ 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)
let token = EmailToken::new_token(
self.recipient_email.clone(),
None,
domain::Origin::VerifyEmail,
&self.settings,
)
.await
.change_context(EmailError::TokenGenerationFailure)?;
@ -226,7 +238,12 @@ 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)
let token = EmailToken::new_token(
self.recipient_email.clone(),
None,
domain::Origin::ResetPassword,
&self.settings,
)
.await
.change_context(EmailError::TokenGenerationFailure)?;
@ -256,7 +273,12 @@ 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)
let token = EmailToken::new_token(
self.recipient_email.clone(),
None,
domain::Origin::MagicLink,
&self.settings,
)
.await
.change_context(EmailError::TokenGenerationFailure)?;
@ -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

View File

@ -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 {

View File

@ -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