mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +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/connector_onboarding.rs @juspay/hyperswitch-dashboard | ||||||
| crates/api_models/src/user @juspay/hyperswitch-dashboard | crates/api_models/src/user @juspay/hyperswitch-dashboard | ||||||
| crates/api_models/src/user.rs @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/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/verify_connector.rs @juspay/hyperswitch-dashboard | ||||||
| crates/api_models/src/connector_onboarding.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 | crates/diesel_models/src/query/dashboard_metadata.rs @juspay/hyperswitch-dashboard | ||||||
|  | |||||||
| @ -15,8 +15,8 @@ use crate::user::{ | |||||||
|     GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse, |     GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse, | ||||||
|     InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest, |     InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest, | ||||||
|     SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest, |     SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest, | ||||||
|     SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, |     SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenResponse, | ||||||
|     UserMerchantCreate, VerifyEmailRequest, |     UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| impl ApiEventMetric for DashboardEntryResponse { | impl ApiEventMetric for DashboardEntryResponse { | ||||||
| @ -64,7 +64,9 @@ common_utils::impl_misc_api_event_type!( | |||||||
|     GetUserDetailsResponse, |     GetUserDetailsResponse, | ||||||
|     SignInWithTokenResponse, |     SignInWithTokenResponse, | ||||||
|     GetUserRoleDetailsRequest, |     GetUserRoleDetailsRequest, | ||||||
|     GetUserRoleDetailsResponse |     GetUserRoleDetailsResponse, | ||||||
|  |     TokenResponse, | ||||||
|  |     UserFromEmailRequest | ||||||
| ); | ); | ||||||
|  |  | ||||||
| #[cfg(feature = "dummy_connector")] | #[cfg(feature = "dummy_connector")] | ||||||
|  | |||||||
| @ -231,3 +231,8 @@ pub enum SignInWithTokenResponse { | |||||||
|     Token(TokenResponse), |     Token(TokenResponse), | ||||||
|     SignInResponse(SignInResponse), |     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 = |     let next_flow = | ||||||
|         domain::NextFlow::from_origin(domain::Origin::SignIn, user_from_db.clone(), &state).await?; |         domain::NextFlow::from_origin(domain::Origin::SignIn, user_from_db.clone(), &state).await?; | ||||||
|  |  | ||||||
|     let token = match next_flow.get_flow() { |     let token = next_flow.get_token(&state).await?; | ||||||
|         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 response = user_api::SignInWithTokenResponse::Token(user_api::TokenResponse { |     let response = user_api::SignInWithTokenResponse::Token(user_api::TokenResponse { | ||||||
|         token: token.clone(), |         token: token.clone(), | ||||||
| @ -1323,3 +1309,38 @@ pub async fn update_user_details( | |||||||
|  |  | ||||||
|     Ok(ApplicationResponse::StatusOk) |     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")] |         #[cfg(feature = "email")] | ||||||
|         { |         { | ||||||
|             route = route |             route = route | ||||||
|  |                 .service(web::resource("/from_email").route(web::post().to(user_from_email))) | ||||||
|                 .service( |                 .service( | ||||||
|                     web::resource("/connect_account").route(web::post().to(user_connect_account)), |                     web::resource("/connect_account").route(web::post().to(user_connect_account)), | ||||||
|                 ) |                 ) | ||||||
|  | |||||||
| @ -219,7 +219,8 @@ impl From<Flow> for ApiIdentifier { | |||||||
|             | Flow::DeleteUserRole |             | Flow::DeleteUserRole | ||||||
|             | Flow::TransferOrgOwnership |             | Flow::TransferOrgOwnership | ||||||
|             | Flow::CreateRole |             | Flow::CreateRole | ||||||
|             | Flow::UpdateRole => Self::UserRole, |             | Flow::UpdateRole | ||||||
|  |             | Flow::UserFromEmail => Self::UserRole, | ||||||
|  |  | ||||||
|             Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { |             Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { | ||||||
|                 Self::ConnectorOnboarding |                 Self::ConnectorOnboarding | ||||||
|  | |||||||
| @ -512,3 +512,22 @@ pub async fn update_user_account_details( | |||||||
|     )) |     )) | ||||||
|     .await |     .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 { | pub struct EmailToken { | ||||||
|     email: String, |     email: String, | ||||||
|     merchant_id: Option<String>, |     merchant_id: Option<String>, | ||||||
|  |     flow: domain::Origin, | ||||||
|     exp: u64, |     exp: u64, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -158,6 +159,7 @@ impl EmailToken { | |||||||
|     pub async fn new_token( |     pub async fn new_token( | ||||||
|         email: domain::UserEmail, |         email: domain::UserEmail, | ||||||
|         merchant_id: Option<String>, |         merchant_id: Option<String>, | ||||||
|  |         flow: domain::Origin, | ||||||
|         settings: &configs::Settings, |         settings: &configs::Settings, | ||||||
|     ) -> CustomResult<String, UserErrors> { |     ) -> CustomResult<String, UserErrors> { | ||||||
|         let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); |         let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); | ||||||
| @ -165,6 +167,7 @@ impl EmailToken { | |||||||
|         let token_payload = Self { |         let token_payload = Self { | ||||||
|             email: email.get_secret().expose(), |             email: email.get_secret().expose(), | ||||||
|             merchant_id, |             merchant_id, | ||||||
|  |             flow, | ||||||
|             exp, |             exp, | ||||||
|         }; |         }; | ||||||
|         jwt::generate_jwt(&token_payload, settings).await |         jwt::generate_jwt(&token_payload, settings).await | ||||||
| @ -177,6 +180,10 @@ impl EmailToken { | |||||||
|     pub fn get_merchant_id(&self) -> Option<&str> { |     pub fn get_merchant_id(&self) -> Option<&str> { | ||||||
|         self.merchant_id.as_deref() |         self.merchant_id.as_deref() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn get_flow(&self) -> domain::Origin { | ||||||
|  |         self.flow.clone() | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn get_link_with_token( | pub fn get_link_with_token( | ||||||
| @ -197,9 +204,14 @@ pub struct VerifyEmail { | |||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl EmailData for VerifyEmail { | impl EmailData for VerifyEmail { | ||||||
|     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { |     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( | ||||||
|             .await |             self.recipient_email.clone(), | ||||||
|             .change_context(EmailError::TokenGenerationFailure)?; |             None, | ||||||
|  |             domain::Origin::VerifyEmail, | ||||||
|  |             &self.settings, | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .change_context(EmailError::TokenGenerationFailure)?; | ||||||
|  |  | ||||||
|         let verify_email_link = |         let verify_email_link = | ||||||
|             get_link_with_token(&self.settings.email.base_url, token, "verify_email"); |             get_link_with_token(&self.settings.email.base_url, token, "verify_email"); | ||||||
| @ -226,9 +238,14 @@ pub struct ResetPassword { | |||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl EmailData for ResetPassword { | impl EmailData for ResetPassword { | ||||||
|     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { |     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( | ||||||
|             .await |             self.recipient_email.clone(), | ||||||
|             .change_context(EmailError::TokenGenerationFailure)?; |             None, | ||||||
|  |             domain::Origin::ResetPassword, | ||||||
|  |             &self.settings, | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .change_context(EmailError::TokenGenerationFailure)?; | ||||||
|  |  | ||||||
|         let reset_password_link = |         let reset_password_link = | ||||||
|             get_link_with_token(&self.settings.email.base_url, token, "set_password"); |             get_link_with_token(&self.settings.email.base_url, token, "set_password"); | ||||||
| @ -256,9 +273,14 @@ pub struct MagicLink { | |||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl EmailData for MagicLink { | impl EmailData for MagicLink { | ||||||
|     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { |     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( | ||||||
|             .await |             self.recipient_email.clone(), | ||||||
|             .change_context(EmailError::TokenGenerationFailure)?; |             None, | ||||||
|  |             domain::Origin::MagicLink, | ||||||
|  |             &self.settings, | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .change_context(EmailError::TokenGenerationFailure)?; | ||||||
|  |  | ||||||
|         let magic_link_login = |         let magic_link_login = | ||||||
|             get_link_with_token(&self.settings.email.base_url, token, "verify_email"); |             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 struct InviteUser { | ||||||
|     pub recipient_email: domain::UserEmail, |     pub recipient_email: domain::UserEmail, | ||||||
|     pub user_name: domain::UserName, |     pub user_name: domain::UserName, | ||||||
| @ -290,6 +313,7 @@ impl EmailData for InviteUser { | |||||||
|         let token = EmailToken::new_token( |         let token = EmailToken::new_token( | ||||||
|             self.recipient_email.clone(), |             self.recipient_email.clone(), | ||||||
|             Some(self.merchant_id.clone()), |             Some(self.merchant_id.clone()), | ||||||
|  |             domain::Origin::ResetPassword, | ||||||
|             &self.settings, |             &self.settings, | ||||||
|         ) |         ) | ||||||
|         .await |         .await | ||||||
| @ -310,6 +334,7 @@ impl EmailData for InviteUser { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub struct InviteRegisteredUser { | pub struct InviteRegisteredUser { | ||||||
|     pub recipient_email: domain::UserEmail, |     pub recipient_email: domain::UserEmail, | ||||||
|     pub user_name: domain::UserName, |     pub user_name: domain::UserName, | ||||||
| @ -324,6 +349,7 @@ impl EmailData for InviteRegisteredUser { | |||||||
|         let token = EmailToken::new_token( |         let token = EmailToken::new_token( | ||||||
|             self.recipient_email.clone(), |             self.recipient_email.clone(), | ||||||
|             Some(self.merchant_id.clone()), |             Some(self.merchant_id.clone()), | ||||||
|  |             domain::Origin::AcceptInvitationFromEmail, | ||||||
|             &self.settings, |             &self.settings, | ||||||
|         ) |         ) | ||||||
|         .await |         .await | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ use masking::Secret; | |||||||
|  |  | ||||||
| use super::UserFromStorage; | use super::UserFromStorage; | ||||||
| use crate::{ | use crate::{ | ||||||
|     core::errors::{UserErrors, UserResult}, |     core::errors::{StorageErrorExt, UserErrors, UserResult}, | ||||||
|     routes::AppState, |     routes::AppState, | ||||||
|     services::authentication as auth, |     services::authentication as auth, | ||||||
| }; | }; | ||||||
| @ -225,6 +225,25 @@ impl NextFlow { | |||||||
|     pub fn get_flow(&self) -> UserFlow { |     pub fn get_flow(&self) -> UserFlow { | ||||||
|         self.next_flow |         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 { | impl From<UserFlow> for TokenPurpose { | ||||||
|  | |||||||
| @ -394,6 +394,8 @@ pub enum Flow { | |||||||
|     CreateRole, |     CreateRole, | ||||||
|     /// Update Role |     /// Update Role | ||||||
|     UpdateRole, |     UpdateRole, | ||||||
|  |     /// User email flow start | ||||||
|  |     UserFromEmail, | ||||||
|     /// List initial webhook delivery attempts |     /// List initial webhook delivery attempts | ||||||
|     WebhookEventInitialDeliveryAttemptList, |     WebhookEventInitialDeliveryAttemptList, | ||||||
|     /// List delivery attempts for a webhook event |     /// List delivery attempts for a webhook event | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Mani Chandra
					Mani Chandra