mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +08:00 
			
		
		
		
	feat(user_roles): Add accept invitation API and UserJWTAuth (#3365)
				
					
				
			This commit is contained in:
		| @ -90,11 +90,10 @@ pub async fn signup( | ||||
|             UserStatus::Active, | ||||
|         ) | ||||
|         .await?; | ||||
|     let token = | ||||
|         utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; | ||||
|     let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; | ||||
|  | ||||
|     Ok(ApplicationResponse::Json( | ||||
|         utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, | ||||
|         utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, | ||||
|     )) | ||||
| } | ||||
|  | ||||
| @ -118,11 +117,10 @@ pub async fn signin( | ||||
|     user_from_db.compare_password(request.password)?; | ||||
|  | ||||
|     let user_role = user_from_db.get_role_from_db(state.clone()).await?; | ||||
|     let token = | ||||
|         utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; | ||||
|     let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; | ||||
|  | ||||
|     Ok(ApplicationResponse::Json( | ||||
|         utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, | ||||
|         utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, | ||||
|     )) | ||||
| } | ||||
|  | ||||
| @ -600,7 +598,7 @@ pub async fn switch_merchant_id( | ||||
|             .ok_or(UserErrors::InvalidRoleOperation.into()) | ||||
|             .attach_printable("User doesn't have access to switch")?; | ||||
|  | ||||
|         let token = utils::user::generate_jwt_auth_token(state, &user, user_role).await?; | ||||
|         let token = utils::user::generate_jwt_auth_token(&state, &user, user_role).await?; | ||||
|         (token, user_role.role_id.clone()) | ||||
|     }; | ||||
|  | ||||
| @ -712,11 +710,10 @@ pub async fn verify_email( | ||||
|  | ||||
|     let user_from_db: domain::UserFromStorage = user.into(); | ||||
|     let user_role = user_from_db.get_role_from_db(state.clone()).await?; | ||||
|     let token = | ||||
|         utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; | ||||
|     let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; | ||||
|  | ||||
|     Ok(ApplicationResponse::Json( | ||||
|         utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, | ||||
|         utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, | ||||
|     )) | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| use api_models::user_role as user_role_api; | ||||
| use diesel_models::user_role::UserRoleUpdate; | ||||
| use diesel_models::{enums::UserStatus, user_role::UserRoleUpdate}; | ||||
| use error_stack::ResultExt; | ||||
| use router_env::logger; | ||||
|  | ||||
| use crate::{ | ||||
|     core::errors::{UserErrors, UserResponse}, | ||||
| @ -115,3 +116,48 @@ pub async fn update_user_role( | ||||
|  | ||||
|     Ok(ApplicationResponse::StatusOk) | ||||
| } | ||||
|  | ||||
| pub async fn accept_invitation( | ||||
|     state: AppState, | ||||
|     user_token: auth::UserWithoutMerchantFromToken, | ||||
|     req: user_role_api::AcceptInvitationRequest, | ||||
| ) -> UserResponse<user_role_api::AcceptInvitationResponse> { | ||||
|     let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async { | ||||
|         state | ||||
|             .store | ||||
|             .update_user_role_by_user_id_merchant_id( | ||||
|                 user_token.user_id.as_str(), | ||||
|                 merchant_id, | ||||
|                 UserRoleUpdate::UpdateStatus { | ||||
|                     status: UserStatus::Active, | ||||
|                     modified_by: user_token.user_id.clone(), | ||||
|                 }, | ||||
|             ) | ||||
|             .await | ||||
|             .map_err(|e| { | ||||
|                 logger::error!("Error while accepting invitation {}", e); | ||||
|             }) | ||||
|             .ok() | ||||
|     })) | ||||
|     .await | ||||
|     .into_iter() | ||||
|     .reduce(Option::or) | ||||
|     .flatten() | ||||
|     .ok_or(UserErrors::MerchantIdNotFound)?; | ||||
|  | ||||
|     if let Some(true) = req.need_dashboard_entry_response { | ||||
|         let user_from_db = state | ||||
|             .store | ||||
|             .find_user_by_id(user_token.user_id.as_str()) | ||||
|             .await | ||||
|             .change_context(UserErrors::InternalServerError)? | ||||
|             .into(); | ||||
|  | ||||
|         let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; | ||||
|         return Ok(ApplicationResponse::Json( | ||||
|             utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     Ok(ApplicationResponse::StatusOk) | ||||
| } | ||||
|  | ||||
| @ -922,6 +922,7 @@ impl User { | ||||
|             .service(web::resource("/role").route(web::get().to(get_role_from_token))) | ||||
|             .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) | ||||
|             .service(web::resource("/user/invite").route(web::post().to(invite_user))) | ||||
|             .service(web::resource("/user/invite/accept").route(web::post().to(accept_invitation))) | ||||
|             .service(web::resource("/update").route(web::post().to(update_user_account_details))) | ||||
|             .service( | ||||
|                 web::resource("/data") | ||||
|  | ||||
| @ -185,7 +185,8 @@ impl From<Flow> for ApiIdentifier { | ||||
|             | Flow::GetRole | ||||
|             | Flow::GetRoleFromToken | ||||
|             | Flow::UpdateUserRole | ||||
|             | Flow::GetAuthorizationInfo => Self::UserRole, | ||||
|             | Flow::GetAuthorizationInfo | ||||
|             | Flow::AcceptInvitation => Self::UserRole, | ||||
|  | ||||
|             Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { | ||||
|                 Self::ConnectorOnboarding | ||||
|  | ||||
| @ -96,3 +96,22 @@ pub async fn update_user_role( | ||||
|     )) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| pub async fn accept_invitation( | ||||
|     state: web::Data<AppState>, | ||||
|     req: HttpRequest, | ||||
|     json_payload: web::Json<user_role_api::AcceptInvitationRequest>, | ||||
| ) -> HttpResponse { | ||||
|     let flow = Flow::AcceptInvitation; | ||||
|     let payload = json_payload.into_inner(); | ||||
|     Box::pin(api::server_wrap( | ||||
|         flow, | ||||
|         state.clone(), | ||||
|         &req, | ||||
|         payload, | ||||
|         user_role_core::accept_invitation, | ||||
|         &auth::UserWithoutMerchantJWTAuth, | ||||
|         api_locking::LockAction::NotApplicable, | ||||
|     )) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| @ -55,6 +55,9 @@ pub enum AuthenticationType { | ||||
|         merchant_id: String, | ||||
|         user_id: Option<String>, | ||||
|     }, | ||||
|     UserJwt { | ||||
|         user_id: String, | ||||
|     }, | ||||
|     MerchantId { | ||||
|         merchant_id: String, | ||||
|     }, | ||||
| @ -81,11 +84,32 @@ impl AuthenticationType { | ||||
|                 user_id: _, | ||||
|             } | ||||
|             | Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()), | ||||
|             Self::AdminApiKey | Self::NoAuth => None, | ||||
|             Self::AdminApiKey | Self::UserJwt { .. } | Self::NoAuth => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug)] | ||||
| pub struct UserWithoutMerchantFromToken { | ||||
|     pub user_id: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct UserAuthToken { | ||||
|     pub user_id: String, | ||||
|     pub exp: u64, | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "olap")] | ||||
| impl UserAuthToken { | ||||
|     pub async fn new_token(user_id: String, settings: &settings::Settings) -> UserResult<String> { | ||||
|         let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); | ||||
|         let exp = jwt::generate_exp(exp_duration)?.as_secs(); | ||||
|         let token_payload = Self { user_id, exp }; | ||||
|         jwt::generate_jwt(&token_payload, settings).await | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct AuthToken { | ||||
|     pub user_id: String, | ||||
| @ -276,6 +300,33 @@ pub async fn get_admin_api_key( | ||||
|         .await | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct UserWithoutMerchantJWTAuth; | ||||
|  | ||||
| #[cfg(feature = "olap")] | ||||
| #[async_trait] | ||||
| impl<A> AuthenticateAndFetch<UserWithoutMerchantFromToken, A> for UserWithoutMerchantJWTAuth | ||||
| where | ||||
|     A: AppStateInfo + Sync, | ||||
| { | ||||
|     async fn authenticate_and_fetch( | ||||
|         &self, | ||||
|         request_headers: &HeaderMap, | ||||
|         state: &A, | ||||
|     ) -> RouterResult<(UserWithoutMerchantFromToken, AuthenticationType)> { | ||||
|         let payload = parse_jwt_payload::<A, UserAuthToken>(request_headers, state).await?; | ||||
|  | ||||
|         Ok(( | ||||
|             UserWithoutMerchantFromToken { | ||||
|                 user_id: payload.user_id.clone(), | ||||
|             }, | ||||
|             AuthenticationType::UserJwt { | ||||
|                 user_id: payload.user_id, | ||||
|             }, | ||||
|         )) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct AdminApiAuth; | ||||
|  | ||||
|  | ||||
| @ -739,7 +739,7 @@ impl UserFromStorage { | ||||
|     } | ||||
|  | ||||
|     #[cfg(feature = "email")] | ||||
|     pub fn get_verification_days_left(&self, state: AppState) -> UserResult<Option<i64>> { | ||||
|     pub fn get_verification_days_left(&self, state: &AppState) -> UserResult<Option<i64>> { | ||||
|         if self.0.is_verified { | ||||
|             return Ok(None); | ||||
|         } | ||||
|  | ||||
| @ -56,7 +56,7 @@ impl UserFromToken { | ||||
| } | ||||
|  | ||||
| pub async fn generate_jwt_auth_token( | ||||
|     state: AppState, | ||||
|     state: &AppState, | ||||
|     user: &UserFromStorage, | ||||
|     user_role: &UserRole, | ||||
| ) -> UserResult<Secret<String>> { | ||||
| @ -89,17 +89,13 @@ pub async fn generate_jwt_auth_token_with_custom_role_attributes( | ||||
|     Ok(Secret::new(token)) | ||||
| } | ||||
|  | ||||
| #[allow(unused_variables)] | ||||
| pub fn get_dashboard_entry_response( | ||||
|     state: AppState, | ||||
|     state: &AppState, | ||||
|     user: UserFromStorage, | ||||
|     user_role: UserRole, | ||||
|     token: Secret<String>, | ||||
| ) -> UserResult<user_api::DashboardEntryResponse> { | ||||
|     #[cfg(feature = "email")] | ||||
|     let verification_days_left = user.get_verification_days_left(state)?; | ||||
|     #[cfg(not(feature = "email"))] | ||||
|     let verification_days_left = None; | ||||
|     let verification_days_left = get_verification_days_left(state, &user)?; | ||||
|  | ||||
|     Ok(user_api::DashboardEntryResponse { | ||||
|         merchant_id: user_role.merchant_id, | ||||
| @ -111,3 +107,14 @@ pub fn get_dashboard_entry_response( | ||||
|         user_role: user_role.role_id, | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[allow(unused_variables)] | ||||
| pub fn get_verification_days_left( | ||||
|     state: &AppState, | ||||
|     user: &UserFromStorage, | ||||
| ) -> UserResult<Option<i64>> { | ||||
|     #[cfg(feature = "email")] | ||||
|     return user.get_verification_days_left(state); | ||||
|     #[cfg(not(feature = "email"))] | ||||
|     return Ok(None); | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Mani Chandra
					Mani Chandra