//! 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, Extension(ctx): Extension, Json(req): Json, ) -> SaasResult<(StatusCode, Json)> { // 验证 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, Extension(ctx): Extension, ) -> SaasResult>> { 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, Extension(ctx): Extension, Path(id): Path, ) -> SaasResult { 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, Extension(ctx): Extension, Path(id): Path, Json(req): Json, ) -> SaasResult> { // 验证 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, Extension(ctx): Extension, Path(id): Path, ) -> SaasResult>> { let deliveries = service::list_deliveries(&state.db, &ctx.account_id, &id).await?; Ok(Json(deliveries)) }