Backend: - Add 2FA authentication with TOTP support - Add API keys management system - Add audit logging for security events - Add file upload/management system - Add notifications system with preferences - Add session management - Add webhooks integration - Add analytics endpoints - Add export functionality - Add password policy enforcement - Add new database migrations for core tables Frontend: - Add module position system (top/bottom sidebar sections) - Add search and notifications module configuration tabs - Add mobile logo replacing hamburger menu - Center page title absolutely when no tabs present - Align sidebar footer toggles with navigation items - Add lighter icon color in dark theme for mobile - Add API keys management page - Add notifications page with context - Add admin analytics and audit logs pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
381 lines
11 KiB
Python
381 lines
11 KiB
Python
"""Webhook management endpoints."""
|
|
|
|
import json
|
|
from typing import Any, List
|
|
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.dependencies import get_db, get_current_superuser
|
|
from app.models.user import User
|
|
from app import crud
|
|
from app.schemas.webhook import (
|
|
WebhookCreate,
|
|
WebhookUpdate,
|
|
Webhook as WebhookSchema,
|
|
WebhookWithSecret,
|
|
WebhookDelivery as WebhookDeliverySchema,
|
|
WebhookTest,
|
|
WEBHOOK_EVENTS,
|
|
)
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=List[WebhookSchema])
|
|
def list_webhooks(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
is_active: bool = None
|
|
):
|
|
"""
|
|
List all webhooks.
|
|
Requires superuser permissions.
|
|
"""
|
|
webhooks = crud.webhook.get_multi(db, skip=skip, limit=limit, is_active=is_active)
|
|
# Convert events from JSON string to list
|
|
result = []
|
|
for webhook in webhooks:
|
|
webhook_dict = {
|
|
"id": webhook.id,
|
|
"name": webhook.name,
|
|
"url": webhook.url,
|
|
"secret": webhook.secret,
|
|
"events": json.loads(webhook.events) if webhook.events else [],
|
|
"is_active": webhook.is_active,
|
|
"retry_count": webhook.retry_count,
|
|
"timeout_seconds": webhook.timeout_seconds,
|
|
"created_by": webhook.created_by,
|
|
"created_at": webhook.created_at,
|
|
"updated_at": webhook.updated_at,
|
|
"last_triggered_at": webhook.last_triggered_at,
|
|
"success_count": webhook.success_count,
|
|
"failure_count": webhook.failure_count,
|
|
}
|
|
result.append(webhook_dict)
|
|
return result
|
|
|
|
|
|
@router.post("/", response_model=WebhookWithSecret, status_code=status.HTTP_201_CREATED)
|
|
def create_webhook(
|
|
webhook_in: WebhookCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
Create a new webhook.
|
|
Returns the webhook with its secret (only shown once at creation).
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.create(db, obj_in=webhook_in, created_by=current_user.id)
|
|
|
|
# Log the action
|
|
crud.audit_log.log_action(
|
|
db,
|
|
user_id=current_user.id,
|
|
username=current_user.username,
|
|
action="create",
|
|
resource_type="webhook",
|
|
resource_id=webhook.id,
|
|
details={"name": webhook.name, "url": webhook.url},
|
|
status="success"
|
|
)
|
|
|
|
return {
|
|
"id": webhook.id,
|
|
"name": webhook.name,
|
|
"url": webhook.url,
|
|
"secret": webhook.secret,
|
|
"events": json.loads(webhook.events) if webhook.events else [],
|
|
"is_active": webhook.is_active,
|
|
"retry_count": webhook.retry_count,
|
|
"timeout_seconds": webhook.timeout_seconds,
|
|
"created_by": webhook.created_by,
|
|
"created_at": webhook.created_at,
|
|
"updated_at": webhook.updated_at,
|
|
"last_triggered_at": webhook.last_triggered_at,
|
|
"success_count": webhook.success_count,
|
|
"failure_count": webhook.failure_count,
|
|
}
|
|
|
|
|
|
@router.get("/events", response_model=List[str])
|
|
def list_webhook_events(
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
List all available webhook event types.
|
|
"""
|
|
return WEBHOOK_EVENTS
|
|
|
|
|
|
@router.get("/{webhook_id}", response_model=WebhookSchema)
|
|
def get_webhook(
|
|
webhook_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
Get a specific webhook.
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.get(db, id=webhook_id)
|
|
if not webhook:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found"
|
|
)
|
|
|
|
return {
|
|
"id": webhook.id,
|
|
"name": webhook.name,
|
|
"url": webhook.url,
|
|
"secret": webhook.secret,
|
|
"events": json.loads(webhook.events) if webhook.events else [],
|
|
"is_active": webhook.is_active,
|
|
"retry_count": webhook.retry_count,
|
|
"timeout_seconds": webhook.timeout_seconds,
|
|
"created_by": webhook.created_by,
|
|
"created_at": webhook.created_at,
|
|
"updated_at": webhook.updated_at,
|
|
"last_triggered_at": webhook.last_triggered_at,
|
|
"success_count": webhook.success_count,
|
|
"failure_count": webhook.failure_count,
|
|
}
|
|
|
|
|
|
@router.put("/{webhook_id}", response_model=WebhookSchema)
|
|
def update_webhook(
|
|
webhook_id: str,
|
|
webhook_in: WebhookUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
Update a webhook.
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.get(db, id=webhook_id)
|
|
if not webhook:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found"
|
|
)
|
|
|
|
webhook = crud.webhook.update(db, db_obj=webhook, obj_in=webhook_in)
|
|
|
|
# Log the action
|
|
crud.audit_log.log_action(
|
|
db,
|
|
user_id=current_user.id,
|
|
username=current_user.username,
|
|
action="update",
|
|
resource_type="webhook",
|
|
resource_id=webhook.id,
|
|
details={"name": webhook.name},
|
|
status="success"
|
|
)
|
|
|
|
return {
|
|
"id": webhook.id,
|
|
"name": webhook.name,
|
|
"url": webhook.url,
|
|
"secret": webhook.secret,
|
|
"events": json.loads(webhook.events) if webhook.events else [],
|
|
"is_active": webhook.is_active,
|
|
"retry_count": webhook.retry_count,
|
|
"timeout_seconds": webhook.timeout_seconds,
|
|
"created_by": webhook.created_by,
|
|
"created_at": webhook.created_at,
|
|
"updated_at": webhook.updated_at,
|
|
"last_triggered_at": webhook.last_triggered_at,
|
|
"success_count": webhook.success_count,
|
|
"failure_count": webhook.failure_count,
|
|
}
|
|
|
|
|
|
@router.delete("/{webhook_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_webhook(
|
|
webhook_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
Delete a webhook.
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.get(db, id=webhook_id)
|
|
if not webhook:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found"
|
|
)
|
|
|
|
# Log the action
|
|
crud.audit_log.log_action(
|
|
db,
|
|
user_id=current_user.id,
|
|
username=current_user.username,
|
|
action="delete",
|
|
resource_type="webhook",
|
|
resource_id=webhook_id,
|
|
details={"name": webhook.name},
|
|
status="success"
|
|
)
|
|
|
|
crud.webhook.delete(db, id=webhook_id)
|
|
return None
|
|
|
|
|
|
@router.post("/{webhook_id}/regenerate-secret", response_model=WebhookWithSecret)
|
|
def regenerate_webhook_secret(
|
|
webhook_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
Regenerate the secret for a webhook.
|
|
Returns the new secret (only shown once).
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.get(db, id=webhook_id)
|
|
if not webhook:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found"
|
|
)
|
|
|
|
webhook = crud.webhook.regenerate_secret(db, db_obj=webhook)
|
|
|
|
# Log the action
|
|
crud.audit_log.log_action(
|
|
db,
|
|
user_id=current_user.id,
|
|
username=current_user.username,
|
|
action="regenerate_secret",
|
|
resource_type="webhook",
|
|
resource_id=webhook.id,
|
|
details={"name": webhook.name},
|
|
status="success"
|
|
)
|
|
|
|
return {
|
|
"id": webhook.id,
|
|
"name": webhook.name,
|
|
"url": webhook.url,
|
|
"secret": webhook.secret,
|
|
"events": json.loads(webhook.events) if webhook.events else [],
|
|
"is_active": webhook.is_active,
|
|
"retry_count": webhook.retry_count,
|
|
"timeout_seconds": webhook.timeout_seconds,
|
|
"created_by": webhook.created_by,
|
|
"created_at": webhook.created_at,
|
|
"updated_at": webhook.updated_at,
|
|
"last_triggered_at": webhook.last_triggered_at,
|
|
"success_count": webhook.success_count,
|
|
"failure_count": webhook.failure_count,
|
|
}
|
|
|
|
|
|
@router.post("/{webhook_id}/test", response_model=WebhookDeliverySchema)
|
|
async def test_webhook(
|
|
webhook_id: str,
|
|
test_data: WebhookTest = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
Send a test delivery to a webhook.
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.get(db, id=webhook_id)
|
|
if not webhook:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found"
|
|
)
|
|
|
|
event_type = test_data.event_type if test_data else "test.ping"
|
|
payload = test_data.payload if test_data and test_data.payload else None
|
|
|
|
delivery = await crud.webhook_service.test_webhook(
|
|
db,
|
|
webhook=webhook,
|
|
event_type=event_type,
|
|
payload=payload
|
|
)
|
|
|
|
# Log the action
|
|
crud.audit_log.log_action(
|
|
db,
|
|
user_id=current_user.id,
|
|
username=current_user.username,
|
|
action="test",
|
|
resource_type="webhook",
|
|
resource_id=webhook.id,
|
|
details={"status": delivery.status},
|
|
status="success"
|
|
)
|
|
|
|
return delivery
|
|
|
|
|
|
@router.get("/{webhook_id}/deliveries", response_model=List[WebhookDeliverySchema])
|
|
def list_webhook_deliveries(
|
|
webhook_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
skip: int = 0,
|
|
limit: int = 50
|
|
):
|
|
"""
|
|
List deliveries for a specific webhook.
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.get(db, id=webhook_id)
|
|
if not webhook:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found"
|
|
)
|
|
|
|
return crud.webhook_delivery.get_by_webhook(
|
|
db,
|
|
webhook_id=webhook_id,
|
|
skip=skip,
|
|
limit=limit
|
|
)
|
|
|
|
|
|
@router.post("/{webhook_id}/deliveries/{delivery_id}/retry", response_model=WebhookDeliverySchema)
|
|
async def retry_webhook_delivery(
|
|
webhook_id: str,
|
|
delivery_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
):
|
|
"""
|
|
Retry a failed webhook delivery.
|
|
Requires superuser permissions.
|
|
"""
|
|
webhook = crud.webhook.get(db, id=webhook_id)
|
|
if not webhook:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found"
|
|
)
|
|
|
|
delivery = crud.webhook_delivery.get(db, id=delivery_id)
|
|
if not delivery or delivery.webhook_id != webhook_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Delivery not found"
|
|
)
|
|
|
|
await crud.webhook_service.deliver(db, webhook, delivery)
|
|
|
|
db.refresh(delivery)
|
|
return delivery
|