1use ferrox_errors::AppError;
6use hmac::{Hmac, Mac};
7use reqwest::Client;
8use serde::Serialize;
9use sha2::Sha256;
10use tracing::{error, info};
11
12type HmacSha256 = Hmac<Sha256>;
13
14#[derive(Clone)]
15pub struct WebhookSender {
16 client: Client,
17 secret_key: String,
18}
19
20impl WebhookSender {
21 pub fn new(secret_key: &str) -> Self {
22 Self {
23 client: Client::new(),
24 secret_key: secret_key.to_string(),
25 }
26 }
27
28 pub fn sign_payload(&self, payload: &str) -> Result<String, AppError> {
30 let mut mac = HmacSha256::new_from_slice(self.secret_key.as_bytes())
31 .map_err(|e| AppError::InternalError(format!("HMAC Error: {}", e)))?;
32 mac.update(payload.as_bytes());
33 let result = mac.finalize();
34 Ok(hex::encode(result.into_bytes()))
35 }
36
37 pub fn dispatch<T: Serialize + Send + 'static>(&self, url: &str, payload: T) {
40 let sender = self.clone();
41 let url_owned = url.to_string();
42
43 tokio::spawn(async move {
44 let json_payload = match serde_json::to_string(&payload) {
45 Ok(json) => json,
46 Err(e) => {
47 error!("Failed to serialize webhook payload: {}", e);
48 return;
49 }
50 };
51
52 let signature = match sender.sign_payload(&json_payload) {
53 Ok(sig) => sig,
54 Err(e) => {
55 error!("Failed to sign webhook payload: {}", e);
56 return;
57 }
58 };
59
60 match sender.client.post(&url_owned)
61 .header("Content-Type", "application/json")
62 .header("x-ferrox-signature", signature)
63 .body(json_payload)
64 .send()
65 .await
66 {
67 Ok(response) => {
68 if response.status().is_success() {
69 info!("✅ Webhook successfully delivered to {}", url_owned);
70 } else {
71 error!("❌ Webhook to {} returned status: {}", url_owned, response.status());
72 }
74 }
75 Err(e) => {
76 error!("❌ Webhook delivery to {} failed completely: {}", url_owned, e);
77 }
79 }
80 });
81 }
82}