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:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

View 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