Bug: feat: force password for user



Support to have a check whether for newly invited users, whether the password has been changed once or not. This will be limited to users with email feature flag disabled. 
diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs
index 11588bbfbaf..66831c7aeb8 100644
--- a/crates/api_models/src/user/dashboard_metadata.rs
+++ b/crates/api_models/src/user/dashboard_metadata.rs
@@ -24,6 +24,8 @@ pub enum SetMetaDataRequest {
     ConfigureWoocom,
     SetupWoocomWebhook,
     IsMultipleConfiguration,
+    #[serde(skip)]
+    IsChangePasswordRequired,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
@@ -110,6 +112,7 @@ pub enum GetMetaDataRequest {
     ConfigureWoocom,
     SetupWoocomWebhook,
     IsMultipleConfiguration,
+    IsChangePasswordRequired,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
@@ -146,4 +149,5 @@ pub enum GetMetaDataResponse {
     ConfigureWoocom(bool),
     SetupWoocomWebhook(bool),
     IsMultipleConfiguration(bool),
+    IsChangePasswordRequired(bool),
 }
diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs
index babffdbc4a8..93a98dc824b 100644
--- a/crates/diesel_models/src/enums.rs
+++ b/crates/diesel_models/src/enums.rs
@@ -511,4 +511,5 @@ pub enum DashboardMetadata {
     ConfigureWoocom,
     SetupWoocomWebhook,
     IsMultipleConfiguration,
+    IsChangePasswordRequired,
 }
diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs
index b1cb034eb1f..d91a4078128 100644
--- a/crates/diesel_models/src/query/dashboard_metadata.rs
+++ b/crates/diesel_models/src/query/dashboard_metadata.rs
@@ -105,7 +105,7 @@ impl DashboardMetadata {
         .await
     }
 
-    pub async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
+    pub async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
         conn: &PgPooledConn,
         user_id: String,
         merchant_id: String,
@@ -118,4 +118,20 @@ impl DashboardMetadata {
         )
         .await
     }
+
+    pub async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
+        conn: &PgPooledConn,
+        user_id: String,
+        merchant_id: String,
+        data_key: enums::DashboardMetadata,
+    ) -> StorageResult<Self> {
+        generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(
+            conn,
+            dsl::user_id
+                .eq(user_id)
+                .and(dsl::merchant_id.eq(merchant_id))
+                .and(dsl::data_key.eq(data_key)),
+        )
+        .await
+    }
 }
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 41a407bd670..58d702cbac1 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -8,8 +8,9 @@ use error_stack::ResultExt;
 use masking::ExposeInterface;
 #[cfg(feature = "email")]
 use router_env::env;
-#[cfg(feature = "email")]
 use router_env::logger;
+#[cfg(not(feature = "email"))]
+use user_api::dashboard_metadata::SetMetaDataRequest;
 
 use super::errors::{UserErrors, UserResponse, UserResult};
 #[cfg(feature = "email")]
@@ -308,6 +309,20 @@ pub async fn change_password(
         .await
         .change_context(UserErrors::InternalServerError)?;
 
+    #[cfg(not(feature = "email"))]
+    {
+        state
+            .store
+            .delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
+                &user_from_token.user_id,
+                &user_from_token.merchant_id,
+                diesel_models::enums::DashboardMetadata::IsChangePasswordRequired,
+            )
+            .await
+            .map_err(|e| logger::error!("Error while deleting dashboard metadata {}", e))
+            .ok();
+    }
+
     Ok(ApplicationResponse::StatusOk)
 }
 
