sagapay_sdk/
webhook.rs

1use hmac::{Hmac, Mac};
2use sha2::Sha256;
3use std::collections::HashMap;
4
5use crate::error::Error;
6use crate::models::WebhookPayload;
7
8/// Handler for SagaPay webhook notifications
9#[derive(Debug, Clone)]
10pub struct WebhookHandler {
11    api_secret: String,
12}
13
14impl WebhookHandler {
15    /// Create a new webhook handler
16    pub fn new(api_secret: &str) -> Self {
17        Self {
18            api_secret: api_secret.to_string(),
19        }
20    }
21
22    /// Process and verify a webhook
23    ///
24    /// # Example
25    ///
26    /// ```no_run
27    /// use sagapay::{WebhookHandler, Error};
28    /// use std::collections::HashMap;
29    ///
30    /// async fn handle_webhook(
31    ///     headers: HashMap<String, String>,
32    ///     body: &str,
33    /// ) -> Result<(), Error> {
34    ///     let api_secret = "your-api-secret";
35    ///     let handler = WebhookHandler::new(api_secret);
36    ///
37    ///     // Get the signature from headers
38    ///     let signature = headers.get("x-sagapay-signature")
39    ///         .ok_or_else(|| Error::InvalidSignature)?;
40    ///
41    ///     // Process and validate the webhook
42    ///     let payload = handler.process_webhook(body, signature)?;
43    ///
44    ///     // Handle the webhook data
45    ///     println!("Transaction ID: {}", payload.id);
46    ///     println!("Type: {:?}", payload.transaction_type);
47    ///     println!("Status: {:?}", payload.status);
48    ///     println!("Amount: {}", payload.amount);
49    ///
50    ///     Ok(())
51    /// }
52    /// ```
53    pub fn process_webhook(&self, body: &str, signature: &str) -> Result<WebhookPayload, Error> {
54        // Verify the signature
55        if !self.verify_signature(body, signature) {
56            return Err(Error::InvalidSignature);
57        }
58
59        // Parse the webhook payload
60        let payload: WebhookPayload =
61            serde_json::from_str(body).map_err(|e| Error::JsonError(e))?;
62
63        Ok(payload)
64    }
65
66    /// Helper method to extract headers and process a webhook in a single step
67    pub fn process_webhook_from_request(
68        &self,
69        headers: &HashMap<String, String>,
70        body: &str,
71    ) -> Result<WebhookPayload, Error> {
72        // Get the signature from headers
73        let signature = headers
74            .get("x-sagapay-signature")
75            .ok_or_else(|| Error::InvalidSignature)?;
76
77        self.process_webhook(body, signature)
78    }
79
80    /// Verify the HMAC signature of a webhook payload
81    fn verify_signature(&self, payload: &str, signature: &str) -> bool {
82        let mut mac = Hmac::<Sha256>::new_from_slice(self.api_secret.as_bytes())
83            .expect("HMAC can take key of any size");
84
85        mac.update(payload.as_bytes());
86
87        // Calculate our own signature
88        let computed = hex::encode(mac.finalize().into_bytes());
89
90        // Compare with the provided signature
91        computed == signature
92    }
93}