mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(user_roles): Add accept invitation API and UserJWTAuth (#3365)
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||||
|
|
||||||
use crate::user_role::{
|
use crate::user_role::{
|
||||||
AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, RoleInfoResponse,
|
AcceptInvitationRequest, AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse,
|
||||||
UpdateUserRoleRequest,
|
RoleInfoResponse, UpdateUserRoleRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
common_utils::impl_misc_api_event_type!(
|
common_utils::impl_misc_api_event_type!(
|
||||||
@ -10,5 +10,6 @@ common_utils::impl_misc_api_event_type!(
|
|||||||
RoleInfoResponse,
|
RoleInfoResponse,
|
||||||
GetRoleRequest,
|
GetRoleRequest,
|
||||||
AuthorizationInfoResponse,
|
AuthorizationInfoResponse,
|
||||||
UpdateUserRoleRequest
|
UpdateUserRoleRequest,
|
||||||
|
AcceptInvitationRequest
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
use crate::user::DashboardEntryResponse;
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
#[derive(Debug, serde::Serialize)]
|
||||||
pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
|
pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
|
||||||
|
|
||||||
@ -91,3 +93,11 @@ pub enum UserStatus {
|
|||||||
Active,
|
Active,
|
||||||
InvitationSent,
|
InvitationSent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct AcceptInvitationRequest {
|
||||||
|
pub merchant_ids: Vec<String>,
|
||||||
|
pub need_dashboard_entry_response: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type AcceptInvitationResponse = DashboardEntryResponse;
|
||||||
|
|||||||
@ -90,11 +90,10 @@ pub async fn signup(
|
|||||||
UserStatus::Active,
|
UserStatus::Active,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let token =
|
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
|
||||||
utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?;
|
|
||||||
|
|
||||||
Ok(ApplicationResponse::Json(
|
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)?;
|
user_from_db.compare_password(request.password)?;
|
||||||
|
|
||||||
let user_role = user_from_db.get_role_from_db(state.clone()).await?;
|
let user_role = user_from_db.get_role_from_db(state.clone()).await?;
|
||||||
let token =
|
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
|
||||||
utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?;
|
|
||||||
|
|
||||||
Ok(ApplicationResponse::Json(
|
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())
|
.ok_or(UserErrors::InvalidRoleOperation.into())
|
||||||
.attach_printable("User doesn't have access to switch")?;
|
.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())
|
(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_from_db: domain::UserFromStorage = user.into();
|
||||||
let user_role = user_from_db.get_role_from_db(state.clone()).await?;
|
let user_role = user_from_db.get_role_from_db(state.clone()).await?;
|
||||||
let token =
|
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
|
||||||
utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?;
|
|
||||||
|
|
||||||
Ok(ApplicationResponse::Json(
|
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 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 error_stack::ResultExt;
|
||||||
|
use router_env::logger;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::errors::{UserErrors, UserResponse},
|
core::errors::{UserErrors, UserResponse},
|
||||||
@ -115,3 +116,48 @@ pub async fn update_user_role(
|
|||||||
|
|
||||||
Ok(ApplicationResponse::StatusOk)
|
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").route(web::get().to(get_role_from_token)))
|
||||||
.service(web::resource("/role/{role_id}").route(web::get().to(get_role)))
|
.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").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("/update").route(web::post().to(update_user_account_details)))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/data")
|
web::resource("/data")
|
||||||
|
|||||||
@ -185,7 +185,8 @@ impl From<Flow> for ApiIdentifier {
|
|||||||
| Flow::GetRole
|
| Flow::GetRole
|
||||||
| Flow::GetRoleFromToken
|
| Flow::GetRoleFromToken
|
||||||
| Flow::UpdateUserRole
|
| Flow::UpdateUserRole
|
||||||
| Flow::GetAuthorizationInfo => Self::UserRole,
|
| Flow::GetAuthorizationInfo
|
||||||
|
| Flow::AcceptInvitation => Self::UserRole,
|
||||||
|
|
||||||
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
||||||
Self::ConnectorOnboarding
|
Self::ConnectorOnboarding
|
||||||
|
|||||||
@ -96,3 +96,22 @@ pub async fn update_user_role(
|
|||||||
))
|
))
|
||||||
.await
|
.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,
|
merchant_id: String,
|
||||||
user_id: Option<String>,
|
user_id: Option<String>,
|
||||||
},
|
},
|
||||||
|
UserJwt {
|
||||||
|
user_id: String,
|
||||||
|
},
|
||||||
MerchantId {
|
MerchantId {
|
||||||
merchant_id: String,
|
merchant_id: String,
|
||||||
},
|
},
|
||||||
@ -81,11 +84,32 @@ impl AuthenticationType {
|
|||||||
user_id: _,
|
user_id: _,
|
||||||
}
|
}
|
||||||
| Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()),
|
| 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)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub struct AuthToken {
|
pub struct AuthToken {
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
@ -276,6 +300,33 @@ pub async fn get_admin_api_key(
|
|||||||
.await
|
.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)]
|
#[derive(Debug)]
|
||||||
pub struct AdminApiAuth;
|
pub struct AdminApiAuth;
|
||||||
|
|
||||||
|
|||||||
@ -739,7 +739,7 @@ impl UserFromStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[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 {
|
if self.0.is_verified {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ impl UserFromToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_jwt_auth_token(
|
pub async fn generate_jwt_auth_token(
|
||||||
state: AppState,
|
state: &AppState,
|
||||||
user: &UserFromStorage,
|
user: &UserFromStorage,
|
||||||
user_role: &UserRole,
|
user_role: &UserRole,
|
||||||
) -> UserResult<Secret<String>> {
|
) -> UserResult<Secret<String>> {
|
||||||
@ -89,17 +89,13 @@ pub async fn generate_jwt_auth_token_with_custom_role_attributes(
|
|||||||
Ok(Secret::new(token))
|
Ok(Secret::new(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn get_dashboard_entry_response(
|
pub fn get_dashboard_entry_response(
|
||||||
state: AppState,
|
state: &AppState,
|
||||||
user: UserFromStorage,
|
user: UserFromStorage,
|
||||||
user_role: UserRole,
|
user_role: UserRole,
|
||||||
token: Secret<String>,
|
token: Secret<String>,
|
||||||
) -> UserResult<user_api::DashboardEntryResponse> {
|
) -> UserResult<user_api::DashboardEntryResponse> {
|
||||||
#[cfg(feature = "email")]
|
let verification_days_left = get_verification_days_left(state, &user)?;
|
||||||
let verification_days_left = user.get_verification_days_left(state)?;
|
|
||||||
#[cfg(not(feature = "email"))]
|
|
||||||
let verification_days_left = None;
|
|
||||||
|
|
||||||
Ok(user_api::DashboardEntryResponse {
|
Ok(user_api::DashboardEntryResponse {
|
||||||
merchant_id: user_role.merchant_id,
|
merchant_id: user_role.merchant_id,
|
||||||
@ -111,3 +107,14 @@ pub fn get_dashboard_entry_response(
|
|||||||
user_role: user_role.role_id,
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -335,6 +335,8 @@ pub enum Flow {
|
|||||||
VerifyEmailRequest,
|
VerifyEmailRequest,
|
||||||
/// Update user account details
|
/// Update user account details
|
||||||
UpdateUserAccountDetails,
|
UpdateUserAccountDetails,
|
||||||
|
/// Accept user invitation
|
||||||
|
AcceptInvitation,
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|||||||
Reference in New Issue
Block a user