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