From a47372a451b60defda35fa212565b889ed5b2d2b Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 19 Jan 2024 18:35:04 +0530 Subject: [PATCH] feat(user_roles): Add accept invitation API and `UserJWTAuth` (#3365) --- crates/api_models/src/events/user_role.rs | 7 +-- crates/api_models/src/user_role.rs | 10 ++++ crates/router/src/core/user.rs | 17 +++---- crates/router/src/core/user_role.rs | 48 +++++++++++++++++- crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/user_role.rs | 19 +++++++ crates/router/src/services/authentication.rs | 53 +++++++++++++++++++- crates/router/src/types/domain/user.rs | 2 +- crates/router/src/utils/user.rs | 21 +++++--- crates/router_env/src/logger/types.rs | 2 + 11 files changed, 159 insertions(+), 24 deletions(-) diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs index aa8d13dab6..c8d8fd96a7 100644 --- a/crates/api_models/src/events/user_role.rs +++ b/crates/api_models/src/events/user_role.rs @@ -1,8 +1,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user_role::{ - AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, RoleInfoResponse, - UpdateUserRoleRequest, + AcceptInvitationRequest, AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, + RoleInfoResponse, UpdateUserRoleRequest, }; common_utils::impl_misc_api_event_type!( @@ -10,5 +10,6 @@ common_utils::impl_misc_api_event_type!( RoleInfoResponse, GetRoleRequest, AuthorizationInfoResponse, - UpdateUserRoleRequest + UpdateUserRoleRequest, + AcceptInvitationRequest ); diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index b057f8ca8b..d2548935f6 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -1,3 +1,5 @@ +use crate::user::DashboardEntryResponse; + #[derive(Debug, serde::Serialize)] pub struct ListRolesResponse(pub Vec); @@ -91,3 +93,11 @@ pub enum UserStatus { Active, InvitationSent, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct AcceptInvitationRequest { + pub merchant_ids: Vec, + pub need_dashboard_entry_response: Option, +} + +pub type AcceptInvitationResponse = DashboardEntryResponse; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 729cef65c2..3384e22900 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -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)?, )) } diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index d8ff836e1f..245f8d246d 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -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 { + 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) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 3d63df2fe8..4345109a67 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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") diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index d3a2e1af9a..1c967222dc 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -185,7 +185,8 @@ impl From 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 diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index fe305942d0..73b1ef1b01 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -96,3 +96,22 @@ pub async fn update_user_role( )) .await } + +pub async fn accept_invitation( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> 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 +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 3370912394..eaadc0d5c7 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -55,6 +55,9 @@ pub enum AuthenticationType { merchant_id: String, user_id: Option, }, + 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 { + 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 AuthenticateAndFetch 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::(request_headers, state).await?; + + Ok(( + UserWithoutMerchantFromToken { + user_id: payload.user_id.clone(), + }, + AuthenticationType::UserJwt { + user_id: payload.user_id, + }, + )) + } +} + #[derive(Debug)] pub struct AdminApiAuth; diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 53c88f8aea..bbe21f289a 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -739,7 +739,7 @@ impl UserFromStorage { } #[cfg(feature = "email")] - pub fn get_verification_days_left(&self, state: AppState) -> UserResult> { + pub fn get_verification_days_left(&self, state: &AppState) -> UserResult> { if self.0.is_verified { return Ok(None); } diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index a115fa2a2d..a3f9e7978a 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -56,7 +56,7 @@ impl UserFromToken { } pub async fn generate_jwt_auth_token( - state: AppState, + state: &AppState, user: &UserFromStorage, user_role: &UserRole, ) -> UserResult> { @@ -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, ) -> UserResult { - #[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> { + #[cfg(feature = "email")] + return user.get_verification_days_left(state); + #[cfg(not(feature = "email"))] + return Ok(None); +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 7e3a692517..ba323ebc5e 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -335,6 +335,8 @@ pub enum Flow { VerifyEmailRequest, /// Update user account details UpdateUserAccountDetails, + /// Accept user invitation + AcceptInvitation, } ///