@@ -475,8 +490,8 @@ pub async fn invite_user(
             .insert_user_role(UserRoleNew {
                 user_id: new_user.get_user_id().to_owned(),
                 merchant_id: user_from_token.merchant_id.clone(),
-                role_id: request.role_id,
-                org_id: user_from_token.org_id,
+                role_id: request.role_id.clone(),
+                org_id: user_from_token.org_id.clone(),
                 status: invitation_status,
                 created_by: user_from_token.user_id.clone(),
                 last_modified_by: user_from_token.user_id,
@@ -515,6 +530,20 @@ pub async fn invite_user(
         #[cfg(not(feature = "email"))]
         {
             is_email_sent = false;
+            let invited_user_token = auth::UserFromToken {
+                user_id: new_user.get_user_id(),
+                merchant_id: user_from_token.merchant_id,
+                org_id: user_from_token.org_id,
+                role_id: request.role_id,
+            };
+
+            let set_metadata_request = SetMetaDataRequest::IsChangePasswordRequired;
+            dashboard_metadata::set_metadata(
+                state.clone(),
+                invited_user_token,
+                set_metadata_request,
+            )
+            .await?;
         }
 
         Ok(ApplicationResponse::Json(user_api::InviteUserResponse {
@@ -692,6 +721,17 @@ async fn handle_new_user_invitation(
     #[cfg(not(feature = "email"))]
     {
         is_email_sent = false;
+
+        let invited_user_token = auth::UserFromToken {
+            user_id: new_user.get_user_id(),
+            merchant_id: user_from_token.merchant_id.clone(),
+            org_id: user_from_token.org_id.clone(),
+            role_id: request.role_id.clone(),
+        };
+
+        let set_metadata_request = SetMetaDataRequest::IsChangePasswordRequired;
+        dashboard_metadata::set_metadata(state.clone(), invited_user_token, set_metadata_request)
+            .await?;
     }
 
     Ok(InviteMultipleUserResponse {
diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs
index 24ff292870e..82f95564768 100644
--- a/crates/router/src/core/user/dashboard_metadata.rs
+++ b/crates/router/src/core/user/dashboard_metadata.rs
@@ -105,6 +105,9 @@ fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult<types::Me
         api::SetMetaDataRequest::IsMultipleConfiguration => {
             Ok(types::MetaData::IsMultipleConfiguration(true))
         }
+        api::SetMetaDataRequest::IsChangePasswordRequired => {
+            Ok(types::MetaData::IsChangePasswordRequired(true))
+        }
     }
 }
 
@@ -131,6 +134,7 @@ fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum {
         api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom,
         api::GetMetaDataRequest::SetupWoocomWebhook => DBEnum::SetupWoocomWebhook,
         api::GetMetaDataRequest::IsMultipleConfiguration => DBEnum::IsMultipleConfiguration,
+        api::GetMetaDataRequest::IsChangePasswordRequired => DBEnum::IsChangePasswordRequired,
     }
 }
 
@@ -207,6 +211,9 @@ fn into_response(
         DBEnum::IsMultipleConfiguration => Ok(api::GetMetaDataResponse::IsMultipleConfiguration(
             data.is_some(),
         )),
+        DBEnum::IsChangePasswordRequired => Ok(api::GetMetaDataResponse::IsChangePasswordRequired(
+            data.is_some(),
+        )),
     }
 }
 
@@ -520,6 +527,17 @@ async fn insert_metadata(
             )
             .await
         }
+        types::MetaData::IsChangePasswordRequired(data) => {
+            utils::insert_user_scoped_metadata_to_db(
+                state,
+                user.user_id,
+                user.merchant_id,
+                user.org_id,
+                metadata_key,
+                data,
+            )
+            .await
+        }
     }
 }
 
diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs
index 8e2ac0b6ad3..49dd43313c4 100644
--- a/crates/router/src/db/dashboard_metadata.rs
+++ b/crates/router/src/db/dashboard_metadata.rs
@@ -14,6 +14,7 @@ pub trait DashboardMetadataInterface {
         &self,
         metadata: storage::DashboardMetadataNew,
     ) -> CustomResult<storage::DashboardMetadata, errors::StorageError>;
+
     async fn update_metadata(
         &self,
         user_id: Option<String>,
@@ -30,6 +31,7 @@ pub trait DashboardMetadataInterface {
         org_id: &str,
         data_keys: Vec<enums::DashboardMetadata>,
     ) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
+
     async fn find_merchant_scoped_dashboard_metadata(
         &self,
         merchant_id: &str,
@@ -37,11 +39,18 @@ pub trait DashboardMetadataInterface {
         data_keys: Vec<enums::DashboardMetadata>,
     ) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
 
-    async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
+    async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
         &self,
         user_id: &str,
         merchant_id: &str,
     ) -> CustomResult<bool, errors::StorageError>;
+
+    async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+        data_key: enums::DashboardMetadata,
+    ) -> CustomResult<storage::DashboardMetadata, errors::StorageError>;
 }
 
 #[async_trait::async_trait]
@@ -117,16 +126,34 @@ impl DashboardMetadataInterface for Store {
         .map_err(Into::into)
         .into_report()
     }
-    async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
+    async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
         &self,
         user_id: &str,
         merchant_id: &str,
     ) -> CustomResult<bool, errors::StorageError> {
         let conn = connection::pg_connection_write(self).await?;
-        storage::DashboardMetadata::delete_user_scoped_dashboard_metadata_by_merchant_id(
+        storage::DashboardMetadata::delete_all_user_scoped_dashboard_metadata_by_merchant_id(
+            &conn,
+            user_id.to_owned(),
+            merchant_id.to_owned(),
+        )
+        .await
+        .map_err(Into::into)
+        .into_report()
+    }
+
+    async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+        data_key: enums::DashboardMetadata,
+    ) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::DashboardMetadata::delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
             &conn,
             user_id.to_owned(),
             merchant_id.to_owned(),
+            data_key,
         )
         .await
         .map_err(Into::into)
