Bug: feat: get specific user role details api



Currently frontend is using the user list api to get the details of the users in a particular merchant account.

If somebody is in a user details page and refreshes, FE will again call list api to get the details as there is no api to get the details of a specific user.
diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index ec3e2dce9b2..4ef04981572 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -11,11 +11,11 @@ use crate::user::{
         GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
     },
     AcceptInviteFromEmailRequest, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest,
-    CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse,
-    InviteUserRequest, InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest,
-    SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
-    SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate,
-    VerifyEmailRequest,
+    CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest,
+    GetUserDetailsRequest, GetUserDetailsResponse, InviteUserRequest, InviteUserResponse,
+    ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest, SendVerifyEmailRequest,
+    SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest,
+    UpdateUserAccountDetailsRequest, UserMerchantCreate, VerifyEmailRequest,
 };
 
 impl ApiEventMetric for DashboardEntryResponse {
@@ -48,7 +48,7 @@ common_utils::impl_misc_api_event_type!(
     SwitchMerchantIdRequest,
     CreateInternalUserRequest,
     UserMerchantCreate,
-    GetUsersResponse,
+    ListUsersResponse,
     AuthorizeResponse,
     ConnectAccountRequest,
     ForgotPasswordRequest,
@@ -60,7 +60,9 @@ common_utils::impl_misc_api_event_type!(
     SendVerifyEmailRequest,
     AcceptInviteFromEmailRequest,
     SignInResponse,
-    UpdateUserAccountDetailsRequest
+    UpdateUserAccountDetailsRequest,
+    GetUserDetailsRequest,
+    GetUserDetailsResponse
 );
 
 #[cfg(feature = "dummy_connector")]
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 8f77f72aad5..0db711f5eca 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -1,3 +1,4 @@
+use common_enums::{PermissionGroup, RoleScope};
 use common_utils::{crypto::OptionalEncryptableName, pii};
 use masking::Secret;
 
@@ -143,7 +144,7 @@ pub struct UserMerchantCreate {
 }
 
 #[derive(Debug, serde::Serialize)]
-pub struct GetUsersResponse(pub Vec<UserDetails>);
+pub struct ListUsersResponse(pub Vec<UserDetails>);
 
 #[derive(Debug, serde::Serialize)]
 pub struct UserDetails {
@@ -156,6 +157,24 @@ pub struct UserDetails {
     pub last_modified_at: time::PrimitiveDateTime,
 }
 
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct GetUserDetailsRequest {
+    pub email: pii::Email,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct GetUserDetailsResponse {
+    pub email: pii::Email,
+    pub name: Secret<String>,
+    pub role_id: String,
+    pub role_name: String,
+    pub status: UserStatus,
+    #[serde(with = "common_utils::custom_serde::iso8601")]
+    pub last_modified_at: time::PrimitiveDateTime,
+    pub groups: Vec<PermissionGroup>,
+    pub role_scope: RoleScope,
+}
+
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
 pub struct VerifyEmailRequest {
     pub token: Secret<String>,
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index b8b8cbd24c7..6516022cfd7 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -1193,10 +1193,53 @@ pub async fn list_merchant_ids_for_user(
     ))
 }
 
-pub async fn get_users_for_merchant_account(
+pub async fn get_user_details_in_merchant_account(
     state: AppState,
     user_from_token: auth::UserFromToken,
-) -> UserResponse<user_api::GetUsersResponse> {
+    request: user_api::GetUserDetailsRequest,
+) -> UserResponse<user_api::GetUserDetailsResponse> {
+    let required_user = utils::user::get_user_from_db_by_email(&state, request.email.try_into()?)
+        .await
+        .to_not_found_response(UserErrors::InvalidRoleOperation)?;
+
+    let required_user_role = state
+        .store
+        .find_user_role_by_user_id_merchant_id(
+            required_user.get_user_id(),
+            &user_from_token.merchant_id,
+        )
+        .await
+        .to_not_found_response(UserErrors::InvalidRoleOperation)
+        .attach_printable("User not found in the merchant account")?;
+
+    let role_info = roles::RoleInfo::from_role_id(
+        &state,
+        &required_user_role.role_id,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .change_context(UserErrors::InternalServerError)
+    .attach_printable("User role exists but the corresponding role doesn't")?;
+
+    Ok(ApplicationResponse::Json(
+        user_api::GetUserDetailsResponse {
+            email: required_user.get_email(),
+            name: required_user.get_name(),
+            role_id: role_info.get_role_id().to_string(),
+            role_name: role_info.get_role_name().to_string(),
+            status: required_user_role.status.foreign_into(),
+            last_modified_at: required_user_role.last_modified,
+            groups: role_info.get_permission_groups().to_vec(),
+            role_scope: role_info.get_scope(),
+        },
+    ))
+}
+
+pub async fn list_users_for_merchant_account(
+    state: AppState,
+    user_from_token: auth::UserFromToken,
+) -> UserResponse<user_api::ListUsersResponse> {
     let users_and_user_roles = state
         .store
         .find_users_and_roles_by_merchant_id(user_from_token.merchant_id.as_str())
@@ -1235,7 +1278,7 @@ pub async fn get_users_for_merchant_account(
         })
         .collect();
 
-    Ok(ApplicationResponse::Json(user_api::GetUsersResponse(
+    Ok(ApplicationResponse::Json(user_api::ListUsersResponse(
         user_details_vec,
     )))
 }
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 73558c78d7b..b37e034d8db 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1115,7 +1115,10 @@ impl User {
         // User management
         route = route.service(
             web::scope("/user")
-                .service(web::resource("/list").route(web::get().to(get_user_details)))
+                .service(web::resource("").route(web::get().to(get_user_role_details)))
+                .service(
+                    web::resource("/list").route(web::get().to(list_users_for_merchant_account)),
+                )
                 .service(web::resource("/invite").route(web::post().to(invite_user)))
                 .service(
                     web::resource("/invite_multiple").route(web::post().to(invite_multiple_user)),
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 9471289a0c8..897b33266b8 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -182,6 +182,7 @@ impl From<Flow> for ApiIdentifier {
             | Flow::DeleteSampleData
             | Flow::UserMerchantAccountList
             | Flow::GetUserDetails
+            | Flow::ListUsersForMerchantAccount
             | Flow::ForgotPassword
             | Flow::ResetPassword
             | Flow::InviteUser
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 0568b31338b..3c10ce38f9f 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -314,14 +314,35 @@ pub async fn list_merchant_ids_for_user(
     .await
 }
 
-pub async fn get_user_details(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
+pub async fn get_user_role_details(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    payload: web::Query<user_api::GetUserDetailsRequest>,
+) -> HttpResponse {
     let flow = Flow::GetUserDetails;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        payload.into_inner(),
+        user_core::get_user_details_in_merchant_account,
+        &auth::JWTAuth(Permission::UsersRead),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn list_users_for_merchant_account(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+) -> HttpResponse {
+    let flow = Flow::ListUsersForMerchantAccount;
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
         (),
-        |state, user, _| user_core::get_users_for_merchant_account(state, user),
+        |state, user, _| user_core::list_users_for_merchant_account(state, user),
         &auth::JWTAuth(Permission::UsersRead),
         api_locking::LockAction::NotApplicable,
     ))
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 62c09c42455..b4adc077af1 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -330,8 +330,10 @@ pub enum Flow {
     DeleteSampleData,
     /// List merchant accounts for user
     UserMerchantAccountList,
-    /// Get users for merchant account
+    /// Get details of a user in a merchant account
     GetUserDetails,
+    /// List users for merchant account
+    ListUsersForMerchantAccount,
     /// PaymentMethodAuth Link token create
     PmAuthLinkTokenCreate,
     /// PaymentMethodAuth Exchange token create

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
Currently there is no way to get the details of user and their role directly. This API solves it by taking the email of the user and returns the details.

### Additional Changes

- [x] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->

## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
closes [#3987](https://github.com/juspay/hyperswitch/issues/3987)


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
Postman.

```
curl --location 'http://localhost:8080/user/user?email=user@hyperswitch.io' \
--header 'Authorization: Bearer JWT'
```
```
{
    "email": "user email",
    "name": "user name",
    "role_id": "merchant_view_only",
    "role_name": "View Only",
    "status": "Active",
    "last_modified_at": "2024-03-01T06:23:44.250Z",
    "groups": [
        "operations_view",
        "connectors_view",
        "workflows_view",
        "analytics_view",
        "users_view",
        "merchant_details_view"
    ],
    "role_scope": "organization"
}
```

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
