Bug: feat(email): Send `auth_id` in email URLs



Currently links in the emails point to public dashboard. For SSO tenants/orgs, they should point to different url, which has `auth_id`.
diff --git a/config/config.example.toml b/config/config.example.toml
index f29876e648b..10f8ba7a5e7 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -346,7 +346,6 @@ wildcard_origin = false                 # If true, allows any origin to make req
 [email]
 sender_email = "example@example.com" # Sender email
 aws_region = ""                      # AWS region used by AWS SES
-base_url = ""                        # Base url used when adding links that should redirect to self
 allowed_unverified_days = 1          # Number of days the api calls ( with jwt token ) can be made without verifying the email
 active_email_client = "SES"          # The currently active email client
 
@@ -359,6 +358,7 @@ sts_role_session_name = "" # An identifier for the assumed role session, used to
 password_validity_in_days = 90       # Number of days after which password should be updated
 two_factor_auth_expiry_in_secs = 300 # Number of seconds after which 2FA should be done again if doing update/change from inside
 totp_issuer_name = "Hyperswitch"     # Name of the issuer for TOTP
+base_url = ""                        # Base url used for user specific redirects and emails
 
 #tokenization configuration which describe token lifetime and payment method for specific connector
 [tokenization]
diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml
index 652b4950316..68df3d28e24 100644
--- a/config/deployments/env_specific.toml
+++ b/config/deployments/env_specific.toml
@@ -58,7 +58,6 @@ wildcard_origin = false                 # If true, allows any origin to make req
 [email]
 sender_email = "example@example.com" # Sender email
 aws_region = ""                      # AWS region used by AWS SES
-base_url = ""                        # Dashboard base url used when adding links that should redirect to self, say https://app.hyperswitch.io for example
 allowed_unverified_days = 1          # Number of days the api calls ( with jwt token ) can be made without verifying the email
 active_email_client = "SES"          # The currently active email client
 
diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml
index f5f69568da6..b75062c9340 100644
--- a/config/deployments/integration_test.toml
+++ b/config/deployments/integration_test.toml
@@ -118,6 +118,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
 password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Integ"
+base_url = "https://integ.hyperswitch.io"
 
 [frm]
 enabled = true
diff --git a/config/deployments/production.toml b/config/deployments/production.toml
index bbaf22067b5..98008212fc0 100644
--- a/config/deployments/production.toml
+++ b/config/deployments/production.toml
@@ -125,6 +125,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
 password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Production"
+base_url = "https://live.hyperswitch.io"
 
 [frm]
 enabled = false
diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml
index de274ad9411..040986f7948 100644
--- a/config/deployments/sandbox.toml
+++ b/config/deployments/sandbox.toml
@@ -125,6 +125,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
 password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Sandbox"
+base_url = "https://app.hyperswitch.io"
 
 [frm]
 enabled = true
diff --git a/config/development.toml b/config/development.toml
index 4e230cd2025..ecece79a696 100644
--- a/config/development.toml
+++ b/config/development.toml
@@ -264,7 +264,6 @@ wildcard_origin = true
 [email]
 sender_email = "example@example.com"
 aws_region = ""
-base_url = "http://localhost:8080"
 allowed_unverified_days = 1
 active_email_client = "SES"
 
@@ -276,6 +275,7 @@ sts_role_session_name = ""
 password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Dev"
+base_url = "http://localhost:8080"
 
 [bank_config.eps]
 stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }
diff --git a/config/docker_compose.toml b/config/docker_compose.toml
index 791f2510824..643893c7cb2 100644
--- a/config/docker_compose.toml
+++ b/config/docker_compose.toml
@@ -56,6 +56,7 @@ recon_admin_api_key = "recon_test_admin"
 password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch"