@@ -267,7 +294,7 @@ impl DashboardMetadataInterface for MockDb {
         }
         Ok(query_result)
     }
-    async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
+    async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
         &self,
         user_id: &str,
         merchant_id: &str,
@@ -294,4 +321,31 @@ impl DashboardMetadataInterface for MockDb {
 
         Ok(true)
     }
+
+    async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+        data_key: enums::DashboardMetadata,
+    ) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
+        let mut dashboard_metadata = self.dashboard_metadata.lock().await;
+
+        let index_to_remove = dashboard_metadata
+            .iter()
+            .position(|metadata_inner| {
+                metadata_inner
+                    .user_id
+                    .as_deref()
+                    .map_or(false, |user_id_inner| user_id_inner == user_id)
+                    && metadata_inner.merchant_id == merchant_id
+                    && metadata_inner.data_key == data_key
+            })
+            .ok_or(errors::StorageError::ValueNotFound(
+                "No data found".to_string(),
+            ))?;
+
+        let deleted_value = dashboard_metadata.swap_remove(index_to_remove);
+
+        Ok(deleted_value)
+    }
 }
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 0a9030bae29..029b1a57764 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -1964,6 +1964,7 @@ impl UserRoleInterface for KafkaStore {
             .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update)
             .await
     }
+
     async fn delete_user_role_by_user_id_merchant_id(
         &self,
         user_id: &str,
@@ -2021,6 +2022,7 @@ impl DashboardMetadataInterface for KafkaStore {
             .find_user_scoped_dashboard_metadata(user_id, merchant_id, org_id, data_keys)
             .await
     }
+
     async fn find_merchant_scoped_dashboard_metadata(
         &self,
         merchant_id: &str,
@@ -2032,13 +2034,28 @@ impl DashboardMetadataInterface for KafkaStore {
             .await
     }
 
-    async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
+    async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
         &self,
         user_id: &str,
         merchant_id: &str,
     ) -> CustomResult<bool, errors::StorageError> {
         self.diesel_store
-            .delete_user_scoped_dashboard_metadata_by_merchant_id(user_id, merchant_id)
+            .delete_all_user_scoped_dashboard_metadata_by_merchant_id(user_id, merchant_id)
+            .await
+    }
+
+    async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
+        &self,
+        user_id: &str,
+        merchant_id: &str,
+        data_key: enums::DashboardMetadata,
+    ) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
+        self.diesel_store
+            .delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
+                user_id,
+                merchant_id,
+                data_key,
+            )
             .await
     }
 }
