use crate::models::Webhook; use hmac::{Hmac, Mac}; use sha2::Sha256; use sqlx::PgPool; use uuid::Uuid; pub struct WebhookService { pool: PgPool, } impl WebhookService { pub fn new(pool: PgPool) -> Self { Self { pool } } pub async fn dispatch(&self, org_id: Uuid, event_type: &str, payload: &serde_json::Value) { let webhooks = match sqlx::query_as::<_, Webhook>( "SELECT * FROM webhooks WHERE organization_id = $1 AND is_active = TRUE AND $2 = ANY(events)" ) .bind(org_id) .bind(event_type) .fetch_all(&self.pool) .await { Ok(w) => w, Err(e) => { tracing::error!("Error al obtener los webhooks para la org {}: {}", org_id, e); return; } }; if webhooks.is_empty() { return; } let client = reqwest::Client::new(); let payload_str = payload.to_string(); for webhook in webhooks { let mut request = client .post(&webhook.url) .header("Content-Type", "application/json") .header("X-OpenCCB-Event", event_type) .header("X-OpenCCB-Delivery", Uuid::new_v4().to_string()); if let Some(secret) = &webhook.secret { let signature = generate_signature(secret, &payload_str); request = request.header("X-OpenCCB-Signature", signature); } let url = webhook.url.clone(); let res = request.body(payload_str.clone()).send().await; match res { Ok(response) => { if !response.status().is_success() { tracing::warn!( "La entrega del webhook a {} (evento: {}) falló con el estado {}", url, event_type, response.status() ); } } Err(e) => { tracing::error!( "Error al entregar el webhook a {} (evento: {}): {}", url, event_type, e ); } } } } } fn generate_signature(secret: &str, payload: &str) -> String { type HmacSha256 = Hmac; let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); mac.update(payload.as_bytes()); let result = mac.finalize(); hex::encode(result.into_bytes()) }