Bug: feat(users): Force users to reset password on first login for non-email flow



Currently in non-email flow, users who are invited are not forced to change their password.

This is being forced in email flow, this should also be present in non-email flow as well.
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index 8331dff95c1..c48fe3320f3 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -1,4 +1,8 @@
-use std::{collections::HashSet, ops, str::FromStr};
+use std::{
+    collections::HashSet,
+    ops::{Deref, Not},
+    str::FromStr,
+};
 
 use api_models::{
     admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api,
@@ -153,7 +157,7 @@ impl TryFrom<pii::Email> for UserEmail {
     }
 }
 
-impl ops::Deref for UserEmail {
+impl Deref for UserEmail {
     type Target = Secret<String, pii::EmailStrategy>;
 
     fn deref(&self) -> &Self::Target {
@@ -565,10 +569,24 @@ pub struct NewUser {
     user_id: String,
     name: UserName,
     email: UserEmail,
-    password: Option<UserPassword>,
+    password: Option<NewUserPassword>,
     new_merchant: NewUserMerchant,
 }
 
+#[derive(Clone)]
+pub struct NewUserPassword {
+    password: UserPassword,
+    is_temporary: bool,
+}
+
+impl Deref for NewUserPassword {
+    type Target = UserPassword;
+
+    fn deref(&self) -> &Self::Target {
+        &self.password
+    }
+}
+
 impl NewUser {
     pub fn get_user_id(&self) -> String {
         self.user_id.clone()
@@ -587,7 +605,9 @@ impl NewUser {
     }
 
     pub fn get_password(&self) -> Option<UserPassword> {
-        self.password.clone()
+        self.password
+            .as_ref()
+            .map(|password| password.deref().clone())
     }
 
     pub async fn insert_user_in_db(
@@ -697,7 +717,9 @@ impl TryFrom<NewUser> for storage_user::UserNew {
             totp_status: TotpStatus::NotSet,
             totp_secret: None,
             totp_recovery_codes: None,
-            last_password_modified_at: value.password.is_some().then_some(now),
+            last_password_modified_at: value
+                .password
+                .and_then(|password_inner| password_inner.is_temporary.not().then_some(now)),
         })
     }
 }
@@ -708,7 +730,10 @@ impl TryFrom<user_api::SignUpWithMerchantIdRequest> for NewUser {
     fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult<Self> {
         let email = value.email.clone().try_into()?;
         let name = UserName::new(value.name.clone())?;
-        let password = UserPassword::new(value.password.clone())?;
+        let password = NewUserPassword {
+            password: UserPassword::new(value.password.clone())?,
+            is_temporary: false,
+        };
         let user_id = uuid::Uuid::new_v4().to_string();
         let new_merchant = NewUserMerchant::try_from(value)?;
 
@@ -729,7 +754,10 @@ impl TryFrom<user_api::SignUpRequest> for NewUser {
         let user_id = uuid::Uuid::new_v4().to_string();
         let email = value.email.clone().try_into()?;
         let name = UserName::try_from(value.email.clone())?;
-        let password = UserPassword::new(value.password.clone())?;
+        let password = NewUserPassword {
+            password: UserPassword::new(value.password.clone())?,
+            is_temporary: false,
+        };
         let new_merchant = NewUserMerchant::try_from(value)?;
 
         Ok(Self {
@@ -770,7 +798,10 @@ impl TryFrom<(user_api::CreateInternalUserRequest, id_type::OrganizationId)> for
         let user_id = uuid::Uuid::new_v4().to_string();
         let email = value.email.clone().try_into()?;
         let name = UserName::new(value.name.clone())?;
-        let password = UserPassword::new(value.password.clone())?;
+        let password = NewUserPassword {
+            password: UserPassword::new(value.password.clone())?,
+            is_temporary: false,
+        };
         let new_merchant = NewUserMerchant::try_from((value, org_id))?;
 
         Ok(Self {
@@ -789,16 +820,21 @@ impl TryFrom<UserMerchantCreateRequestWithToken> for NewUser {
     fn try_from(value: UserMerchantCreateRequestWithToken) -> Result<Self, Self::Error> {
         let user = value.0.clone();
         let new_merchant = NewUserMerchant::try_from(value)?;
+        let password = user
+            .0
+            .password
+            .map(UserPassword::new_password_without_validation)
+            .transpose()?
+            .map(|password| NewUserPassword {
+                password,
+                is_temporary: false,
+            });
 
         Ok(Self {
             user_id: user.0.user_id,
             name: UserName::new(user.0.name)?,
             email: user.0.email.clone().try_into()?,
-            password: user
-                .0
-                .password
-                .map(UserPassword::new_password_without_validation)
-                .transpose()?,
+            password,
             new_merchant,
         })
     }
@@ -810,8 +846,10 @@ impl TryFrom<InviteeUserRequestWithInvitedUserToken> for NewUser {
         let user_id = uuid::Uuid::new_v4().to_string();
         let email = value.0.email.clone().try_into()?;
         let name = UserName::new(value.0.name.clone())?;
-        let password = cfg!(not(feature = "email"))
-            .then_some(UserPassword::new(password::get_temp_password())?);
+        let password = cfg!(not(feature = "email")).then_some(NewUserPassword {
+            password: UserPassword::new(password::get_temp_password())?,
+            is_temporary: true,
+        });
         let new_merchant = NewUserMerchant::try_from(value)?;
 
         Ok(Self {

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

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

## Description
<!-- Describe your changes in detail -->
Currently in non-email flow, users who are invited are not forced to change their password. This PR will add that.

### 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
<!--
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 [#6482](https://github.com/juspay/hyperswitch/issues/6482).

## 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)?
-->
1. Invite a new user in non-email flow
    ```
    curl --location 'http://localhost:8080/user/user/invite_multiple?token_only=true' \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiN2I2NTA1NGQtNjgzNC00NzU2LTgyNDYtN2RkOWM1ZmQzMzRmIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzI5MTc4MDgzIiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTczMDk4MzEyOSwib3JnX2lkIjoib3JnXzRiYWFjbHZxUklJTEl2a25NdnZQIiwicHJvZmlsZV9pZCI6InByb18yOUFWY0ltVnFDYTV1UHdMMzlQWCJ9.E5ZfkG5y0tqGa_vzGXe5LTJlyljqP8tKs1T2qokRtr0' \
    --data-raw '[
        {
            "email": "new email",
            "name": "name",
            "role_id": "merchant_view_only"
        }
    ]'
    ```

    ```json
    [
        {
            "email": "email from request",
            "is_email_sent": false,
            "password": "4724b955-bc1d-4565-bbb6-1a09b2372bfaA"
        }
    ]
    ```
2. Sign in as the new user
    ```
    curl --location 'http://localhost:8080/user/v2/signin' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "email": "email used in invite",
        "password": "password from invite API response"
    }'
    ```
    
    ```json
    {
        "token": "TOTP SPT",
        "token_type": "totp"
    }
    ```

3. Terminate 2FA (This API should give reset password SPT)
    ```
    curl --location 'http://localhost:8080/user/2fa/terminate?skip_two_factor_auth=true' \
    --header 'Authorization: TOTP SPT' \
    ```


    ```json
    {
        "token": "Force set password SPT",
        "token_type": "force_set_password"
    }
    ```

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