2026-07-26 03:17:40 +02:00
|
|
|
"""API routes for Webhook CRUD and testing."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
2026-08-13 17:51:04 +02:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
2026-07-26 03:17:40 +02:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.core.db import get_db
|
2026-08-13 17:51:04 +02:00
|
|
|
from app.core.rate_limit import RateLimitPolicy, check_rate_limit_policy
|
2026-07-26 03:17:40 +02:00
|
|
|
from app.deps import get_current_user, require_permission
|
|
|
|
|
from app.schemas.webhook import (
|
|
|
|
|
WebhookCreate,
|
|
|
|
|
WebhookResponse,
|
|
|
|
|
WebhookUpdate,
|
|
|
|
|
)
|
|
|
|
|
from app.services import webhook_service
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/v1/webhooks", tags=["webhooks"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"",
|
|
|
|
|
response_model=list[WebhookResponse],
|
2026-08-16 01:17:18 +02:00
|
|
|
dependencies=[Depends(require_permission("workflows:read"))],
|
2026-07-26 03:17:40 +02:00
|
|
|
)
|
|
|
|
|
async def list_webhooks(
|
|
|
|
|
event: str | None = Query(None, description="Filter by event name"),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""List all webhooks for the current tenant."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
2026-07-29 01:52:47 +02:00
|
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
|
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
webhooks = await webhook_service.list_webhooks(db, tenant_id, event=event, user_id=user_id, is_system_admin=is_admin)
|
|
|
|
|
return webhooks
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
2026-07-26 03:17:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"",
|
|
|
|
|
response_model=WebhookResponse,
|
|
|
|
|
status_code=201,
|
2026-08-16 01:17:18 +02:00
|
|
|
dependencies=[Depends(require_permission("workflows:write"))],
|
2026-07-26 03:17:40 +02:00
|
|
|
)
|
|
|
|
|
async def create_webhook(
|
|
|
|
|
body: WebhookCreate,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Create a new webhook subscription."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
|
|
|
user_id = uuid.UUID(current_user["user_id"])
|
2026-07-29 01:52:47 +02:00
|
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
webhook = await webhook_service.create_webhook(
|
|
|
|
|
db, tenant_id, user_id, body.model_dump(), is_system_admin=is_admin
|
|
|
|
|
)
|
|
|
|
|
return webhook
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
2026-07-26 03:17:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/{webhook_id}",
|
|
|
|
|
response_model=WebhookResponse,
|
|
|
|
|
dependencies=[Depends(require_permission("automation:read"))],
|
|
|
|
|
)
|
|
|
|
|
async def get_webhook(
|
|
|
|
|
webhook_id: str,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Get a single webhook by ID."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
2026-07-29 01:52:47 +02:00
|
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
|
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
|
|
|
|
2026-07-26 03:17:40 +02:00
|
|
|
try:
|
|
|
|
|
wh_id = uuid.UUID(webhook_id)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
raise HTTPException(400, detail={"detail": "Invalid webhook_id", "code": "invalid_id"}) from None
|
|
|
|
|
|
2026-07-29 01:52:47 +02:00
|
|
|
try:
|
|
|
|
|
webhook = await webhook_service.get_webhook(db, tenant_id, wh_id, user_id=user_id, is_system_admin=is_admin)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
2026-07-26 03:17:40 +02:00
|
|
|
if webhook is None:
|
|
|
|
|
raise HTTPException(404, detail={"detail": "Webhook not found", "code": "not_found"})
|
|
|
|
|
return webhook
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch(
|
|
|
|
|
"/{webhook_id}",
|
|
|
|
|
response_model=WebhookResponse,
|
|
|
|
|
dependencies=[Depends(require_permission("automation:write"))],
|
|
|
|
|
)
|
|
|
|
|
async def update_webhook(
|
|
|
|
|
webhook_id: str,
|
|
|
|
|
body: WebhookUpdate,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Update an existing webhook subscription."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
|
|
|
user_id = uuid.UUID(current_user["user_id"])
|
2026-07-29 01:52:47 +02:00
|
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
|
|
|
|
2026-07-26 03:17:40 +02:00
|
|
|
try:
|
|
|
|
|
wh_id = uuid.UUID(webhook_id)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
raise HTTPException(400, detail={"detail": "Invalid webhook_id", "code": "invalid_id"}) from None
|
|
|
|
|
|
|
|
|
|
update_data = {k: v for k, v in body.model_dump().items() if v is not None}
|
|
|
|
|
if not update_data:
|
|
|
|
|
raise HTTPException(400, detail={"detail": "No fields to update", "code": "no_updates"})
|
|
|
|
|
|
2026-07-29 01:52:47 +02:00
|
|
|
try:
|
|
|
|
|
webhook = await webhook_service.update_webhook(
|
|
|
|
|
db, tenant_id, wh_id, update_data, user_id=user_id, is_system_admin=is_admin
|
|
|
|
|
)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
2026-07-26 03:17:40 +02:00
|
|
|
if webhook is None:
|
|
|
|
|
raise HTTPException(404, detail={"detail": "Webhook not found", "code": "not_found"})
|
|
|
|
|
return webhook
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
|
|
|
"/{webhook_id}",
|
|
|
|
|
status_code=204,
|
|
|
|
|
dependencies=[Depends(require_permission("automation:write"))],
|
|
|
|
|
)
|
|
|
|
|
async def delete_webhook(
|
|
|
|
|
webhook_id: str,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Delete a webhook subscription."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
2026-07-29 01:52:47 +02:00
|
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
|
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
|
|
|
|
2026-07-26 03:17:40 +02:00
|
|
|
try:
|
|
|
|
|
wh_id = uuid.UUID(webhook_id)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
raise HTTPException(400, detail={"detail": "Invalid webhook_id", "code": "invalid_id"}) from None
|
|
|
|
|
|
2026-07-29 01:52:47 +02:00
|
|
|
try:
|
|
|
|
|
deleted = await webhook_service.delete_webhook(db, tenant_id, wh_id, user_id=user_id, is_system_admin=is_admin)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
2026-07-26 03:17:40 +02:00
|
|
|
if not deleted:
|
|
|
|
|
raise HTTPException(404, detail={"detail": "Webhook not found", "code": "not_found"})
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/{webhook_id}/test",
|
|
|
|
|
dependencies=[Depends(require_permission("automation:write"))],
|
|
|
|
|
)
|
|
|
|
|
async def test_webhook(
|
|
|
|
|
webhook_id: str,
|
2026-08-13 17:51:04 +02:00
|
|
|
request: Request,
|
2026-07-26 03:17:40 +02:00
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Send a test payload to a webhook to verify connectivity."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
2026-07-29 01:52:47 +02:00
|
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
|
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
|
|
|
|
2026-08-13 17:51:04 +02:00
|
|
|
# Rate limit — WEBHOOK policy
|
|
|
|
|
await check_rate_limit_policy(
|
|
|
|
|
f"rate:webhook:test:{tenant_id}:{user_id}",
|
|
|
|
|
RateLimitPolicy.WEBHOOK,
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-26 03:17:40 +02:00
|
|
|
try:
|
|
|
|
|
wh_id = uuid.UUID(webhook_id)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
raise HTTPException(400, detail={"detail": "Invalid webhook_id", "code": "invalid_id"}) from None
|
|
|
|
|
|
2026-07-29 01:52:47 +02:00
|
|
|
try:
|
|
|
|
|
webhook = await webhook_service.get_webhook(db, tenant_id, wh_id, user_id=user_id, is_system_admin=is_admin)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
2026-07-26 03:17:40 +02:00
|
|
|
if webhook is None:
|
|
|
|
|
raise HTTPException(404, detail={"detail": "Webhook not found", "code": "not_found"})
|
|
|
|
|
|
|
|
|
|
test_payload = {
|
|
|
|
|
"type": "test",
|
|
|
|
|
"message": "This is a test webhook from LeoCRM",
|
|
|
|
|
"webhook_id": str(webhook.id),
|
|
|
|
|
}
|
2026-07-29 01:52:47 +02:00
|
|
|
try:
|
|
|
|
|
result = await webhook_service.send_webhook(webhook, "webhook.test", test_payload, user_id=user_id, is_system_admin=is_admin)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(status_code=403, detail=str(e)) from e
|
2026-07-26 03:17:40 +02:00
|
|
|
return result
|