"""Webhooks router — CRUD for webhook configurations and async dispatch. Webhooks fire when events occur in runner.py and sweep.py. Delivery is dispatched asynchronously via Celery (or synchronous fallback). Each delivery attempt retries up to 3 times with exponential backoff. Delivery status is logged to the WebhookDelivery table. """ import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from auth import get_current_user from main import get_db from models import User, WebhookConfig from schemas import ( WebhookCreate, WebhookListResponse, WebhookResponse, WebhookUpdate, ) router = APIRouter() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _get_webhook_or_404(db: Session, webhook_id: uuid.UUID) -> WebhookConfig: webhook = db.query(WebhookConfig).filter(WebhookConfig.id == webhook_id).first() if webhook is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Webhook not found") return webhook # --------------------------------------------------------------------------- # CRUD # --------------------------------------------------------------------------- @router.get("/", response_model=WebhookListResponse) def list_webhooks( event_type: str | None = None, db: Session = Depends(get_db), _user: User = Depends(get_current_user), ) -> WebhookListResponse: """List all webhook configurations, optionally filtered by event_type.""" query = db.query(WebhookConfig) if event_type: query = query.filter(WebhookConfig.event_type == event_type) webhooks = query.order_by(WebhookConfig.event_type).all() return WebhookListResponse( items=[WebhookResponse.model_validate(wh) for wh in webhooks], total=len(webhooks), ) @router.post("/", response_model=WebhookResponse, status_code=status.HTTP_201_CREATED) def create_webhook( body: WebhookCreate, db: Session = Depends(get_db), _user: User = Depends(get_current_user), ) -> WebhookResponse: """Create a new webhook configuration.""" webhook = WebhookConfig( event_type=body.event_type, url=body.url, headers=body.headers, is_active=body.is_active, ) db.add(webhook) db.commit() db.refresh(webhook) return WebhookResponse.model_validate(webhook) @router.get("/{webhook_id}", response_model=WebhookResponse) def get_webhook( webhook_id: uuid.UUID, db: Session = Depends(get_db), _user: User = Depends(get_current_user), ) -> WebhookResponse: """Get a single webhook configuration.""" webhook = _get_webhook_or_404(db, webhook_id) return WebhookResponse.model_validate(webhook) @router.put("/{webhook_id}", response_model=WebhookResponse) def update_webhook( webhook_id: uuid.UUID, body: WebhookUpdate, db: Session = Depends(get_db), _user: User = Depends(get_current_user), ) -> WebhookResponse: """Update a webhook configuration.""" webhook = _get_webhook_or_404(db, webhook_id) if body.event_type is not None: webhook.event_type = body.event_type if body.url is not None: webhook.url = body.url if body.headers is not None: webhook.headers = body.headers if body.is_active is not None: webhook.is_active = body.is_active db.commit() db.refresh(webhook) return WebhookResponse.model_validate(webhook) @router.delete("/{webhook_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_webhook( webhook_id: uuid.UUID, db: Session = Depends(get_db), _user: User = Depends(get_current_user), ) -> None: """Delete a webhook configuration.""" webhook = _get_webhook_or_404(db, webhook_id) db.delete(webhook) db.commit()