Add comprehensive backend features and mobile UI improvements
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>
This commit is contained in:
380
backend/app/api/v1/webhooks.py
Normal file
380
backend/app/api/v1/webhooks.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user