246 lines
7.9 KiB
PL/PgSQL
246 lines
7.9 KiB
PL/PgSQL
-- Migration: Add Webhook Support to Triggers
|
|
-- Date: 2026-01-20
|
|
-- Description: Adds webhook capabilities to the trigger system, allowing any trigger
|
|
-- to be webhook-enabled with a unique webhook key for external integrations.
|
|
|
|
|
|
-- Add webhook columns to trigger table
|
|
ALTER TABLE trigger
|
|
ADD COLUMN IF NOT EXISTS webhook_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
ADD COLUMN IF NOT EXISTS webhook_key VARCHAR(64) UNIQUE,
|
|
ADD COLUMN IF NOT EXISTS webhook_secret VARCHAR(128);
|
|
|
|
-- Add comments for documentation
|
|
COMMENT ON COLUMN trigger.webhook_enabled IS
|
|
'Whether webhooks are enabled for this trigger. When enabled, external systems can POST to the webhook URL to create events.';
|
|
|
|
COMMENT ON COLUMN trigger.webhook_key IS
|
|
'Unique webhook key used in the webhook URL. Format: wh_[32 alphanumeric chars]. Acts as a bearer token for webhook authentication.';
|
|
|
|
COMMENT ON COLUMN trigger.webhook_secret IS
|
|
'Optional secret for HMAC signature verification. When set, webhook requests must include a valid X-Webhook-Signature header.';
|
|
|
|
-- Create index for fast webhook key lookup
|
|
CREATE INDEX IF NOT EXISTS idx_trigger_webhook_key
|
|
ON trigger(webhook_key)
|
|
WHERE webhook_key IS NOT NULL;
|
|
|
|
-- Create index for querying webhook-enabled triggers
|
|
CREATE INDEX IF NOT EXISTS idx_trigger_webhook_enabled
|
|
ON trigger(webhook_enabled)
|
|
WHERE webhook_enabled = TRUE;
|
|
|
|
-- Add webhook-related metadata tracking to events
|
|
-- Events use the 'config' JSONB column for metadata
|
|
-- We'll add indexes to efficiently query webhook-sourced events
|
|
|
|
-- Create index for webhook-sourced events (using config column)
|
|
CREATE INDEX IF NOT EXISTS idx_event_webhook_source
|
|
ON event((config->>'source'))
|
|
WHERE (config->>'source') = 'webhook';
|
|
|
|
-- Create index for webhook key lookup in event config
|
|
CREATE INDEX IF NOT EXISTS idx_event_webhook_key
|
|
ON event((config->>'webhook_key'))
|
|
WHERE config->>'webhook_key' IS NOT NULL;
|
|
|
|
-- Function to generate webhook key
|
|
CREATE OR REPLACE FUNCTION generate_webhook_key()
|
|
RETURNS VARCHAR(64) AS $$
|
|
DECLARE
|
|
key_prefix VARCHAR(3) := 'wh_';
|
|
random_suffix VARCHAR(32);
|
|
new_key VARCHAR(64);
|
|
max_attempts INT := 10;
|
|
attempt INT := 0;
|
|
BEGIN
|
|
LOOP
|
|
-- Generate 32 random alphanumeric characters
|
|
random_suffix := encode(gen_random_bytes(24), 'base64');
|
|
random_suffix := REPLACE(random_suffix, '/', '');
|
|
random_suffix := REPLACE(random_suffix, '+', '');
|
|
random_suffix := REPLACE(random_suffix, '=', '');
|
|
random_suffix := LOWER(LEFT(random_suffix, 32));
|
|
|
|
-- Construct full key
|
|
new_key := key_prefix || random_suffix;
|
|
|
|
-- Check if key already exists
|
|
IF NOT EXISTS (SELECT 1 FROM trigger WHERE webhook_key = new_key) THEN
|
|
RETURN new_key;
|
|
END IF;
|
|
|
|
-- Increment attempt counter
|
|
attempt := attempt + 1;
|
|
IF attempt >= max_attempts THEN
|
|
RAISE EXCEPTION 'Failed to generate unique webhook key after % attempts', max_attempts;
|
|
END IF;
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION generate_webhook_key() IS
|
|
'Generates a unique webhook key with format wh_[32 alphanumeric chars]. Ensures uniqueness by checking existing keys.';
|
|
|
|
-- Function to enable webhooks for a trigger
|
|
CREATE OR REPLACE FUNCTION enable_trigger_webhook(
|
|
p_trigger_id BIGINT
|
|
)
|
|
RETURNS TABLE(
|
|
webhook_enabled BOOLEAN,
|
|
webhook_key VARCHAR(64),
|
|
webhook_url TEXT
|
|
) AS $$
|
|
DECLARE
|
|
v_new_key VARCHAR(64);
|
|
v_existing_key VARCHAR(64);
|
|
v_base_url TEXT;
|
|
BEGIN
|
|
-- Check if trigger exists
|
|
IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN
|
|
RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id;
|
|
END IF;
|
|
|
|
-- Get existing webhook key if any
|
|
SELECT t.webhook_key INTO v_existing_key
|
|
FROM trigger t
|
|
WHERE t.id = p_trigger_id;
|
|
|
|
-- Generate new key if one doesn't exist
|
|
IF v_existing_key IS NULL THEN
|
|
v_new_key := generate_webhook_key();
|
|
ELSE
|
|
v_new_key := v_existing_key;
|
|
END IF;
|
|
|
|
-- Update trigger to enable webhooks
|
|
UPDATE trigger
|
|
SET
|
|
webhook_enabled = TRUE,
|
|
webhook_key = v_new_key,
|
|
updated = NOW()
|
|
WHERE id = p_trigger_id;
|
|
|
|
-- Construct webhook URL (base URL should be configured elsewhere)
|
|
-- For now, return just the path
|
|
v_base_url := '/api/v1/webhooks/' || v_new_key;
|
|
|
|
-- Return result
|
|
RETURN QUERY
|
|
SELECT
|
|
TRUE::BOOLEAN as webhook_enabled,
|
|
v_new_key as webhook_key,
|
|
v_base_url as webhook_url;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION enable_trigger_webhook(BIGINT) IS
|
|
'Enables webhooks for a trigger. Generates a new webhook key if one does not exist. Returns webhook details.';
|
|
|
|
-- Function to disable webhooks for a trigger
|
|
CREATE OR REPLACE FUNCTION disable_trigger_webhook(
|
|
p_trigger_id BIGINT
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
-- Check if trigger exists
|
|
IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN
|
|
RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id;
|
|
END IF;
|
|
|
|
-- Update trigger to disable webhooks
|
|
-- Note: We keep the webhook_key for audit purposes
|
|
UPDATE trigger
|
|
SET
|
|
webhook_enabled = FALSE,
|
|
updated = NOW()
|
|
WHERE id = p_trigger_id;
|
|
|
|
RETURN TRUE;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION disable_trigger_webhook(BIGINT) IS
|
|
'Disables webhooks for a trigger. Webhook key is retained for audit purposes.';
|
|
|
|
-- Function to regenerate webhook key for a trigger
|
|
CREATE OR REPLACE FUNCTION regenerate_trigger_webhook_key(
|
|
p_trigger_id BIGINT
|
|
)
|
|
RETURNS TABLE(
|
|
webhook_key VARCHAR(64),
|
|
previous_key_revoked BOOLEAN
|
|
) AS $$
|
|
DECLARE
|
|
v_old_key VARCHAR(64);
|
|
v_new_key VARCHAR(64);
|
|
BEGIN
|
|
-- Check if trigger exists
|
|
IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN
|
|
RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id;
|
|
END IF;
|
|
|
|
-- Get existing key
|
|
SELECT t.webhook_key INTO v_old_key
|
|
FROM trigger t
|
|
WHERE t.id = p_trigger_id;
|
|
|
|
-- Generate new key
|
|
v_new_key := generate_webhook_key();
|
|
|
|
-- Update trigger with new key
|
|
UPDATE trigger
|
|
SET
|
|
webhook_key = v_new_key,
|
|
updated = NOW()
|
|
WHERE id = p_trigger_id;
|
|
|
|
-- Return result
|
|
RETURN QUERY
|
|
SELECT
|
|
v_new_key as webhook_key,
|
|
(v_old_key IS NOT NULL)::BOOLEAN as previous_key_revoked;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION regenerate_trigger_webhook_key(BIGINT) IS
|
|
'Regenerates the webhook key for a trigger. The old key is immediately revoked.';
|
|
|
|
-- Create a view for webhook statistics
|
|
CREATE OR REPLACE VIEW webhook_stats AS
|
|
SELECT
|
|
t.id as trigger_id,
|
|
t.ref as trigger_ref,
|
|
t.webhook_enabled,
|
|
t.webhook_key,
|
|
t.created as webhook_created_at,
|
|
COUNT(e.id) as total_events,
|
|
MAX(e.created) as last_event_at,
|
|
MIN(e.created) as first_event_at
|
|
FROM trigger t
|
|
LEFT JOIN event e ON
|
|
e.trigger = t.id
|
|
AND (e.config->>'source') = 'webhook'
|
|
WHERE t.webhook_enabled = TRUE
|
|
GROUP BY t.id, t.ref, t.webhook_enabled, t.webhook_key, t.created;
|
|
|
|
COMMENT ON VIEW webhook_stats IS
|
|
'Statistics for webhook-enabled triggers including event counts and timestamps.';
|
|
|
|
-- Grant permissions (adjust as needed for your RBAC setup)
|
|
-- GRANT SELECT ON webhook_stats TO attune_api;
|
|
-- GRANT EXECUTE ON FUNCTION generate_webhook_key() TO attune_api;
|
|
-- GRANT EXECUTE ON FUNCTION enable_trigger_webhook(BIGINT) TO attune_api;
|
|
-- GRANT EXECUTE ON FUNCTION disable_trigger_webhook(BIGINT) TO attune_api;
|
|
-- GRANT EXECUTE ON FUNCTION regenerate_trigger_webhook_key(BIGINT) TO attune_api;
|
|
|
|
-- Trigger update timestamp is already handled by existing triggers
|
|
-- No need to add it again
|
|
|
|
-- Migration complete messages
|
|
DO $$
|
|
BEGIN
|
|
RAISE NOTICE 'Webhook support migration completed successfully';
|
|
RAISE NOTICE 'Webhook-enabled triggers can now receive events via POST /api/v1/webhooks/:webhook_key';
|
|
END $$;
|