+base_url = "http://localhost:8080"
 
 [locker]
 host = ""
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 6d567e7dca1..0006afea0d9 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -355,3 +355,8 @@ pub struct AuthMethodDetails {
     pub auth_type: common_enums::UserAuthType,
     pub name: Option<OpenIdProvider>,
 }
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct AuthIdQueryParam {
+    pub auth_id: Option<String>,
+}
diff --git a/crates/external_services/src/email.rs b/crates/external_services/src/email.rs
index 07552cb519f..fc577a012c3 100644
--- a/crates/external_services/src/email.rs
+++ b/crates/external_services/src/email.rs
@@ -126,9 +126,6 @@ pub struct EmailSettings {
     /// The AWS region to send SES requests to.
     pub aws_region: String,
 
-    /// Base-url used when adding links that should redirect to self
-    pub base_url: String,
-
     /// Number of days for verification of the email
     pub allowed_unverified_days: i64,
 
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index 80756391741..0569c4dbed6 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -484,6 +484,7 @@ pub struct UserSettings {
     pub password_validity_in_days: u16,
     pub two_factor_auth_expiry_in_secs: i64,
     pub totp_issuer_name: String,
+    pub base_url: String,
 }
 
 #[derive(Debug, Deserialize, Clone)]
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 505cee0c4ac..0fd583143cc 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -39,6 +39,7 @@ pub mod sample_data;
 pub async fn signup_with_merchant_id(
     state: SessionState,
     request: user_api::SignUpWithMerchantIdRequest,
+    auth_id: Option<String>,
 ) -> UserResponse<user_api::SignUpWithMerchantIdResponse> {
     let new_user = domain::NewUser::try_from(request.clone())?;
     new_user
@@ -64,6 +65,7 @@ pub async fn signup_with_merchant_id(
         user_name: domain::UserName::new(user_from_db.get_name())?,
         settings: state.conf.clone(),
         subject: "Get back to Hyperswitch - Reset Your Password Now",
+        auth_id,
     };
 
     let send_email_result = state
@@ -241,6 +243,7 @@ pub async fn signin_token_only_flow(
 pub async fn connect_account(
     state: SessionState,
     request: user_api::ConnectAccountRequest,
+    auth_id: Option<String>,
 ) -> UserResponse<user_api::ConnectAccountResponse> {
     let find_user = state.global_store.find_user_by_email(&request.email).await;
 
@@ -253,6 +256,7 @@ pub async fn connect_account(
             settings: state.conf.clone(),
             user_name: domain::UserName::new(user_from_db.get_name())?,
             subject: "Unlock Hyperswitch: Use Your Magic Link to Sign In",
+            auth_id,
         };
 
         let send_email_result = state
@@ -303,6 +307,7 @@ pub async fn connect_account(
             recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
             settings: state.conf.clone(),
             subject: "Welcome to the Hyperswitch community!",
+            auth_id,
         };
 
         let send_email_result = state
@@ -401,6 +406,7 @@ pub async fn change_password(
 pub async fn forgot_password(
     state: SessionState,
     request: user_api::ForgotPasswordRequest,
+    auth_id: Option<String>,
 ) -> UserResponse<()> {
     let user_email = domain::UserEmail::from_pii_email(request.email)?;
 
@@ -422,6 +428,7 @@ pub async fn forgot_password(
         settings: state.conf.clone(),
         user_name: domain::UserName::new(user_from_db.get_name())?,
         subject: "Get back to Hyperswitch - Reset Your Password Now",
+        auth_id,
     };
 
     state
@@ -596,6 +603,7 @@ pub async fn invite_multiple_user(
     requests: Vec<user_api::InviteUserRequest>,
     req_state: ReqState,
     is_token_only: Option<bool>,
+    auth_id: Option<String>,
 ) -> UserResponse<Vec<InviteMultipleUserResponse>> {
     if requests.len() > 10 {
         return Err(report!(UserErrors::MaxInvitationsError))
@@ -603,7 +611,15 @@ pub async fn invite_multiple_user(
     }
 
     let responses = futures::future::join_all(requests.iter().map(|request| async {
-        match handle_invitation(&state, &user_from_token, request, &req_state, is_token_only).await
+        match handle_invitation(
+            &state,
+            &user_from_token,
+            request,
+            &req_state,
+            is_token_only,
+            &auth_id,
+        )
+        .await
         {
             Ok(response) => response,
             Err(error) => InviteMultipleUserResponse {
@@ -625,6 +641,7 @@ async fn handle_invitation(
     request: &user_api::InviteUserRequest,
     req_state: &ReqState,
     is_token_only: Option<bool>,
+    auth_id: &Option<String>,
 ) -> UserResult<InviteMultipleUserResponse> {
     let inviter_user = user_from_token.get_user_from_db(state).await?;
 
@@ -656,7 +673,14 @@ async fn handle_invitation(
         .await;
 
     if let Ok(invitee_user) = invitee_user {
-        handle_existing_user_invitation(state, user_from_token, request, invitee_user.into()).await
+        handle_existing_user_invitation(
+            state,
+            user_from_token,
+            request,
+            invitee_user.into(),
+            auth_id,
+        )
+        .await
     } else if invitee_user
         .as_ref()
         .map_err(|e| e.current_context().is_db_not_found())
@@ -669,6 +693,7 @@ async fn handle_invitation(
             request,
             req_state.clone(),
             is_token_only,
+            auth_id,
         )
         .await
     } else {
@@ -676,12 +701,13 @@ async fn handle_invitation(
     }
 }
 
-//TODO: send email
+#[allow(unused_variables)]
 async fn handle_existing_user_invitation(
     state: &SessionState,
     user_from_token: &auth::UserFromToken,
     request: &user_api::InviteUserRequest,
     invitee_user_from_db: domain::UserFromStorage,
+    auth_id: &Option<String>,
 ) -> UserResult<InviteMultipleUserResponse> {
     let now = common_utils::date_time::now();
     state
@@ -722,6 +748,7 @@ async fn handle_existing_user_invitation(
             settings: state.conf.clone(),
             subject: "You have been invited to join Hyperswitch Community!",
             merchant_id: user_from_token.merchant_id.clone(),
+            auth_id: auth_id.clone(),
         };
 
         is_email_sent = state
@@ -748,12 +775,14 @@ async fn handle_existing_user_invitation(
     })
 }
 
+#[allow(unused_variables)]
 async fn handle_new_user_invitation(
     state: &SessionState,
     user_from_token: &auth::UserFromToken,
     request: &user_api::InviteUserRequest,
     req_state: ReqState,
     is_token_only: Option<bool>,
+    auth_id: &Option<String>,
 ) -> UserResult<InviteMultipleUserResponse> {
     let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?;
 
@@ -809,6 +838,7 @@ async fn handle_new_user_invitation(
                 settings: state.conf.clone(),
                 subject: "You have been invited to join Hyperswitch Community!",
                 merchant_id: user_from_token.merchant_id.clone(),
+                auth_id: auth_id.clone(),
             })
         } else {
             Box::new(email_types::InviteUser {
@@ -817,6 +847,7 @@ async fn handle_new_user_invitation(
                 settings: state.conf.clone(),
                 subject: "You have been invited to join Hyperswitch Community!",
                 merchant_id: user_from_token.merchant_id.clone(),
+                auth_id: auth_id.clone(),
             })
         };
         let send_email_result = state
@@ -862,7 +893,7 @@ pub async fn resend_invite(
     state: SessionState,
     user_from_token: auth::UserFromToken,
     request: user_api::ReInviteUserRequest,
-    _req_state: ReqState,
+    auth_id: Option<String>,
 ) -> UserResponse<()> {
     let invitee_email = domain::UserEmail::from_pii_email(request.email)?;
     let user: domain::UserFromStorage = state
@@ -906,6 +937,7 @@ pub async fn resend_invite(
         settings: state.conf.clone(),
         subject: "You have been invited to join Hyperswitch Community!",
         merchant_id: user_from_token.merchant_id,
+        auth_id,
     };
     state
         .email_client
@@ -1518,6 +1550,7 @@ pub async fn verify_email_token_only_flow(
 pub async fn send_verification_mail(
     state: SessionState,
     req: user_api::SendVerifyEmailRequest,
+    auth_id: Option<String>,
 ) -> UserResponse<()> {
     let user_email = domain::UserEmail::try_from(req.email)?;
     let user = state
@@ -1540,6 +1573,7 @@ pub async fn send_verification_mail(
         recipient_email: domain::UserEmail::from_pii_email(user.email)?,
         settings: state.conf.clone(),
         subject: "Welcome to the Hyperswitch community!",
+        auth_id,
     };
 
     state
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index f297e41e4e0..816e7f7e2f8 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -39,15 +39,19 @@ pub async fn user_signup_with_merchant_id(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::SignUpWithMerchantIdRequest>,
+    query: web::Query<user_api::AuthIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::UserSignUpWithMerchantId;
     let req_payload = json_payload.into_inner();
+    let auth_id = query.into_inner().auth_id;
     Box::pin(api::server_wrap(
         flow.clone(),
         state,
         &http_req,
         req_payload.clone(),
-        |state, _, req_body, _| user_core::signup_with_merchant_id(state, req_body),
+        |state, _, req_body, _| {
+            user_core::signup_with_merchant_id(state, req_body, auth_id.clone())
+        },
         &auth::AdminApiAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -113,15 +117,17 @@ pub async fn user_connect_account(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::ConnectAccountRequest>,
+    query: web::Query<user_api::AuthIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::UserConnectAccount;
     let req_payload = json_payload.into_inner();
+    let auth_id = query.into_inner().auth_id;
     Box::pin(api::server_wrap(
         flow.clone(),
         state,
         &http_req,
         req_payload.clone(),
-        |state, _, req_body, _| user_core::connect_account(state, req_body),
+        |state, _, req_body, _| user_core::connect_account(state, req_body, auth_id.clone()),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -382,14 +388,16 @@ pub async fn forgot_password(
     state: web::Data<AppState>,
     req: HttpRequest,
     payload: web::Json<user_api::ForgotPasswordRequest>,
+    query: web::Query<user_api::AuthIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::ForgotPassword;
+    let auth_id = query.into_inner().auth_id;
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
         payload.into_inner(),
-        |state, _, payload, _| user_core::forgot_password(state, payload),
+        |state, _, payload, _| user_core::forgot_password(state, payload, auth_id.clone()),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -431,21 +439,31 @@ pub async fn reset_password(
         .await
     }
 }
+
 pub async fn invite_multiple_user(
     state: web::Data<AppState>,
     req: HttpRequest,
     payload: web::Json<Vec<user_api::InviteUserRequest>>,
-    query: web::Query<user_api::TokenOnlyQueryParam>,
+    token_only_query_param: web::Query<user_api::TokenOnlyQueryParam>,
+    auth_id_query_param: web::Query<user_api::AuthIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::InviteMultipleUser;
-    let is_token_only = query.into_inner().token_only;
+    let is_token_only = token_only_query_param.into_inner().token_only;
+    let auth_id = auth_id_query_param.into_inner().auth_id;
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
         payload.into_inner(),
         |state, user, payload, req_state| {
-            user_core::invite_multiple_user(state, user, payload, req_state, is_token_only)
+            user_core::invite_multiple_user(
+                state,
+                user,
+                payload,
+                req_state,
+                is_token_only,
+                auth_id.clone(),
+            )
         },
         &auth::JWTAuth(Permission::UsersWrite),
         api_locking::LockAction::NotApplicable,
@@ -458,14 +476,18 @@ pub async fn resend_invite(
     state: web::Data<AppState>,
     req: HttpRequest,
     payload: web::Json<user_api::ReInviteUserRequest>,
+    query: web::Query<user_api::AuthIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::ReInviteUser;
+    let auth_id = query.into_inner().auth_id;
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
         payload.into_inner(),
-        user_core::resend_invite,
+        |state, user, req_payload, _| {
+            user_core::resend_invite(state, user, req_payload, auth_id.clone())
+        },
         &auth::JWTAuth(Permission::UsersWrite),
         api_locking::LockAction::NotApplicable,
     ))
@@ -551,14 +573,16 @@ pub async fn verify_email_request(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::SendVerifyEmailRequest>,
+    query: web::Query<user_api::AuthIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::VerifyEmailRequest;
+    let auth_id = query.into_inner().auth_id;
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &http_req,
         json_payload.into_inner(),
-        |state, _, req_body, _| user_core::send_verification_mail(state, req_body),
+        |state, _, req_body, _| user_core::send_verification_mail(state, req_body, auth_id.clone()),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index 878cd87ea23..1d4e77ff68a 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -190,14 +190,21 @@ pub fn get_link_with_token(
     base_url: impl std::fmt::Display,
     token: impl std::fmt::Display,
     action: impl std::fmt::Display,
+    auth_id: &Option<impl std::fmt::Display>,
 ) -> String {
-    format!("{base_url}/user/{action}?token={token}")
+    let email_url = format!("{base_url}/user/{action}?token={token}");
+    if let Some(auth_id) = auth_id {
+        format!("{email_url}&auth_id={auth_id}")
+    } else {
+        email_url
+    }
 }
 
 pub struct VerifyEmail {
     pub recipient_email: domain::UserEmail,
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
+    pub auth_id: Option<String>,
 }
 
 /// Currently only HTML is supported
@@ -213,8 +220,12 @@ impl EmailData for VerifyEmail {
         .await
         .change_context(EmailError::TokenGenerationFailure)?;
 
-        let verify_email_link =
-            get_link_with_token(&self.settings.email.base_url, token, "verify_email");
+        let verify_email_link = get_link_with_token(
+            &self.settings.user.base_url,
+            token,
+            "verify_email",
+            &self.auth_id,
+        );
 
         let body = html::get_html_body(EmailBody::Verify {
             link: verify_email_link,
@@ -233,6 +244,7 @@ pub struct ResetPassword {
     pub user_name: domain::UserName,
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
+    pub auth_id: Option<String>,
 }
 
 #[async_trait::async_trait]
@@ -247,8 +259,12 @@ impl EmailData for ResetPassword {
         .await
         .change_context(EmailError::TokenGenerationFailure)?;
 
-        let reset_password_link =
-            get_link_with_token(&self.settings.email.base_url, token, "set_password");
+        let reset_password_link = get_link_with_token(
+            &self.settings.user.base_url,
+            token,
+            "set_password",
+            &self.auth_id,
+        );
 
         let body = html::get_html_body(EmailBody::Reset {
             link: reset_password_link,
@@ -268,6 +284,7 @@ pub struct MagicLink {
     pub user_name: domain::UserName,
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
+    pub auth_id: Option<String>,
 }
 
 #[async_trait::async_trait]
@@ -282,8 +299,12 @@ impl EmailData for MagicLink {
         .await
         .change_context(EmailError::TokenGenerationFailure)?;
 
-        let magic_link_login =
-            get_link_with_token(&self.settings.email.base_url, token, "verify_email");
+        let magic_link_login = get_link_with_token(
+            &self.settings.user.base_url,
+            token,
+            "verify_email",
+            &self.auth_id,
+        );
 
         let body = html::get_html_body(EmailBody::MagicLink {
             link: magic_link_login,
@@ -305,6 +326,7 @@ pub struct InviteUser {
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
     pub merchant_id: String,
+    pub auth_id: Option<String>,
 }
 
 #[async_trait::async_trait]
@@ -319,8 +341,12 @@ impl EmailData for InviteUser {
         .await
         .change_context(EmailError::TokenGenerationFailure)?;
 
-        let invite_user_link =
-            get_link_with_token(&self.settings.email.base_url, token, "set_password");
+        let invite_user_link = get_link_with_token(
+            &self.settings.user.base_url,
+            token,
+            "set_password",
+            &self.auth_id,
+        );
 
         let body = html::get_html_body(EmailBody::InviteUser {
             link: invite_user_link,
@@ -341,6 +367,7 @@ pub struct InviteRegisteredUser {
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
     pub merchant_id: String,
+    pub auth_id: Option<String>,
 }
 
 #[async_trait::async_trait]
@@ -356,9 +383,10 @@ impl EmailData for InviteRegisteredUser {
         .change_context(EmailError::TokenGenerationFailure)?;
 
         let invite_user_link = get_link_with_token(
-            &self.settings.email.base_url,
+            &self.settings.user.base_url,
             token,
             "accept_invite_from_email",
+            &self.auth_id,
         );
         let body = html::get_html_body(EmailBody::AcceptInviteFromEmail {
             link: invite_user_link,

## 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 introduce `auth_id` field in email types and also email APIs.

When an email is triggered from a API which gets `auth_id`, then that will be used to construct the email URL as well.

### Additional Changes

- [x] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [x] 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 #5119.

## 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)?
-->
All the following APIs will accept a query param called `auth_id`.
1. /user/connect_account
2. /user/forgot_password
3. /user/user/invite_multiple
4. /user/resend_invite
5. /user/verify_email_request

Example:
```
curl --location 'http://localhost:8080/user/connect_account?auth_id=2d784c51-c6ba-4adf-8d9f-407049ddacd2' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "email"
}'
```


In the email received from the API, the url in the email will also have the same `auth_id` query param which is the same value as above.

`auth_id` param is optional, and if it is not sent, the email link also will not have any parameter.
## 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
