Bug: feat(core): add api for hypersense integration



# Hypersense
### Description:
#### An AI Ops tool build for Payments Cost Observability
- Monitor your overall transaction payment-related costs across multiple regions, PSPs, and businesses. 
- Obtain actionable insights to optimise costs, track cost deviations, and effectively audit PSP invoices.

Learn more: [Hypersense site](https://www.hypersense.io/)
## Purpose of Change
To integrate hypersense with hyperswitch dashboard.


## Proposed Changes
- add endpoints for, get `token`, `verify_token` and `signout` for `hypersense` integration.
- limit the scope of the use of token by introducing a new type of token, `ExternalToken` 
containing enum `ExternalServiceType`

## Additional Changes
#### `external_service_auth` module and `ExternalToken`
- `external_service_auth` is a generic service used to issue, verify and invalidate `ExternalToken`,
which can be associated with an external service using the enum field `external_service_type: ExternalServiceType` in the `ExternalToken`.
- Essentially a new service can be integrated by simply creating routes and handlers, and calling the same core functions 
specified in `external_service_auth` modules.

diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs
index 2124ff1aff9..9a2a0ca8396 100644
--- a/crates/api_models/src/events.rs
+++ b/crates/api_models/src/events.rs
@@ -2,6 +2,7 @@ pub mod apple_pay_certificates_migration;
 pub mod connector_onboarding;
 pub mod customer;
 pub mod dispute;
+pub mod external_service_auth;
 pub mod gsm;
 mod locker_migration;
 pub mod payment;
diff --git a/crates/api_models/src/events/external_service_auth.rs b/crates/api_models/src/events/external_service_auth.rs
new file mode 100644
index 00000000000..31196150b2c
--- /dev/null
+++ b/crates/api_models/src/events/external_service_auth.rs
@@ -0,0 +1,30 @@
+use common_utils::events::{ApiEventMetric, ApiEventsType};
+
+use crate::external_service_auth::{
+    ExternalSignoutTokenRequest, ExternalTokenResponse, ExternalVerifyTokenRequest,
+    ExternalVerifyTokenResponse,
+};
+
+impl ApiEventMetric for ExternalTokenResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::ExternalServiceAuth)
+    }
+}
+
+impl ApiEventMetric for ExternalVerifyTokenRequest {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::ExternalServiceAuth)
+    }
+}
+
+impl ApiEventMetric for ExternalVerifyTokenResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::ExternalServiceAuth)
+    }
+}
+
+impl ApiEventMetric for ExternalSignoutTokenRequest {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::ExternalServiceAuth)
+    }
+}
diff --git a/crates/api_models/src/external_service_auth.rs b/crates/api_models/src/external_service_auth.rs
new file mode 100644
index 00000000000..775529c4359
--- /dev/null
+++ b/crates/api_models/src/external_service_auth.rs
@@ -0,0 +1,35 @@
+use common_utils::{id_type, pii};
+use masking::Secret;
+
+#[derive(Debug, serde::Serialize)]
+pub struct ExternalTokenResponse {
+    pub token: Secret<String>,
+}
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+pub struct ExternalVerifyTokenRequest {
+    pub token: Secret<String>,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+pub struct ExternalSignoutTokenRequest {
+    pub token: Secret<String>,
+}
+
+#[derive(serde::Serialize, Debug)]
+#[serde(untagged)]
+pub enum ExternalVerifyTokenResponse {
+    Hypersense {
+        user_id: String,
+        merchant_id: id_type::MerchantId,
+        name: Secret<String>,
+        email: pii::Email,
+    },
+}
+
+impl ExternalVerifyTokenResponse {
+    pub fn get_user_id(&self) -> &str {
+        match self {
+            Self::Hypersense { user_id, .. } => user_id,
+        }
+    }
+}
diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs
index 60d1d11f27d..03df84a9d74 100644
--- a/crates/api_models/src/lib.rs
+++ b/crates/api_models/src/lib.rs
@@ -16,6 +16,7 @@ pub mod ephemeral_key;
 #[cfg(feature = "errors")]
 pub mod errors;
 pub mod events;
+pub mod external_service_auth;
 pub mod feature_matrix;
 pub mod files;
 pub mod gsm;
diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs
index 2e90d646e9e..556090858fb 100644
--- a/crates/common_utils/src/events.rs
+++ b/crates/common_utils/src/events.rs
@@ -99,6 +99,7 @@ pub enum ApiEventsType {
     ApplePayCertificatesMigration,
     FraudCheck,
     Recon,
+    ExternalServiceAuth,
     Dispute {
         dispute_id: String,
     },
diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs
index c22cecc20f9..18b3ad14358 100644
--- a/crates/router/src/core.rs
+++ b/crates/router/src/core.rs
@@ -18,6 +18,7 @@ pub mod customers;
 pub mod disputes;
 pub mod encryption;
 pub mod errors;
+pub mod external_service_auth;
 pub mod files;
 #[cfg(feature = "frm")]
 pub mod fraud_check;
diff --git a/crates/router/src/core/external_service_auth.rs b/crates/router/src/core/external_service_auth.rs
new file mode 100644
index 00000000000..92c8f5b249d
--- /dev/null
+++ b/crates/router/src/core/external_service_auth.rs
@@ -0,0 +1,94 @@
+use api_models::external_service_auth as external_service_auth_api;
+use common_utils::fp_utils;
+use error_stack::ResultExt;
+use masking::ExposeInterface;
+
+use crate::{
+    core::errors::{self, RouterResponse},
+    services::{
+        api as service_api,
+        authentication::{self, ExternalServiceType, ExternalToken},
+    },
+    SessionState,
+};
+
+pub async fn generate_external_token(
+    state: SessionState,
+    user: authentication::UserFromToken,
+    external_service_type: ExternalServiceType,
+) -> RouterResponse<external_service_auth_api::ExternalTokenResponse> {
+    let token = ExternalToken::new_token(
+        user.user_id.clone(),
+        user.merchant_id.clone(),
+        &state.conf,
+        external_service_type.clone(),
+    )
+    .await
+    .change_context(errors::ApiErrorResponse::InternalServerError)
+    .attach_printable_lazy(|| {
+        format!(
+            "Failed to create external token for params [user_id, mid, external_service_type] [{}, {:?}, {:?}]",
+            user.user_id, user.merchant_id, external_service_type,
+        )
+    })?;
+
+    Ok(service_api::ApplicationResponse::Json(
+        external_service_auth_api::ExternalTokenResponse {
+            token: token.into(),
+        },
+    ))
+}
+
+pub async fn signout_external_token(
+    state: SessionState,
+    json_payload: external_service_auth_api::ExternalSignoutTokenRequest,
+) -> RouterResponse<()> {
+    let token = authentication::decode_jwt::<ExternalToken>(&json_payload.token.expose(), &state)
+        .await
+        .change_context(errors::ApiErrorResponse::Unauthorized)?;
+
+    authentication::blacklist::insert_user_in_blacklist(&state, &token.user_id)
+        .await
+        .change_context(errors::ApiErrorResponse::InvalidJwtToken)?;
+
+    Ok(service_api::ApplicationResponse::StatusOk)
+}
+
+pub async fn verify_external_token(
+    state: SessionState,
+    json_payload: external_service_auth_api::ExternalVerifyTokenRequest,
+    external_service_type: ExternalServiceType,
+) -> RouterResponse<external_service_auth_api::ExternalVerifyTokenResponse> {
+    let token_from_payload = json_payload.token.expose();
+
+    let token = authentication::decode_jwt::<ExternalToken>(&token_from_payload, &state)
+        .await
+        .change_context(errors::ApiErrorResponse::Unauthorized)?;
+
+    fp_utils::when(
+        authentication::blacklist::check_user_in_blacklist(&state, &token.user_id, token.exp)
+            .await?,
+        || Err(errors::ApiErrorResponse::InvalidJwtToken),
+    )?;
+
+    token.check_service_type(&external_service_type)?;
+
+    let user_in_db = state
+        .global_store
+        .find_user_by_id(&token.user_id)
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("User not found in database")?;
+
+    let email = user_in_db.email.clone();
+    let name = user_in_db.name;
+
+    Ok(service_api::ApplicationResponse::Json(
+        external_service_auth_api::ExternalVerifyTokenResponse::Hypersense {
+            user_id: user_in_db.user_id,
+            merchant_id: token.merchant_id,
+            name,
+            email,
+        },
+    ))
+}
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index 9b43d7f2b16..18e8e7cdccf 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -144,6 +144,7 @@ pub fn mk_app(
             .service(routes::MerchantConnectorAccount::server(state.clone()))
             .service(routes::RelayWebhooks::server(state.clone()))
             .service(routes::Webhooks::server(state.clone()))
+            .service(routes::Hypersense::server(state.clone()))
             .service(routes::Relay::server(state.clone()));
 
         #[cfg(feature = "oltp")]
diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs
index 80906570d7c..b589d4755f8 100644
--- a/crates/router/src/routes.rs
+++ b/crates/router/src/routes.rs
@@ -23,6 +23,7 @@ pub mod files;
 pub mod fraud_check;
 pub mod gsm;
 pub mod health;
+pub mod hypersense;
 pub mod lock_utils;
 #[cfg(feature = "v1")]
 pub mod locker_migration;
@@ -69,9 +70,9 @@ pub use self::app::PaymentMethodsSession;
 pub use self::app::Recon;
 pub use self::app::{
     ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding,
-    Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Mandates,
-    MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll,
-    Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks,
+    Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Hypersense,
+    Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments,
+    Poll, Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks,
 };
 #[cfg(feature = "olap")]
 pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents};
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index c5e1f8a1082..f69f17e5ed8 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -85,6 +85,7 @@ pub use crate::{
 use crate::{
     configs::{secrets_transformers, Settings},
     db::kafka_store::{KafkaStore, TenantID},
+    routes::hypersense as hypersense_routes,
 };
 
 #[derive(Clone)]
@@ -1311,6 +1312,27 @@ impl Recon {
     }
 }
 
+pub struct Hypersense;
+
+impl Hypersense {
+    pub fn server(state: AppState) -> Scope {
+        web::scope("/hypersense")
+            .app_data(web::Data::new(state))
+            .service(
+                web::resource("/token")
+                    .route(web::get().to(hypersense_routes::get_hypersense_token)),
+            )
+            .service(
+                web::resource("/verify_token")
+                    .route(web::post().to(hypersense_routes::verify_hypersense_token)),
+            )
+            .service(
+                web::resource("/signout")
+                    .route(web::post().to(hypersense_routes::signout_hypersense_token)),
+            )
+    }
+}
+
 #[cfg(feature = "olap")]
 pub struct Blocklist;
 
diff --git a/crates/router/src/routes/hypersense.rs b/crates/router/src/routes/hypersense.rs
new file mode 100644
index 00000000000..a018dfa6605
--- /dev/null
+++ b/crates/router/src/routes/hypersense.rs
@@ -0,0 +1,76 @@
+use actix_web::{web, HttpRequest, HttpResponse};
+use api_models::external_service_auth as external_service_auth_api;
+use router_env::Flow;
+
+use super::AppState;
+use crate::{
+    core::{api_locking, external_service_auth},
+    services::{
+        api,
+        authentication::{self, ExternalServiceType},
+    },
+};
+
+pub async fn get_hypersense_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
+    let flow = Flow::HypersenseTokenRequest;
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        (),
+        |state, user, _, _| {
+            external_service_auth::generate_external_token(
+                state,
+                user,
+                ExternalServiceType::Hypersense,
+            )
+        },
+        &authentication::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn signout_hypersense_token(
+    state: web::Data<AppState>,
+    http_req: HttpRequest,
+    json_payload: web::Json<external_service_auth_api::ExternalSignoutTokenRequest>,
+) -> HttpResponse {
+    let flow = Flow::HypersenseSignoutToken;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &http_req,
+        json_payload.into_inner(),
+        |state, _: (), json_payload, _| {
+            external_service_auth::signout_external_token(state, json_payload)
+        },
+        &authentication::NoAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn verify_hypersense_token(
+    state: web::Data<AppState>,
+    http_req: HttpRequest,
+    json_payload: web::Json<external_service_auth_api::ExternalVerifyTokenRequest>,
+) -> HttpResponse {
+    let flow = Flow::HypersenseVerifyToken;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &http_req,
+        json_payload.into_inner(),
+        |state, _: (), json_payload, _| {
+            external_service_auth::verify_external_token(
+                state,
+                json_payload,
+                ExternalServiceType::Hypersense,
+            )
+        },
+        &authentication::NoAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index ce9dda97c9f..a50a27b9ec4 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -39,6 +39,7 @@ pub enum ApiIdentifier {
     ApplePayCertificatesMigration,
     Relay,
     Documentation,
+    Hypersense,
     PaymentMethodsSession,
 }
 
@@ -311,6 +312,10 @@ impl From<Flow> for ApiIdentifier {
 
             Flow::FeatureMatrix => Self::Documentation,
 
+            Flow::HypersenseTokenRequest
+            | Flow::HypersenseVerifyToken
+            | Flow::HypersenseSignoutToken => Self::Hypersense,
+
             Flow::PaymentMethodSessionCreate
             | Flow::PaymentMethodSessionRetrieve
             | Flow::PaymentMethodSessionUpdateSavedPaymentMethod => Self::PaymentMethodsSession,
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index edf127ac30f..502ebb5099f 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -13,9 +13,7 @@ use api_models::payouts;
 use api_models::{payment_methods::PaymentMethodListRequest, payments};
 use async_trait::async_trait;
 use common_enums::TokenPurpose;
-#[cfg(feature = "v2")]
-use common_utils::fp_utils;
-use common_utils::{date_time, id_type};
+use common_utils::{date_time, fp_utils, id_type};
 #[cfg(feature = "v2")]
 use diesel_models::ephemeral_key;
 use error_stack::{report, ResultExt};
@@ -195,6 +193,13 @@ impl AuthenticationType {
     }
 }
 
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, serde::Deserialize, strum::Display)]
+#[serde(rename_all = "snake_case")]
+#[strum(serialize_all = "snake_case")]
+pub enum ExternalServiceType {
+    Hypersense,
+}
+
 #[cfg(feature = "olap")]
 #[derive(Clone, Debug)]
 pub struct UserFromSinglePurposeToken {
@@ -3857,3 +3862,44 @@ impl ReconToken {
         jwt::generate_jwt(&token_payload, settings).await
     }
 }
+#[derive(serde::Serialize, serde::Deserialize)]
+pub struct ExternalToken {
+    pub user_id: String,
+    pub merchant_id: id_type::MerchantId,
+    pub exp: u64,
+    pub external_service_type: ExternalServiceType,
+}
+
+impl ExternalToken {
+    pub async fn new_token(
+        user_id: String,
+        merchant_id: id_type::MerchantId,
+        settings: &Settings,
+        external_service_type: ExternalServiceType,
+    ) -> 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,
+            merchant_id,
+            exp,
+            external_service_type,
+        };
+        jwt::generate_jwt(&token_payload, settings).await
+    }
+
+    pub fn check_service_type(
+        &self,
+        required_service_type: &ExternalServiceType,
+    ) -> RouterResult<()> {
+        Ok(fp_utils::when(
+            &self.external_service_type != required_service_type,
+            || {
+                Err(errors::ApiErrorResponse::AccessForbidden {
+                    resource: required_service_type.to_string(),
+                })
+            },
+        )?)
+    }
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index b88effea34f..3eda63bdbaa 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -543,6 +543,12 @@ pub enum Flow {
     RelayRetrieve,
     /// Incoming Relay Webhook Receive
     IncomingRelayWebhookReceive,
+    /// Generate Hypersense Token
+    HypersenseTokenRequest,
+    /// Verify Hypersense Token
+    HypersenseVerifyToken,
+    /// Signout Hypersense Token
+    HypersenseSignoutToken,
     /// Payment Method Session Create
     PaymentMethodSessionCreate,
     /// Payment Method Session Retrieve

## 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 -->
#### Endpoints
- GET `hypersense/token` (returns hypersense token).
- POST `hypersense/verify_token` (verifies the token passed as payload).
- POST `hypersense/signout` (invalidates the token).

 
### Additional Changes

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

#### `external_service_auth` module and `ExternalToken`
- `external_service_auth` is a generic service used to issue, verify and invalidate `ExternalToken`,
which can be associated with an external service using the enum field `external_service_type: ExternalServiceType` in the `ExternalToken`.
- Essentially a new service can be integrated by simply creating routes and handlers, and calling the same core functions 
specified in `external_service_auth` modules.
<!--
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).
-->
### Hypersense
#### Description:
##### An AI Ops tool build for Payments Cost Observability
- Monitor your overall transaction payment-related costs across multiple regions, PSPs, and businesses. 
- Obtain actionable insights to optimise costs, track cost deviations, and effectively audit PSP invoices.

Learn more: [Hypersense site](https://www.hypersense.io/)
#### Purpose of Change
To integrate hypersense with hyperswitch dashboard.

## Fixes #7217 

## 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. get the hypersense_token from the response by hitting this curl.
```sh
curl --location '<BASE_URL>/hypersense/token' \
--header 'Authorization: Bearer <TOKEN>'
```
2. using the token obtained above try hitting this to verify.
```sh
curl --location '<BASE_URL>/hypersense/verify_token' \
--header 'Content-Type: application/json' \
--data '{
    "token": "<HYPERSENSE_TOKEN>"
}'
```
3. signout to invalidate this token
```sh
curl --location '<BASE_URL>//hypersense/signout' \
--header 'Content-Type: application/json' \
--data '{
    "token": "<HYPERSENSE_TOKEN>"
}'
```
## 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
