diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 14676656d0..b29b28f013 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -14,8 +14,8 @@ use crate::user::{ CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse, InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest, - SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest, - SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenResponse, + SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, + SwitchMerchantIdRequest, TokenOrPayloadResponse, TokenResponse, UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest, }; @@ -38,6 +38,12 @@ impl ApiEventMetric for VerifyTokenResponse { } } +impl ApiEventMetric for TokenOrPayloadResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} + common_utils::impl_misc_api_event_type!( SignUpRequest, SignUpWithMerchantIdRequest, @@ -62,7 +68,6 @@ common_utils::impl_misc_api_event_type!( SignInResponse, UpdateUserAccountDetailsRequest, GetUserDetailsResponse, - SignInWithTokenResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse, TokenResponse, diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index a6d5cf3635..fe4e37f8a8 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -227,9 +227,9 @@ pub struct TokenResponse { #[derive(Debug, serde::Serialize)] #[serde(untagged)] -pub enum SignInWithTokenResponse { +pub enum TokenOrPayloadResponse { Token(TokenResponse), - SignInResponse(SignInResponse), + Payload(T), } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 25c1c5ecd4..08d63d9097 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -99,6 +99,7 @@ pub enum UserStatus { #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct AcceptInvitationRequest { pub merchant_ids: Vec, + // TODO: Remove this once the token only api is being used pub need_dashboard_entry_response: Option, } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 58a6452c9b..6ad0afa910 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -94,7 +94,7 @@ pub async fn get_user_details( pub async fn signup( state: AppState, request: user_api::SignUpRequest, -) -> UserResponse { +) -> UserResponse> { let new_user = domain::NewUser::try_from(request)?; new_user .get_new_merchant() @@ -117,13 +117,48 @@ pub async fn signup( let response = utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?; + auth::cookies::set_cookie_response(user_api::TokenOrPayloadResponse::Payload(response), token) +} + +pub async fn signup_token_only_flow( + state: AppState, + request: user_api::SignUpRequest, +) -> UserResponse> { + let new_user = domain::NewUser::try_from(request)?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + + let next_flow = + domain::NextFlow::from_origin(domain::Origin::SignUp, user_from_db.clone(), &state).await?; + + let token = next_flow + .get_token_with_user_role(&state, &user_role) + .await?; + + let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse { + token: token.clone(), + token_type: next_flow.get_flow().into(), + }); auth::cookies::set_cookie_response(response, token) } pub async fn signin( state: AppState, request: user_api::SignInRequest, -) -> UserResponse { +) -> UserResponse> { let user_from_db: domain::UserFromStorage = state .store .find_user_by_email(&request.email) @@ -161,16 +196,13 @@ pub async fn signin( let response = signin_strategy.get_signin_response(&state).await?; let token = utils::user::get_token_from_signin_response(&response); - auth::cookies::set_cookie_response( - user_api::SignInWithTokenResponse::SignInResponse(response), - token, - ) + auth::cookies::set_cookie_response(user_api::TokenOrPayloadResponse::Payload(response), token) } pub async fn signin_token_only_flow( state: AppState, request: user_api::SignInRequest, -) -> UserResponse { +) -> UserResponse> { let user_from_db: domain::UserFromStorage = state .store .find_user_by_email(&request.email) @@ -185,7 +217,7 @@ pub async fn signin_token_only_flow( let token = next_flow.get_token(&state).await?; - let response = user_api::SignInWithTokenResponse::Token(user_api::TokenResponse { + let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse { token: token.clone(), token_type: next_flow.get_flow().into(), }); @@ -820,6 +852,73 @@ pub async fn accept_invite_from_email( auth::cookies::set_cookie_response(response, token) } +#[cfg(feature = "email")] +pub async fn accept_invite_from_email_token_only_flow( + state: AppState, + user_token: auth::UserFromSinglePurposeToken, + request: user_api::AcceptInviteFromEmailRequest, +) -> UserResponse> { + let token = request.token.expose(); + + let email_token = auth::decode_jwt::(&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(); + + if user_from_db.get_user_id() != user_token.user_id { + return Err(UserErrors::LinkInvalid.into()); + } + + let merchant_id = email_token + .get_merchant_id() + .ok_or(UserErrors::LinkInvalid)?; + + let user_role = state + .store + .update_user_role_by_user_id_merchant_id( + user_from_db.get_user_id(), + merchant_id, + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user_from_db.get_user_id().to_string(), + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token) + .await + .map_err(|e| logger::error!(?e)); + + let current_flow = domain::CurrentFlow::new( + user_token.origin, + domain::SPTFlow::AcceptInvitationFromEmail.into(), + )?; + let next_flow = current_flow.next(user_from_db.clone(), &state).await?; + + let token = next_flow + .get_token_with_user_role(&state, &user_role) + .await?; + + let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse { + token: token.clone(), + token_type: next_flow.get_flow().into(), + }); + auth::cookies::set_cookie_response(response, token) +} + pub async fn create_internal_user( state: AppState, request: user_api::CreateInternalUserRequest, @@ -1196,6 +1295,60 @@ pub async fn verify_email( auth::cookies::set_cookie_response(response, token) } +#[cfg(feature = "email")] +pub async fn verify_email_token_only_flow( + state: AppState, + user_token: auth::UserFromSinglePurposeToken, + req: user_api::VerifyEmailRequest, +) -> UserResponse> { + let token = req.token.clone().expose(); + let email_token = auth::decode_jwt::(&token, &state) + .await + .change_context(UserErrors::LinkInvalid)?; + + auth::blacklist::check_email_token_in_blacklist(&state, &token).await?; + + let user_from_email = state + .store + .find_user_by_email( + &email_token + .get_email() + .change_context(UserErrors::InternalServerError)?, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + if user_from_email.user_id != user_token.user_id { + return Err(UserErrors::LinkInvalid.into()); + } + + let user_from_db: domain::UserFromStorage = state + .store + .update_user_by_user_id( + user_from_email.user_id.as_str(), + storage_user::UserUpdate::VerifyUser, + ) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token) + .await + .map_err(|e| logger::error!(?e)); + + let current_flow = + domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::VerifyEmail.into())?; + let next_flow = current_flow.next(user_from_db, &state).await?; + let token = next_flow.get_token(&state).await?; + + let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse { + token: token.clone(), + token_type: next_flow.get_flow().into(), + }); + + auth::cookies::set_cookie_response(response, token) +} + #[cfg(feature = "email")] pub async fn send_verification_mail( state: AppState, diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 3f913b88a3..ae20ec51ad 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -172,8 +172,7 @@ pub async fn accept_invitation( state: AppState, user_token: auth::UserFromSinglePurposeToken, req: user_role_api::AcceptInvitationRequest, - _req_state: ReqState, -) -> UserResponse { +) -> UserResponse> { let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async { state .store @@ -215,12 +214,65 @@ pub async fn accept_invitation( user_role, token.clone(), )?; - return auth::cookies::set_cookie_response(response, token); + return auth::cookies::set_cookie_response( + user_api::TokenOrPayloadResponse::Payload(response), + token, + ); } Ok(ApplicationResponse::StatusOk) } +pub async fn accept_invitation_token_only_flow( + state: AppState, + user_token: auth::UserFromSinglePurposeToken, + 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)?; + + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_id(user_token.user_id.as_str()) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + let current_flow = + domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::MerchantSelect.into())?; + let next_flow = current_flow.next(user_from_db.clone(), &state).await?; + + let token = next_flow + .get_token_with_user_role(&state, &user_role) + .await?; + + let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse { + token: token.clone(), + token_type: next_flow.get_flow().into(), + }); + auth::cookies::set_cookie_response(response, token) +} + pub async fn delete_user_role( state: AppState, user_from_token: auth::UserFromToken, diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index ede6edbceb..a3f1816850 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -57,15 +57,23 @@ pub async fn user_signup( state: web::Data, http_req: HttpRequest, json_payload: web::Json, + query: web::Query, ) -> HttpResponse { let flow = Flow::UserSignUp; let req_payload = json_payload.into_inner(); + let is_token_only = query.into_inner().token_only; Box::pin(api::server_wrap( flow.clone(), state, &http_req, req_payload.clone(), - |state, _, req_body, _| user_core::signup(state, req_body), + |state, _, req_body, _| async move { + if let Some(true) = is_token_only { + user_core::signup_token_only_flow(state, req_body).await + } else { + user_core::signup(state, req_body).await + } + }, &auth::NoAuth, api_locking::LockAction::NotApplicable, )) @@ -428,18 +436,37 @@ pub async fn accept_invite_from_email( state: web::Data, req: HttpRequest, payload: web::Json, + query: web::Query, ) -> HttpResponse { let flow = Flow::AcceptInviteFromEmail; - Box::pin(api::server_wrap( - flow, - state.clone(), - &req, - payload.into_inner(), - |state, _, request_payload, _| user_core::accept_invite_from_email(state, request_payload), - &auth::NoAuth, - api_locking::LockAction::NotApplicable, - )) - .await + let is_token_only = query.into_inner().token_only; + if let Some(true) = is_token_only { + Box::pin(api::server_wrap( + flow.clone(), + state, + &req, + payload.into_inner(), + |state, user, req_payload, _| { + user_core::accept_invite_from_email_token_only_flow(state, user, req_payload) + }, + &auth::SinglePurposeJWTAuth(common_enums::TokenPurpose::AcceptInvitationFromEmail), + api_locking::LockAction::NotApplicable, + )) + .await + } else { + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, _, request_payload, _| { + user_core::accept_invite_from_email(state, request_payload) + }, + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } } #[cfg(feature = "email")] @@ -447,18 +474,35 @@ pub async fn verify_email( state: web::Data, http_req: HttpRequest, json_payload: web::Json, + query: web::Query, ) -> HttpResponse { let flow = Flow::VerifyEmail; - Box::pin(api::server_wrap( - flow.clone(), - state, - &http_req, - json_payload.into_inner(), - |state, _, req_payload, _| user_core::verify_email(state, req_payload), - &auth::NoAuth, - api_locking::LockAction::NotApplicable, - )) - .await + let is_token_only = query.into_inner().token_only; + if let Some(true) = is_token_only { + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + json_payload.into_inner(), + |state, user, req_payload, _| { + user_core::verify_email_token_only_flow(state, user, req_payload) + }, + &auth::SinglePurposeJWTAuth(common_enums::TokenPurpose::VerifyEmail), + api_locking::LockAction::NotApplicable, + )) + .await + } else { + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + json_payload.into_inner(), + |state, _, req_payload, _| user_core::verify_email(state, req_payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await + } } #[cfg(feature = "email")] diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 59c07b7985..d406783cc7 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -1,5 +1,8 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::user_role::{self as user_role_api, role as role_api}; +use api_models::{ + user as user_api, + user_role::{self as user_role_api, role as role_api}, +}; use common_enums::TokenPurpose; use router_env::Flow; @@ -206,15 +209,23 @@ pub async fn accept_invitation( state: web::Data, req: HttpRequest, json_payload: web::Json, + query: web::Query, ) -> HttpResponse { let flow = Flow::AcceptInvitation; let payload = json_payload.into_inner(); + let is_token_only = query.into_inner().token_only; Box::pin(api::server_wrap( flow, state.clone(), &req, payload, - user_role_core::accept_invitation, + |state, user, req_body, _| async move { + if let Some(true) = is_token_only { + user_role_core::accept_invitation_token_only_flow(state, user, req_body).await + } else { + user_role_core::accept_invitation(state, user, req_body).await + } + }, &auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite), api_locking::LockAction::NotApplicable, )) diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs index e460bc7464..616c595b24 100644 --- a/crates/router/src/types/domain/user/decision_manager.rs +++ b/crates/router/src/types/domain/user/decision_manager.rs @@ -7,6 +7,7 @@ use crate::{ core::errors::{StorageErrorExt, UserErrors, UserResult}, routes::AppState, services::authentication as auth, + utils, }; #[derive(Eq, PartialEq, Clone, Copy)] @@ -150,8 +151,9 @@ const VERIFY_EMAIL_FLOW: [UserFlow; 5] = [ UserFlow::JWTFlow(JWTFlow::UserInfo), ]; -const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 4] = [ +const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 5] = [ UserFlow::SPTFlow(SPTFlow::TOTP), + UserFlow::SPTFlow(SPTFlow::VerifyEmail), UserFlow::SPTFlow(SPTFlow::AcceptInvitationFromEmail), UserFlow::SPTFlow(SPTFlow::ForceSetPassword), UserFlow::JWTFlow(JWTFlow::UserInfo), @@ -234,16 +236,38 @@ impl NextFlow { { 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)?; + utils::user_role::set_role_permissions_in_cache_by_user_role(state, &user_role) + .await; + jwt_flow.generate_jwt(state, self, &user_role).await } } } + + pub async fn get_token_with_user_role( + &self, + state: &AppState, + user_role: &UserRole, + ) -> UserResult> { + 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)?; + } + utils::user_role::set_role_permissions_in_cache_by_user_role(state, user_role) + .await; + + jwt_flow.generate_jwt(state, self, user_role).await + } + } + } } impl From for TokenPurpose { @@ -274,3 +298,15 @@ impl From for TokenPurpose { } } } + +impl From for UserFlow { + fn from(value: SPTFlow) -> Self { + Self::SPTFlow(value) + } +} + +impl From for UserFlow { + fn from(value: JWTFlow) -> Self { + Self::JWTFlow(value) + } +}