diff --git a/crates/router/src/types/domain/user/dashboard_metadata.rs b/crates/router/src/types/domain/user/dashboard_metadata.rs
index 5e4017a3cb1..9c00809238e 100644
--- a/crates/router/src/types/domain/user/dashboard_metadata.rs
+++ b/crates/router/src/types/domain/user/dashboard_metadata.rs
@@ -25,6 +25,7 @@ pub enum MetaData {
     ConfigureWoocom(bool),
     SetupWoocomWebhook(bool),
     IsMultipleConfiguration(bool),
+    IsChangePasswordRequired(bool),
 }
 
 impl From<&MetaData> for DBEnum {
@@ -51,6 +52,7 @@ impl From<&MetaData> for DBEnum {
             MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom,
             MetaData::SetupWoocomWebhook(_) => Self::SetupWoocomWebhook,
             MetaData::IsMultipleConfiguration(_) => Self::IsMultipleConfiguration,
+            MetaData::IsChangePasswordRequired(_) => Self::IsChangePasswordRequired,
         }
     }
 }
diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs
index bcf270010ea..ac3d918a34f 100644
--- a/crates/router/src/utils/user/dashboard_metadata.rs
+++ b/crates/router/src/utils/user/dashboard_metadata.rs
@@ -219,7 +219,9 @@ pub fn separate_metadata_type_based_on_scope(
             | DBEnum::ConfigureWoocom
             | DBEnum::SetupWoocomWebhook
             | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key),
-            DBEnum::Feedback | DBEnum::ProdIntent => user_scoped.push(key),
+            DBEnum::Feedback | DBEnum::ProdIntent | DBEnum::IsChangePasswordRequired => {
+                user_scoped.push(key)
+            }
         }
     }
     (merchant_scoped, user_scoped)

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

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

## Description
For newly invited users, add support to have check for whether password has been changed once or not.
- Use enum `IsChangePasswordRequired` to have this check.
### Additional Changes

- [ ] 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
For users with email feature flag disabled, there is no support to know whether the newly invited user has change password or not.

## How did you test it?
For Test 
Invite user
```
curl --location 'http://localhost:8080/user/user/invite' \
--header 'Authorization: Bearer JWT' \
--header 'Content-Type: application/json' \
--data-raw '{
  "email": "test_force@gmail.com",
    "name": "test3",
    "role_id": "merchant_admin"
}
'
```
Invited user signin
```
curl --location 'http://localhost:8080/user/signin' \
--header 'Content-Type: application/json' \
--header 'Cookie: token=JWT' \
--data-raw '{
    
    
    "email": "test_force@gmail.com",
    "password": "926fc057-df2e-42af-aada-5e403487f3aa"

}'
```
Check value of Enum `IsChangePasswordRequired`
```
curl --location --request GET 'http://localhost:8080/user/data?keys=IsChangePasswordRequired' \
--header 'Authorization: Bearer JWT' \
--header 'Content-Type: application/json' \
--data '
'
```
Response:
```
[
    {
        "IsChangePasswordRequired": true
    }
]
```
Change Password:
```
curl --location 'http://localhost:8080/user/change_password' \
--header 'Content-Type: application/json' \
--header 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWZjNzgyOTUtM2JiMy00OGJjLThlZDgtNzFjMzVjMzMxYWU2IiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNjk3NzE2ODcwIiwicm9sZV9pZCI6Im1lcmNoYW50X2FkbWluIiwiZXhwIjoxNjk3ODg5NjcxLCJvcmdfaWQiOiJvcmdfejQ5ZExMeTdmbllUODN5TDY3clEifQ.CJzEQ2qbhn-qUiiVBSdvCJiLvWp-5wCF9R54gth6QbQ' \
--header 'Authorization: Bearer JWT' \
--data '{
    "old_password": "926fc057-df2e-42af-aada-5e403487f3aa",
    "new_password": "test"
}
'
```
Again checking the value of enum after login:
```
curl --location --request GET 'http://localhost:8080/user/data?keys=IsChangePasswordRequired' \
--header 'Authorization: Bearer JWT' \
--header 'Content-Type: application/json' \
--data '
'
```
Response should be:
```
[
    {
        "IsChangePasswordRequired": false
    }
]
```

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