diff --git a/backend/app/services/webhook_service.py b/backend/app/services/webhook_service.py new file mode 100644 index 0000000..ca490fc --- /dev/null +++ b/backend/app/services/webhook_service.py @@ -0,0 +1,78 @@ +"""Webhook service: trigger external notifications.""" +import json +import hmac +import hashlib +import httpx +from sqlalchemy.orm import Session +from typing import List, Optional +from app.models.webhook import WebhookConfig + + +class WebhookService: + @staticmethod + def trigger(db: Session, tenant_id: int, event_type: str, payload: dict): + """Find matching webhooks and POST to their URLs.""" + hooks = db.query(WebhookConfig).filter( + WebhookConfig.tenant_id == tenant_id, + WebhookConfig.is_active == True, + ).all() + + for hook in hooks: + events = json.loads(hook.events or "[]") + if event_type not in events: + continue + try: + body = json.dumps({"event": event_type, "data": payload}, ensure_ascii=False) + headers = {"Content-Type": "application/json"} + if hook.secret: + sig = hmac.new( + hook.secret.encode(), body.encode(), hashlib.sha256 + ).hexdigest() + headers["X-Webhook-Signature"] = sig + + with httpx.Client(timeout=10) as client: + client.post(hook.url, content=body, headers=headers) + except Exception: + pass # webhook delivery is best-effort + + @staticmethod + def list_hooks(db, tenant_id): + return db.query(WebhookConfig).filter( + WebhookConfig.tenant_id == tenant_id + ).order_by(WebhookConfig.created_at.desc()).all() + + @staticmethod + def create_hook(db, tenant_id, req): + hook = WebhookConfig( + tenant_id=tenant_id, url=req.url, events=req.events, + secret=req.secret, project_id=req.project_id, + ) + db.add(hook) + db.commit() + db.refresh(hook) + return hook + + @staticmethod + def update_hook(db, tenant_id, hook_id, req): + hook = db.query(WebhookConfig).filter( + WebhookConfig.id == hook_id, WebhookConfig.tenant_id == tenant_id + ).first() + if not hook: + return None + for f in ["url", "events", "secret", "is_active"]: + if hasattr(req, f) and getattr(req, f) is not None: + setattr(hook, f, getattr(req, f)) + db.commit() + db.refresh(hook) + return hook + + @staticmethod + def delete_hook(db, tenant_id, hook_id): + hook = db.query(WebhookConfig).filter( + WebhookConfig.id == hook_id, WebhookConfig.tenant_id == tenant_id + ).first() + if not hook: + return False + db.delete(hook) + db.commit() + return True