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}