use crate::error::{Result, TapsilatError};
use crate::types::{WebhookEvent, WebhookVerificationConfig, WebhookVerificationResult};
use std::time::{SystemTime, UNIX_EPOCH};
pub struct WebhookModule;
impl WebhookModule {
pub fn verify_webhook(payload: &str, signature: &str, secret: &str) -> Result<bool> {
Self::verify_signature(payload, signature, secret)
}
pub fn verify_webhook_advanced(
payload: &str,
signature: &str,
config: &WebhookVerificationConfig,
) -> Result<WebhookVerificationResult> {
let webhook_event: WebhookEvent = serde_json::from_str(payload).map_err(|e| {
TapsilatError::InvalidResponse(format!("Invalid webhook payload: {}", e))
})?;
if let Some(tolerance) = config.tolerance_seconds {
if let Err(e) = Self::verify_timestamp(&webhook_event.timestamp, tolerance) {
return Ok(WebhookVerificationResult {
is_valid: false,
error: Some(format!("Timestamp validation failed: {}", e)),
});
}
}
match Self::verify_signature(payload, signature, &config.secret) {
Ok(is_valid) => Ok(WebhookVerificationResult {
is_valid,
error: if is_valid {
None
} else {
Some("Invalid signature".to_string())
},
}),
Err(e) => Ok(WebhookVerificationResult {
is_valid: false,
error: Some(format!("Signature verification error: {}", e)),
}),
}
}
pub fn parse_webhook(payload: &str) -> Result<WebhookEvent> {
serde_json::from_str(payload).map_err(|e| {
TapsilatError::InvalidResponse(format!("Failed to parse webhook payload: {}", e))
})
}
fn verify_signature(payload: &str, signature: &str, secret: &str) -> Result<bool> {
let signature = signature.strip_prefix("sha256=").unwrap_or(signature);
let expected_signature = Self::create_signature(payload, secret)?;
Ok(signature == expected_signature)
}
fn create_signature(payload: &str, secret: &str) -> Result<String> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
format!("{}{}", secret, payload).hash(&mut hasher);
let hash = hasher.finish();
Ok(format!("{:x}", hash))
}
fn verify_timestamp(timestamp_str: &str, tolerance_seconds: u64) -> Result<()> {
let webhook_time = if timestamp_str.contains('T') {
Self::parse_iso8601_timestamp(timestamp_str)?
} else {
timestamp_str.parse::<u64>().map_err(|e| {
TapsilatError::InvalidResponse(format!("Invalid timestamp format: {}", e))
})?
};
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| TapsilatError::InvalidResponse(format!("System time error: {}", e)))?
.as_secs();
let time_diff = current_time.abs_diff(webhook_time);
if time_diff > tolerance_seconds {
return Err(TapsilatError::InvalidResponse(format!(
"Webhook timestamp too old or too far in future. Difference: {}s, tolerance: {}s",
time_diff, tolerance_seconds
)));
}
Ok(())
}
fn parse_iso8601_timestamp(_timestamp: &str) -> Result<u64> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| TapsilatError::InvalidResponse(format!("Timestamp parsing error: {}", e)))
.map(|d| d.as_secs())
}
pub fn create_verification_config(
secret: String,
tolerance_seconds: Option<u64>,
) -> WebhookVerificationConfig {
WebhookVerificationConfig {
secret,
tolerance_seconds,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_webhook_verification_config() {
let config =
WebhookModule::create_verification_config("test_secret".to_string(), Some(300));
assert_eq!(config.secret, "test_secret");
assert_eq!(config.tolerance_seconds, Some(300));
}
#[test]
fn test_webhook_parsing() {
let payload = r#"{
"event_type": "order.completed",
"data": {
"order_id": "order_123",
"amount": 100.0,
"currency": "TRY",
"status": "completed"
},
"timestamp": "2023-01-01T00:00:00Z"
}"#;
let result = WebhookModule::parse_webhook(payload);
assert!(result.is_ok());
let webhook = result.unwrap();
assert!(matches!(
webhook.event_type,
crate::types::WebhookEventType::OrderCompleted
));
}
}