mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-31 01:57:45 +08:00
refactor: include api key expiry workflow into process tracker (#3661)
This commit is contained in:
@ -138,7 +138,7 @@ mod diesel_impl {
|
||||
|
||||
// Tracking data by process_tracker
|
||||
#[derive(Default, Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ApiKeyExpiryWorkflow {
|
||||
pub struct ApiKeyExpiryTrackingData {
|
||||
pub key_id: String,
|
||||
pub merchant_id: String,
|
||||
pub api_key_expiry: Option<PrimitiveDateTime>,
|
||||
|
||||
@ -13,7 +13,7 @@ default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "acc
|
||||
aws_s3 = ["external_services/aws_s3"]
|
||||
aws_kms = ["external_services/aws_kms"]
|
||||
hashicorp-vault = ["external_services/hashicorp-vault"]
|
||||
email = ["external_services/email", "olap"]
|
||||
email = ["external_services/email", "scheduler/email", "olap"]
|
||||
frm = []
|
||||
stripe = ["dep:serde_qs"]
|
||||
release = ["aws_kms", "stripe", "aws_s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"]
|
||||
|
||||
@ -216,6 +216,8 @@ pub enum PTRunner {
|
||||
PaymentsSyncWorkflow,
|
||||
RefundWorkflowRouter,
|
||||
DeleteTokenizeDataWorkflow,
|
||||
#[cfg(feature = "email")]
|
||||
ApiKeyExpiryWorkflow,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
@ -240,6 +242,10 @@ impl ProcessTrackerWorkflows<routes::AppState> for WorkflowRunner {
|
||||
Some(PTRunner::DeleteTokenizeDataWorkflow) => {
|
||||
Box::new(workflows::tokenized_data::DeleteTokenizeDataWorkflow)
|
||||
}
|
||||
#[cfg(feature = "email")]
|
||||
Some(PTRunner::ApiKeyExpiryWorkflow) => {
|
||||
Box::new(workflows::api_key_expiry::ApiKeyExpiryWorkflow)
|
||||
}
|
||||
_ => Err(ProcessTrackerError::UnexpectedFlow)?,
|
||||
};
|
||||
let app_state = &state.clone();
|
||||
|
||||
@ -253,7 +253,7 @@ pub async fn add_api_key_expiry_task(
|
||||
}
|
||||
}
|
||||
|
||||
let api_key_expiry_tracker = &storage::ApiKeyExpiryWorkflow {
|
||||
let api_key_expiry_tracker = &storage::ApiKeyExpiryTrackingData {
|
||||
key_id: api_key.key_id.clone(),
|
||||
merchant_id: api_key.merchant_id.clone(),
|
||||
// We need API key expiry too, because we need to decide on the schedule_time in
|
||||
@ -427,7 +427,7 @@ pub async fn update_api_key_expiry_task(
|
||||
|
||||
let task_ids = vec![task_id.clone()];
|
||||
|
||||
let updated_tracking_data = &storage::ApiKeyExpiryWorkflow {
|
||||
let updated_tracking_data = &storage::ApiKeyExpiryTrackingData {
|
||||
key_id: api_key.key_id.clone(),
|
||||
merchant_id: api_key.merchant_id.clone(),
|
||||
api_key_expiry: api_key.expires_at,
|
||||
|
||||
@ -0,0 +1,203 @@
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<title>API Key Expiry Notice</title>
|
||||
<body style="background-color: #ececec">
|
||||
<style>
|
||||
.apple-footer a {{
|
||||
text-decoration: none !important;
|
||||
color: #999 !important;
|
||||
border: none !important;
|
||||
}}
|
||||
.apple-email a {{
|
||||
text-decoration: none !important;
|
||||
color: #448bff !important;
|
||||
border: none !important;
|
||||
}}
|
||||
</style>
|
||||
<div
|
||||
id="wrapper"
|
||||
style="
|
||||
background-color: none;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
width: 60%;
|
||||
-premailer-height: 200;
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
class="main-table"
|
||||
style="
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
border-top: 5px solid #0165ef;
|
||||
margin: 0 auto;
|
||||
mso-table-lspace: 0;
|
||||
mso-table-rspace: 0;
|
||||
padding: 0 40;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
"
|
||||
bgcolor="#ffffff"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-lg"
|
||||
style="
|
||||
-premailer-height: 75;
|
||||
-premailer-width: 100%;
|
||||
line-height: 30px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
height="75"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-lg"
|
||||
style="
|
||||
-premailer-height: 75;
|
||||
-premailer-width: 100%;
|
||||
line-height: 30px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
height="25"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-lg"
|
||||
style="
|
||||
-premailer-height: 75;
|
||||
-premailer-width: 100%;
|
||||
line-height: 30px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
height="50"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="headline"
|
||||
style="
|
||||
color: #444;
|
||||
font-family: Roboto, Helvetica, Arial, san-serif;
|
||||
font-size: 30px;
|
||||
font-weight: 100;
|
||||
line-height: 36px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
<p style="font-size: 18px">Dear Merchant,</p>
|
||||
<span style="font-size: 18px">
|
||||
It has come to our attention that your API key will expire in {expires_in} days. To ensure uninterrupted
|
||||
access to our platform and continued smooth operation of your services, we kindly request that you take the
|
||||
necessary actions as soon as possible.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-sm"
|
||||
style="
|
||||
-premailer-height: 20;
|
||||
-premailer-width: 80%;
|
||||
line-height: 10px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-sm"
|
||||
style="
|
||||
-premailer-height: 20;
|
||||
-premailer-width: 100%;
|
||||
line-height: 10px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
height="20"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-lg"
|
||||
style="
|
||||
-premailer-height: 75;
|
||||
-premailer-width: 100%;
|
||||
line-height: 30px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
height="75"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="headline"
|
||||
style="
|
||||
color: #444;
|
||||
font-family: Roboto, Helvetica, Arial, san-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 100;
|
||||
line-height: 36px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
Thanks,<br />
|
||||
Team Hyperswitch
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-lg"
|
||||
style="
|
||||
-premailer-height: 75;
|
||||
-premailer-width: 100%;
|
||||
line-height: 30px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
height="75"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="spacer-lg"
|
||||
style="
|
||||
-premailer-height: 75;
|
||||
-premailer-width: 100%;
|
||||
line-height: 30px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
"
|
||||
height="75"
|
||||
width="100%"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
@ -44,6 +44,9 @@ pub enum EmailBody {
|
||||
user_name: String,
|
||||
user_email: String,
|
||||
},
|
||||
ApiKeyExpiryReminder {
|
||||
expires_in: u8,
|
||||
},
|
||||
}
|
||||
|
||||
pub mod html {
|
||||
@ -113,6 +116,10 @@ Email : {user_email}
|
||||
|
||||
(note: This is an auto generated email. Use merchant email for any further communications)",
|
||||
),
|
||||
EmailBody::ApiKeyExpiryReminder { expires_in } => format!(
|
||||
include_str!("assets/api_key_expiry_reminder.html"),
|
||||
expires_in = expires_in,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -381,3 +388,26 @@ impl EmailData for ProFeatureRequest {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ApiKeyExpiryReminder {
|
||||
pub recipient_email: domain::UserEmail,
|
||||
pub subject: &'static str,
|
||||
pub expires_in: u8,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EmailData for ApiKeyExpiryReminder {
|
||||
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
|
||||
let recipient = self.recipient_email.clone().into_inner();
|
||||
|
||||
let body = html::get_html_body(EmailBody::ApiKeyExpiryReminder {
|
||||
expires_in: self.expires_in,
|
||||
});
|
||||
|
||||
Ok(EmailContents {
|
||||
subject: self.subject.to_string(),
|
||||
body: external_services::email::IntermediateString::new(body),
|
||||
recipient,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
#[cfg(feature = "email")]
|
||||
pub use diesel_models::api_keys::ApiKeyExpiryWorkflow;
|
||||
pub use diesel_models::api_keys::ApiKeyExpiryTrackingData;
|
||||
pub use diesel_models::api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, HashedApiKey};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "email")]
|
||||
pub mod api_key_expiry;
|
||||
pub mod payment_sync;
|
||||
pub mod refund_router;
|
||||
pub mod tokenized_data;
|
||||
|
||||
@ -1,30 +1,35 @@
|
||||
use common_utils::ext_traits::ValueExt;
|
||||
use diesel_models::enums::{self as storage_enums};
|
||||
use common_utils::{errors::ValidationError, ext_traits::ValueExt};
|
||||
use diesel_models::{enums as storage_enums, ApiKeyExpiryTrackingData};
|
||||
use router_env::logger;
|
||||
use scheduler::{workflows::ProcessTrackerWorkflow, SchedulerAppState};
|
||||
|
||||
use super::{ApiKeyExpiryWorkflow, ProcessTrackerWorkflow};
|
||||
use crate::{
|
||||
errors,
|
||||
logger::error,
|
||||
routes::AppState,
|
||||
routes::{metrics, AppState},
|
||||
services::email::types::ApiKeyExpiryReminder,
|
||||
types::{
|
||||
api,
|
||||
domain::UserEmail,
|
||||
storage::{self, ProcessTrackerExt},
|
||||
},
|
||||
utils::OptionExt,
|
||||
};
|
||||
|
||||
pub struct ApiKeyExpiryWorkflow;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow {
|
||||
impl ProcessTrackerWorkflow<AppState> for ApiKeyExpiryWorkflow {
|
||||
async fn execute_workflow<'a>(
|
||||
&'a self,
|
||||
state: &'a AppState,
|
||||
process: storage::ProcessTracker,
|
||||
) -> Result<(), errors::ProcessTrackerError> {
|
||||
let db = &*state.store;
|
||||
let tracking_data: storage::ApiKeyExpiryWorkflow = process
|
||||
let tracking_data: ApiKeyExpiryTrackingData = process
|
||||
.tracking_data
|
||||
.clone()
|
||||
.parse_value("ApiKeyExpiryWorkflow")?;
|
||||
.parse_value("ApiKeyExpiryTrackingData")?;
|
||||
|
||||
let key_store = state
|
||||
.store
|
||||
@ -41,7 +46,13 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow {
|
||||
let email_id = merchant_account
|
||||
.merchant_details
|
||||
.parse_value::<api::MerchantDetails>("MerchantDetails")?
|
||||
.primary_email;
|
||||
.primary_email
|
||||
.ok_or(errors::ProcessTrackerError::EValidationError(
|
||||
ValidationError::MissingRequiredField {
|
||||
field_name: "email".to_string(),
|
||||
}
|
||||
.into(),
|
||||
))?;
|
||||
|
||||
let task_id = process.id.clone();
|
||||
|
||||
@ -53,28 +64,26 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow {
|
||||
usize::try_from(retry_count)
|
||||
.map_err(|_| errors::ProcessTrackerError::TypeConversionError)?,
|
||||
)
|
||||
.ok_or(errors::ProcessTrackerError::EApiErrorResponse(
|
||||
errors::ApiErrorResponse::InvalidDataValue {
|
||||
field_name: "index",
|
||||
}
|
||||
.into(),
|
||||
))?;
|
||||
.ok_or(errors::ProcessTrackerError::EApiErrorResponse)?;
|
||||
|
||||
let email_contents = ApiKeyExpiryReminder {
|
||||
recipient_email: UserEmail::from_pii_email(email_id).map_err(|err| {
|
||||
logger::error!(%err,"Failed to convert recipient's email to UserEmail from pii::Email");
|
||||
errors::ProcessTrackerError::EApiErrorResponse
|
||||
})?,
|
||||
subject: "API Key Expiry Notice",
|
||||
expires_in: *expires_in,
|
||||
};
|
||||
|
||||
state
|
||||
.email_client
|
||||
.clone()
|
||||
.send_email(
|
||||
email_id.ok_or_else(|| errors::ProcessTrackerError::MissingRequiredField)?,
|
||||
"API Key Expiry Notice".to_string(),
|
||||
format!("Dear Merchant,\n
|
||||
It has come to our attention that your API key will expire in {expires_in} days. To ensure uninterrupted access to our platform and continued smooth operation of your services, we kindly request that you take the necessary actions as soon as possible.\n\n
|
||||
Thanks,\n
|
||||
Team Hyperswitch"),
|
||||
.compose_and_send_email(
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| errors::ProcessTrackerError::FlowExecutionError {
|
||||
flow: "ApiKeyExpiryWorkflow",
|
||||
})?;
|
||||
.map_err(errors::ProcessTrackerError::EEmailError)?;
|
||||
|
||||
// If all the mails have been sent, then retry_count would be equal to length of the expiry_reminder_days vector
|
||||
if retry_count
|
||||
@ -82,7 +91,7 @@ Team Hyperswitch"),
|
||||
.map_err(|_| errors::ProcessTrackerError::TypeConversionError)?
|
||||
{
|
||||
process
|
||||
.finish_with_status(db, format!("COMPLETED_BY_PT_{task_id}"))
|
||||
.finish_with_status(state.get_db().as_scheduler(), "COMPLETED_BY_PT".to_string())
|
||||
.await?
|
||||
}
|
||||
// If tasks are remaining that has to be scheduled
|
||||
@ -93,12 +102,7 @@ Team Hyperswitch"),
|
||||
usize::try_from(retry_count + 1)
|
||||
.map_err(|_| errors::ProcessTrackerError::TypeConversionError)?,
|
||||
)
|
||||
.ok_or(errors::ProcessTrackerError::EApiErrorResponse(
|
||||
errors::ApiErrorResponse::InvalidDataValue {
|
||||
field_name: "index",
|
||||
}
|
||||
.into(),
|
||||
))?;
|
||||
.ok_or(errors::ProcessTrackerError::EApiErrorResponse)?;
|
||||
|
||||
let updated_schedule_time = tracking_data.api_key_expiry.map(|api_key_expiry| {
|
||||
api_key_expiry.saturating_sub(time::Duration::days(i64::from(*expiry_reminder_day)))
|
||||
|
||||
@ -7,6 +7,7 @@ edition = "2021"
|
||||
default = ["kv_store", "olap"]
|
||||
olap = ["storage_impl/olap"]
|
||||
kv_store = []
|
||||
email = ["external_services/email"]
|
||||
|
||||
[dependencies]
|
||||
# Third party crates
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
pub use common_utils::errors::{ParsingError, ValidationError};
|
||||
#[cfg(feature = "email")]
|
||||
use external_services::email::EmailError;
|
||||
pub use redis_interface::errors::RedisError;
|
||||
pub use storage_impl::errors::ApplicationError;
|
||||
use storage_impl::errors::StorageError;
|
||||
@ -51,6 +53,9 @@ pub enum ProcessTrackerError {
|
||||
EParsingError(error_stack::Report<ParsingError>),
|
||||
#[error("Validation Error Received: {0}")]
|
||||
EValidationError(error_stack::Report<ValidationError>),
|
||||
#[cfg(feature = "email")]
|
||||
#[error("Received Error EmailError: {0}")]
|
||||
EEmailError(error_stack::Report<EmailError>),
|
||||
#[error("Type Conversion error")]
|
||||
TypeConversionError,
|
||||
}
|
||||
@ -111,3 +116,9 @@ error_to_process_tracker_error!(
|
||||
error_stack::Report<ValidationError>,
|
||||
ProcessTrackerError::EValidationError(error_stack::Report<ValidationError>)
|
||||
);
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
error_to_process_tracker_error!(
|
||||
error_stack::Report<EmailError>,
|
||||
ProcessTrackerError::EEmailError(error_stack::Report<EmailError>)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user