Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(user_roles): Add accept invitation API and UserJWTAuth #3365

Merged
merged 6 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions crates/api_models/src/events/user_role.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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!(
ListRolesResponse,
RoleInfoResponse,
GetRoleRequest,
AuthorizationInfoResponse,
UpdateUserRoleRequest
UpdateUserRoleRequest,
AcceptInvitationRequest
);
10 changes: 10 additions & 0 deletions crates/api_models/src/user_role.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::user::DashboardEntryResponse;

#[derive(Debug, serde::Serialize)]
pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);

Expand Down Expand Up @@ -89,3 +91,11 @@ pub enum UserStatus {
Active,
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;
17 changes: 7 additions & 10 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?,
))
}

Expand All @@ -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)?,
))
}

Expand Down Expand Up @@ -598,7 +596,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())
};

Expand Down Expand Up @@ -710,11 +708,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)?,
))
}

Expand Down
48 changes: 47 additions & 1 deletion crates/router/src/core/user_role.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -99,3 +100,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)
}
1 change: 1 addition & 0 deletions crates/router/src/routes/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,7 @@ impl User {
.service(web::resource("/role/list").route(web::get().to(list_roles)))
.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/accept_invite").route(web::post().to(accept_invitation)))
ThisIsMani marked this conversation as resolved.
Show resolved Hide resolved
.service(
web::resource("/data")
.route(web::get().to(get_multiple_dashboard_metadata))
Expand Down
8 changes: 5 additions & 3 deletions crates/router/src/routes/lock_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,11 @@ impl From<Flow> for ApiIdentifier {
| Flow::VerifyEmail
| Flow::VerifyEmailRequest => Self::User,

Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
Self::UserRole
}
Flow::ListRoles
| Flow::GetRole
| Flow::UpdateUserRole
| Flow::GetAuthorizationInfo
| Flow::AcceptInvitation => Self::UserRole,

Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
Self::ConnectorOnboarding
Expand Down
19 changes: 19 additions & 0 deletions crates/router/src/routes/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,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::UserJWTAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
53 changes: 52 additions & 1 deletion crates/router/src/services/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub enum AuthenticationType {
merchant_id: String,
user_id: Option<String>,
},
UserJwt {
user_id: String,
},
MerchantId {
merchant_id: String,
},
Expand All @@ -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,
Expand Down Expand Up @@ -276,6 +300,33 @@ pub async fn get_admin_api_key(
.await
}

#[derive(Debug)]
pub struct UserJWTAuth;
ThisIsMani marked this conversation as resolved.
Show resolved Hide resolved

#[cfg(feature = "olap")]
#[async_trait]
impl<A> AuthenticateAndFetch<UserWithoutMerchantFromToken, A> for UserJWTAuth
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;

Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/types/domain/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
21 changes: 14 additions & 7 deletions crates/router/src/utils/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>> {
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
2 changes: 2 additions & 0 deletions crates/router_env/src/logger/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@ pub enum Flow {
VerifyEmail,
/// Send verify email
VerifyEmailRequest,
/// Accept user invitation
AcceptInvitation,
}

///
Expand Down
Loading