use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Json;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
pub struct WebhookTestState {
pub received_webhooks: Arc<tokio::sync::RwLock<Vec<ReceivedWebhook>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceivedWebhook {
pub received_at: String,
pub url: String,
pub event: String,
pub payload: serde_json::Value,
pub headers: HashMap<String, String>,
}
impl Default for WebhookTestState {
fn default() -> Self {
Self {
received_webhooks: Arc::new(tokio::sync::RwLock::new(Vec::new())),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct TestWebhookRequest {
pub url: String,
pub event: String,
pub payload: serde_json::Value,
pub headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Serialize)]
pub struct TestWebhookResponse {
pub success: bool,
pub status_code: Option<u16>,
pub response_body: Option<String>,
pub error: Option<String>,
}
pub async fn test_webhook(
State(_state): State<WebhookTestState>,
Json(request): Json<TestWebhookRequest>,
) -> Result<Json<TestWebhookResponse>, StatusCode> {
let client = reqwest::Client::new();
let mut req = client.post(&request.url).json(&request.payload);
if let Some(headers) = &request.headers {
for (key, value) in headers {
req = req.header(key, value);
}
}
match req.send().await {
Ok(response) => {
let status_code = response.status().as_u16();
let response_body = response.text().await.ok();
Ok(Json(TestWebhookResponse {
success: status_code < 400,
status_code: Some(status_code),
response_body,
error: None,
}))
}
Err(e) => Ok(Json(TestWebhookResponse {
success: false,
status_code: None,
response_body: None,
error: Some(e.to_string()),
})),
}
}
pub async fn receive_webhook(
State(state): State<WebhookTestState>,
headers: http::HeaderMap,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, StatusCode> {
let event = headers
.get("x-webhook-event")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
.or_else(|| payload.get("event").and_then(|v| v.as_str()).map(|s| s.to_string()))
.unwrap_or_else(|| "unknown".to_string());
let url = headers
.get("x-webhook-url")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".to_string());
let mut header_map = HashMap::new();
for (key, value) in headers.iter() {
let key_str = key.as_str().to_string();
if let Ok(value_str) = value.to_str() {
header_map.insert(key_str, value_str.to_string());
}
}
let received = ReceivedWebhook {
received_at: chrono::Utc::now().to_rfc3339(),
url,
event,
payload,
headers: header_map,
};
state.received_webhooks.write().await.push(received.clone());
Ok(Json(serde_json::json!({
"status": "received",
"event": received.event,
"received_at": received.received_at,
})))
}
pub async fn get_received_webhooks(
State(state): State<WebhookTestState>,
) -> Result<Json<Vec<ReceivedWebhook>>, StatusCode> {
let webhooks = state.received_webhooks.read().await.clone();
Ok(Json(webhooks))
}
pub async fn clear_received_webhooks(
State(state): State<WebhookTestState>,
) -> Result<Json<serde_json::Value>, StatusCode> {
state.received_webhooks.write().await.clear();
Ok(Json(serde_json::json!({"status": "cleared"})))
}
pub fn webhook_test_router(state: WebhookTestState) -> axum::Router {
use axum::routing::{delete, get, post};
axum::Router::new()
.route("/api/v1/webhooks/test", post(test_webhook))
.route("/api/v1/webhooks/receive", post(receive_webhook))
.route("/api/v1/webhooks/received", get(get_received_webhooks))
.route("/api/v1/webhooks/received", delete(clear_received_webhooks))
.with_state(state)
}