feat(users): Create Token only support for pre-login user flow APIs (#4558)

This commit is contained in:
Mani Chandra
2024-05-07 14:57:05 +05:30
committed by GitHub
parent 71a070e269
commit 5ec00d96de
8 changed files with 343 additions and 41 deletions

View File

@ -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<T> ApiEventMetric for TokenOrPayloadResponse<T> {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
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,

View File

@ -227,9 +227,9 @@ pub struct TokenResponse {
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub enum SignInWithTokenResponse {
pub enum TokenOrPayloadResponse<T> {
Token(TokenResponse),
SignInResponse(SignInResponse),
Payload(T),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]

View File

@ -99,6 +99,7 @@ pub enum UserStatus {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct AcceptInvitationRequest {
pub merchant_ids: Vec<String>,
// TODO: Remove this once the token only api is being used
pub need_dashboard_entry_response: Option<bool>,
}

View File

@ -94,7 +94,7 @@ pub async fn get_user_details(
pub async fn signup(
state: AppState,
request: user_api::SignUpRequest,
) -> UserResponse<user_api::SignUpResponse> {
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignUpResponse>> {
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<user_api::TokenOrPayloadResponse<user_api::SignUpResponse>> {
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<user_api::SignInWithTokenResponse> {
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
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<user_api::SignInWithTokenResponse> {
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
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<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
let token = request.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();
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<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
let token = req.token.clone().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_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,

View File

@ -172,8 +172,7 @@ pub async fn accept_invitation(
state: AppState,
user_token: auth::UserFromSinglePurposeToken,
req: user_role_api::AcceptInvitationRequest,
_req_state: ReqState,
) -> UserResponse<user_api::DashboardEntryResponse> {
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
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<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
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,

View File

@ -57,15 +57,23 @@ pub async fn user_signup(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SignUpRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> 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<AppState>,
req: HttpRequest,
payload: web::Json<user_api::AcceptInviteFromEmailRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> 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<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::VerifyEmailRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> 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")]

View File

@ -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<AppState>,
req: HttpRequest,
json_payload: web::Json<user_role_api::AcceptInvitationRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> 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,
))

View File

@ -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<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)?;
}
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<UserFlow> for TokenPurpose {
@ -274,3 +298,15 @@ impl From<JWTFlow> for TokenPurpose {
}
}
}
impl From<SPTFlow> for UserFlow {
fn from(value: SPTFlow) -> Self {
Self::SPTFlow(value)
}
}
impl From<JWTFlow> for UserFlow {
fn from(value: JWTFlow) -> Self {
Self::JWTFlow(value)
}
}