Webhook infrastructure for external event notifications: - SQL migration: webhook_subscriptions + webhook_deliveries tables - Types: CreateWebhookRequest, UpdateWebhookRequest, WebhookDelivery - Service: CRUD operations + trigger_webhooks + HMAC-SHA256 signing - Handlers: REST API endpoints (CRUD + delivery logs) - Worker: WebhookDeliveryWorker with exponential retry (max 3) NOT YET INTEGRATED: needs mod registration in lib.rs + workers/mod.rs, hmac crate dependency, and route mounting. Code is ready for future integration after stabilization phase completes.
111 lines
3.7 KiB
Rust
111 lines
3.7 KiB
Rust
//! Webhook HTTP 处理器
|
|
//!
|
|
//! 提供 Webhook 订阅的 CRUD 和投递日志查询。
|
|
|
|
use axum::{
|
|
extract::{Extension, Path, State},
|
|
http::StatusCode,
|
|
Json,
|
|
};
|
|
use crate::auth::types::AuthContext;
|
|
use crate::error::SaasResult;
|
|
use crate::state::AppState;
|
|
use super::{service, types::*};
|
|
|
|
/// POST /api/v1/webhooks — 创建 Webhook 订阅
|
|
// @connected
|
|
pub async fn create_subscription(
|
|
State(state): State<AppState>,
|
|
Extension(ctx): Extension<AuthContext>,
|
|
Json(req): Json<CreateWebhookRequest>,
|
|
) -> SaasResult<(StatusCode, Json<WebhookSubscription>)> {
|
|
// 验证 URL 格式
|
|
if req.url.is_empty() {
|
|
return Err(crate::error::SaasError::InvalidInput("URL 不能为空".into()));
|
|
}
|
|
if url::Url::parse(&req.url).is_err() {
|
|
return Err(crate::error::SaasError::InvalidInput("URL 格式无效".into()));
|
|
}
|
|
// 验证事件列表不为空
|
|
if req.events.is_empty() {
|
|
return Err(crate::error::SaasError::InvalidInput(
|
|
"事件列表不能为空,至少需要一个事件".into(),
|
|
));
|
|
}
|
|
// 验证每个事件名称格式 (namespace.action)
|
|
for ev in &req.events {
|
|
if !ev.contains('.') || ev.starts_with('.') || ev.ends_with('.') {
|
|
return Err(crate::error::SaasError::InvalidInput(
|
|
format!("事件名称 '{}' 格式无效,应为 namespace.action 格式", ev),
|
|
));
|
|
}
|
|
}
|
|
|
|
let sub = service::create_subscription(&state.db, &ctx.account_id, &req).await?;
|
|
Ok((StatusCode::CREATED, Json(sub)))
|
|
}
|
|
|
|
/// GET /api/v1/webhooks — 列出 Webhook 订阅
|
|
// @connected
|
|
pub async fn list_subscriptions(
|
|
State(state): State<AppState>,
|
|
Extension(ctx): Extension<AuthContext>,
|
|
) -> SaasResult<Json<Vec<WebhookSubscription>>> {
|
|
let subs = service::list_subscriptions(&state.db, &ctx.account_id).await?;
|
|
Ok(Json(subs))
|
|
}
|
|
|
|
/// DELETE /api/v1/webhooks/:id — 删除 Webhook 订阅
|
|
// @connected
|
|
pub async fn delete_subscription(
|
|
State(state): State<AppState>,
|
|
Extension(ctx): Extension<AuthContext>,
|
|
Path(id): Path<String>,
|
|
) -> SaasResult<StatusCode> {
|
|
service::delete_subscription(&state.db, &ctx.account_id, &id).await?;
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
/// PATCH /api/v1/webhooks/:id — 更新 Webhook 订阅
|
|
// @connected
|
|
pub async fn update_subscription(
|
|
State(state): State<AppState>,
|
|
Extension(ctx): Extension<AuthContext>,
|
|
Path(id): Path<String>,
|
|
Json(req): Json<UpdateWebhookRequest>,
|
|
) -> SaasResult<Json<WebhookSubscription>> {
|
|
// 验证 URL 格式(如果提供了)
|
|
if let Some(ref url) = req.url {
|
|
if url.is_empty() {
|
|
return Err(crate::error::SaasError::InvalidInput("URL 不能为空".into()));
|
|
}
|
|
if url::Url::parse(url).is_err() {
|
|
return Err(crate::error::SaasError::InvalidInput("URL 格式无效".into()));
|
|
}
|
|
}
|
|
// 验证事件名称格式(如果提供了)
|
|
if let Some(ref events) = req.events {
|
|
for ev in events {
|
|
if !ev.contains('.') || ev.starts_with('.') || ev.ends_with('.') {
|
|
return Err(crate::error::SaasError::InvalidInput(
|
|
format!("事件名称 '{}' 格式无效,应为 namespace.action 格式", ev),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let sub = service::update_subscription(&state.db, &ctx.account_id, &id, &req).await?;
|
|
Ok(Json(sub))
|
|
}
|
|
|
|
/// GET /api/v1/webhooks/:id/deliveries — 列出投递日志
|
|
// @connected
|
|
pub async fn list_deliveries(
|
|
State(state): State<AppState>,
|
|
Extension(ctx): Extension<AuthContext>,
|
|
Path(id): Path<String>,
|
|
) -> SaasResult<Json<Vec<WebhookDelivery>>> {
|
|
let deliveries = service::list_deliveries(&state.db, &ctx.account_id, &id).await?;
|
|
Ok(Json(deliveries))
|
|
}
|