Skip to content
New issue

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

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

Already on GitHub? # 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 2 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
);
9 changes: 9 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,10 @@ pub enum UserStatus {
Active,
InvitationSent,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct AcceptInvitationRequest {
pub merchant_ids: Vec<String>,
}

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 #(
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
45 changes: 44 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,45 @@ 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)?;

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?;

Ok(ApplicationResponse::Json(
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
))
}
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
20 changes: 13 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,13 @@ pub fn get_dashboard_entry_response(
user_role: user_role.role_id,
})
}

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