mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
Phase 1: Interactive REST API Catalog - GET /api/openapi/spec: serves parsed openapi.yaml as JSON catalog - POST /api/openapi/try: Try It proxy for inline endpoint testing - Endpoint catalog with tag grouping, search, method badges - Expand: schemas, auth, curl examples, Try It panel Phase 2: OpenAPI Spec Viewer - Spec info header with version, download YAML/JSON, schema browser Phase 3: Webhooks & Event Subscriptions - Migration 011: webhooks table - src/lib/db/webhooks.ts: CRUD + delivery tracking + auto-disable - src/lib/webhookDispatcher.ts: HMAC-SHA256, retries - API: CRUD /api/webhooks + test delivery - Dashboard: add/edit/toggle/test/delete webhook UI 923 tests pass, tsc clean
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
/**
|
|
* Database module: Webhooks
|
|
* CRUD operations for webhook event subscriptions
|
|
*/
|
|
|
|
import { getDbInstance } from "./core";
|
|
import crypto from "crypto";
|
|
|
|
export interface Webhook {
|
|
id: string;
|
|
url: string;
|
|
events: string[];
|
|
secret: string | null;
|
|
enabled: boolean;
|
|
description: string;
|
|
created_at: string;
|
|
last_triggered_at: string | null;
|
|
last_status: number | null;
|
|
failure_count: number;
|
|
}
|
|
|
|
interface WebhookRow {
|
|
id: string;
|
|
url: string;
|
|
events: string;
|
|
secret: string | null;
|
|
enabled: number;
|
|
description: string;
|
|
created_at: string;
|
|
last_triggered_at: string | null;
|
|
last_status: number | null;
|
|
failure_count: number;
|
|
}
|
|
|
|
function rowToWebhook(row: WebhookRow): Webhook {
|
|
return {
|
|
...row,
|
|
events: JSON.parse(row.events || '["*"]'),
|
|
enabled: row.enabled === 1,
|
|
};
|
|
}
|
|
|
|
export function getWebhooks(): Webhook[] {
|
|
const db = getDbInstance();
|
|
const rows = db.prepare("SELECT * FROM webhooks ORDER BY created_at DESC").all() as WebhookRow[];
|
|
return rows.map(rowToWebhook);
|
|
}
|
|
|
|
export function getWebhook(id: string): Webhook | null {
|
|
const db = getDbInstance();
|
|
const row = db.prepare("SELECT * FROM webhooks WHERE id = ?").get(id) as WebhookRow | undefined;
|
|
return row ? rowToWebhook(row) : null;
|
|
}
|
|
|
|
export function getEnabledWebhooks(): Webhook[] {
|
|
const db = getDbInstance();
|
|
const rows = db.prepare("SELECT * FROM webhooks WHERE enabled = 1").all() as WebhookRow[];
|
|
return rows.map(rowToWebhook);
|
|
}
|
|
|
|
export function createWebhook(data: {
|
|
url: string;
|
|
events?: string[];
|
|
secret?: string;
|
|
description?: string;
|
|
}): Webhook {
|
|
const db = getDbInstance();
|
|
const id = crypto.randomUUID();
|
|
const secret = data.secret || `whsec_${crypto.randomBytes(24).toString("hex")}`;
|
|
|
|
db.prepare(
|
|
`INSERT INTO webhooks (id, url, events, secret, description)
|
|
VALUES (?, ?, ?, ?, ?)`
|
|
).run(id, data.url, JSON.stringify(data.events || ["*"]), secret, data.description || "");
|
|
|
|
return getWebhook(id)!;
|
|
}
|
|
|
|
export function updateWebhook(
|
|
id: string,
|
|
data: Partial<{
|
|
url: string;
|
|
events: string[];
|
|
secret: string;
|
|
enabled: boolean;
|
|
description: string;
|
|
}>
|
|
): Webhook | null {
|
|
const db = getDbInstance();
|
|
const existing = getWebhook(id);
|
|
if (!existing) return null;
|
|
|
|
const fields: string[] = [];
|
|
const values: any[] = [];
|
|
|
|
if (data.url !== undefined) {
|
|
fields.push("url = ?");
|
|
values.push(data.url);
|
|
}
|
|
if (data.events !== undefined) {
|
|
fields.push("events = ?");
|
|
values.push(JSON.stringify(data.events));
|
|
}
|
|
if (data.secret !== undefined) {
|
|
fields.push("secret = ?");
|
|
values.push(data.secret);
|
|
}
|
|
if (data.enabled !== undefined) {
|
|
fields.push("enabled = ?");
|
|
values.push(data.enabled ? 1 : 0);
|
|
}
|
|
if (data.description !== undefined) {
|
|
fields.push("description = ?");
|
|
values.push(data.description);
|
|
}
|
|
|
|
if (fields.length === 0) return existing;
|
|
|
|
values.push(id);
|
|
db.prepare(`UPDATE webhooks SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
|
|
return getWebhook(id);
|
|
}
|
|
|
|
export function deleteWebhook(id: string): boolean {
|
|
const db = getDbInstance();
|
|
const result = db.prepare("DELETE FROM webhooks WHERE id = ?").run(id);
|
|
return (result as any).changes > 0;
|
|
}
|
|
|
|
export function recordWebhookDelivery(id: string, status: number, success: boolean): void {
|
|
const db = getDbInstance();
|
|
if (success) {
|
|
db.prepare(
|
|
`UPDATE webhooks SET last_triggered_at = datetime('now'), last_status = ?, failure_count = 0 WHERE id = ?`
|
|
).run(status, id);
|
|
} else {
|
|
db.prepare(
|
|
`UPDATE webhooks SET last_triggered_at = datetime('now'), last_status = ?, failure_count = failure_count + 1 WHERE id = ?`
|
|
).run(status, id);
|
|
}
|
|
}
|
|
|
|
export function disableWebhooksWithHighFailures(threshold = 10): number {
|
|
const db = getDbInstance();
|
|
const result = db
|
|
.prepare(`UPDATE webhooks SET enabled = 0 WHERE failure_count >= ? AND enabled = 1`)
|
|
.run(threshold);
|
|
return (result as any).changes;
|
|
}
|