Bug: feat(invite): make accept invite api




diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs
index aa8d13dab6d..c8d8fd96a7a 100644
--- a/crates/api_models/src/events/user_role.rs
+++ b/crates/api_models/src/events/user_role.rs
@@ -1,8 +1,8 @@
 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!(
@@ -10,5 +10,6 @@ common_utils::impl_misc_api_event_type!(
     RoleInfoResponse,
     GetRoleRequest,
     AuthorizationInfoResponse,
-    UpdateUserRoleRequest
+    UpdateUserRoleRequest,
+    AcceptInvitationRequest
 );
diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs
index b057f8ca8bc..d2548935f62 100644
--- a/crates/api_models/src/user_role.rs
+++ b/crates/api_models/src/user_role.rs
@@ -1,3 +1,5 @@
+use crate::user::DashboardEntryResponse;
+
 #[derive(Debug, serde::Serialize)]
 pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
 
@@ -91,3 +93,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;
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 729cef65c20..3384e229009 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -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)?,
     ))
 }
 
@@ -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)?,
     ))
 }
 
@@ -600,7 +598,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())
     };
 
@@ -712,11 +710,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)?,
     ))
 }
 
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index d8ff836e1f8..245f8d246d2 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -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},
@@ -115,3 +116,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)
+}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 3d63df2fe80..4345109a672 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -922,6 +922,7 @@ impl User {
             .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("/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("/data")
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index d3a2e1af9a7..1c967222dc7 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -185,7 +185,8 @@ impl From<Flow> for ApiIdentifier {
             | Flow::GetRole
             | Flow::GetRoleFromToken
             | Flow::UpdateUserRole
-            | Flow::GetAuthorizationInfo => Self::UserRole,
+            | Flow::GetAuthorizationInfo
+            | Flow::AcceptInvitation => Self::UserRole,
 
             Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
                 Self::ConnectorOnboarding
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index fe305942d03..73b1ef1b01d 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -96,3 +96,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::UserWithoutMerchantJWTAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index 3370912394e..eaadc0d5c7b 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -55,6 +55,9 @@ pub enum AuthenticationType {
         merchant_id: String,
         user_id: Option<String>,
     },
+    UserJwt {
+        user_id: String,
+    },
     MerchantId {
         merchant_id: String,
     },
@@ -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,
@@ -276,6 +300,33 @@ pub async fn get_admin_api_key(
         .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)]
 pub struct AdminApiAuth;
 
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index 53c88f8aea1..bbe21f289aa 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -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);
         }
diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs
index a115fa2a2d8..a3f9e7978aa 100644
--- a/crates/router/src/utils/user.rs
+++ b/crates/router/src/utils/user.rs
@@ -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>> {
@@ -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,
@@ -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);
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 7e3a692517f..ba323ebc5e3 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -335,6 +335,8 @@ pub enum Flow {
     VerifyEmailRequest,
     /// Update user account details
     UpdateUserAccountDetails,
+    /// Accept user invitation
+    AcceptInvitation,
 }
 
 ///

## 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 -->
This PR will add a new API for accepting invitation and also a new JWT auth type which has only `user_id`.

### 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).
-->
To support the Accept Invite flow in the following flow.
```mermaid
flowchart TD
    A["Connect Account"] -- Email --> B{"User Exists"}
    B -- No --> C["Create new \n Org \n Merchant \n User \n User Role"]
    C --> D["Send Verify Email"]
    D --> E["Verify Email Token"]
    B -- Yes --> D
    E --> H{"Is there\n any perferred \nmerchant"}
    F -- ==0 --> I{{"Send list of \nmerchants which\n user has access to\n along with an\n intermediate token"}}
    F -- >1 --> G[Select the first role with active status]
    H -- Yes --> M["Send the token \nwith that merchant_id"]
    H -- No --> F{"How many \n active merchants\n does user have \naccess to"}
    I -- merchant_ids, \nintermediate_token --> O["Accept Invite"]
    subgraph Accept Invite
        O --> P["Change status to active"]
        P --> G
        G --> M
    end
    K["Sign In"] -- Email, Password --> H
```

## 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)?
-->
```curl
curl --location 'http://localhost:8080/user/user/invite/accept' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer JWT' \
--data '{
    "merchant_ids": [
         "merchant_id1",
         "merchant_id2",
         "merchant_id3"
    ],
    "need_dashboard_entry_response": true
}'
```
If any of the `merchant_id` status is `active`, then you will be getting the following response.
```
{
    "token": "JWT with merchant_id, user_id, user_role",
    "merchant_id": "merchant_id",
    "name": "user name",
    "email": "user email",
    "verification_days_left": null,
    "user_role": "user role"
}
```
If `need_dashboard_entry_response` is `false` or not sent, then the response will be 200 OK.

## 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
