use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;
pub struct WebhookScaffold;
impl Scaffold for WebhookScaffold {
fn name(&self) -> &'static str {
"webhook"
}
fn description(&self) -> &'static str {
"Webhook handler: HMAC signature verify, outgoing dispatcher, delivery log, retry queue"
}
fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
let mut r = ScaffoldResult::default();
let d = args.dry_run;
write_file(
&mut r,
"src/app/controllers/webhook_controller.rs",
CONTROLLER,
d,
)?;
write_file(
&mut r,
"src/app/middleware/webhook_signature.rs",
SIGNATURE_MW,
d,
)?;
write_file(
&mut r,
"src/app/jobs/dispatch_webhook_job.rs",
DISPATCH_JOB,
d,
)?;
write_file(&mut r, "migrations/create_webhook_tables.sql", MIGRATION, d)?;
r.warnings.push("Set WEBHOOK_SECRET in .env".into());
r.warnings
.push("Register POST /webhooks/incoming route".into());
Ok(r)
}
}
const CONTROLLER: &str = r#"use axum::{extract::State, response::IntoResponse, Json};
use rok_auth::axum::Response;
pub async fn incoming(Json(payload): Json<serde_json::Value>) -> impl IntoResponse {
// TODO: route payload to appropriate handler based on event type
tracing::info!(?payload, "Webhook received");
Response::json(serde_json::json!({ "received": true }))
}
pub async fn list_deliveries() -> impl IntoResponse {
// TODO: query webhook_deliveries table
Response::json(serde_json::json!({ "data": [] }))
}
"#;
const SIGNATURE_MW: &str = r#"use axum::{body::Bytes, extract::Request, middleware::Next, response::Response};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use rok_problem::Problem;
pub async fn verify_webhook_signature(req: Request, next: Next) -> Result<Response, Problem> {
let secret = std::env::var("WEBHOOK_SECRET").unwrap_or_default();
// TODO: read X-Webhook-Signature header, compute HMAC-SHA256, compare
Ok(next.run(req).await)
}
"#;
const DISPATCH_JOB: &str = r#"use rok_queue::Job;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct DispatchWebhookJob {
pub endpoint_id: i64,
pub payload: serde_json::Value,
pub attempt: u32,
}
#[async_trait::async_trait]
impl Job for DispatchWebhookJob {
const QUEUE: &'static str = "webhooks";
async fn handle(&self) -> anyhow::Result<()> {
// TODO: POST payload to endpoint URL, verify response, log delivery
Ok(())
}
}
"#;
const MIGRATION: &str = r#"CREATE TABLE webhook_endpoints (
id BIGSERIAL PRIMARY KEY,
url TEXT NOT NULL,
secret TEXT NOT NULL,
events TEXT[] NOT NULL DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE webhook_deliveries (
id BIGSERIAL PRIMARY KEY,
endpoint_id BIGINT NOT NULL REFERENCES webhook_endpoints(id) ON DELETE CASCADE,
payload JSONB NOT NULL,
status_code INTEGER,
response TEXT,
attempt INTEGER NOT NULL DEFAULT 1,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;