added webhooks, moved app name and verison to simple config file
This commit is contained in:
500
app/web/api/webhooks.py
Normal file
500
app/web/api/webhooks.py
Normal file
@@ -0,0 +1,500 @@
|
||||
"""
|
||||
Webhooks API blueprint.
|
||||
|
||||
Handles endpoints for managing webhook configurations and viewing delivery logs.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from flask import Blueprint, jsonify, request, current_app
|
||||
|
||||
from web.auth.decorators import api_auth_required
|
||||
from web.models import Webhook, WebhookDeliveryLog, Alert
|
||||
from web.services.webhook_service import WebhookService
|
||||
|
||||
bp = Blueprint('webhooks_api', __name__)
|
||||
|
||||
|
||||
@bp.route('', methods=['GET'])
|
||||
@api_auth_required
|
||||
def list_webhooks():
|
||||
"""
|
||||
List all webhooks with optional filtering.
|
||||
|
||||
Query params:
|
||||
page: Page number (default: 1)
|
||||
per_page: Items per page (default: 20)
|
||||
enabled: Filter by enabled status (true/false)
|
||||
|
||||
Returns:
|
||||
JSON response with webhooks list
|
||||
"""
|
||||
# Get query parameters
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = min(request.args.get('per_page', 20, type=int), 100) # Max 100 items
|
||||
enabled = request.args.get('enabled')
|
||||
|
||||
# Build query
|
||||
query = current_app.db_session.query(Webhook)
|
||||
|
||||
# Apply enabled filter
|
||||
if enabled is not None:
|
||||
enabled_bool = enabled.lower() == 'true'
|
||||
query = query.filter(Webhook.enabled == enabled_bool)
|
||||
|
||||
# Order by name
|
||||
query = query.order_by(Webhook.name)
|
||||
|
||||
# Paginate
|
||||
total = query.count()
|
||||
webhooks = query.offset((page - 1) * per_page).limit(per_page).all()
|
||||
|
||||
# Format response
|
||||
webhooks_data = []
|
||||
|
||||
for webhook in webhooks:
|
||||
# Parse JSON fields
|
||||
alert_types = json.loads(webhook.alert_types) if webhook.alert_types else None
|
||||
severity_filter = json.loads(webhook.severity_filter) if webhook.severity_filter else None
|
||||
custom_headers = json.loads(webhook.custom_headers) if webhook.custom_headers else None
|
||||
|
||||
webhooks_data.append({
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'url': webhook.url,
|
||||
'enabled': webhook.enabled,
|
||||
'auth_type': webhook.auth_type,
|
||||
'auth_token': '***ENCRYPTED***' if webhook.auth_token else None, # Mask sensitive data
|
||||
'custom_headers': custom_headers,
|
||||
'alert_types': alert_types,
|
||||
'severity_filter': severity_filter,
|
||||
'timeout': webhook.timeout,
|
||||
'retry_count': webhook.retry_count,
|
||||
'created_at': webhook.created_at.isoformat() if webhook.created_at else None,
|
||||
'updated_at': webhook.updated_at.isoformat() if webhook.updated_at else None
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'webhooks': webhooks_data,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'pages': (total + per_page - 1) // per_page
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/<int:webhook_id>', methods=['GET'])
|
||||
@api_auth_required
|
||||
def get_webhook(webhook_id):
|
||||
"""
|
||||
Get a specific webhook by ID.
|
||||
|
||||
Args:
|
||||
webhook_id: Webhook ID
|
||||
|
||||
Returns:
|
||||
JSON response with webhook details
|
||||
"""
|
||||
webhook = current_app.db_session.query(Webhook).filter(Webhook.id == webhook_id).first()
|
||||
|
||||
if not webhook:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Webhook {webhook_id} not found'
|
||||
}), 404
|
||||
|
||||
# Parse JSON fields
|
||||
alert_types = json.loads(webhook.alert_types) if webhook.alert_types else None
|
||||
severity_filter = json.loads(webhook.severity_filter) if webhook.severity_filter else None
|
||||
custom_headers = json.loads(webhook.custom_headers) if webhook.custom_headers else None
|
||||
|
||||
return jsonify({
|
||||
'webhook': {
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'url': webhook.url,
|
||||
'enabled': webhook.enabled,
|
||||
'auth_type': webhook.auth_type,
|
||||
'auth_token': '***ENCRYPTED***' if webhook.auth_token else None,
|
||||
'custom_headers': custom_headers,
|
||||
'alert_types': alert_types,
|
||||
'severity_filter': severity_filter,
|
||||
'timeout': webhook.timeout,
|
||||
'retry_count': webhook.retry_count,
|
||||
'created_at': webhook.created_at.isoformat() if webhook.created_at else None,
|
||||
'updated_at': webhook.updated_at.isoformat() if webhook.updated_at else None
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@bp.route('', methods=['POST'])
|
||||
@api_auth_required
|
||||
def create_webhook():
|
||||
"""
|
||||
Create a new webhook.
|
||||
|
||||
Request body:
|
||||
name: Webhook name (required)
|
||||
url: Webhook URL (required)
|
||||
enabled: Whether webhook is enabled (default: true)
|
||||
auth_type: Authentication type (none, bearer, basic, custom)
|
||||
auth_token: Authentication token (encrypted on storage)
|
||||
custom_headers: JSON object with custom headers
|
||||
alert_types: Array of alert types to filter
|
||||
severity_filter: Array of severities to filter
|
||||
timeout: Request timeout in seconds (default: 10)
|
||||
retry_count: Number of retry attempts (default: 3)
|
||||
|
||||
Returns:
|
||||
JSON response with created webhook
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate required fields
|
||||
if not data.get('name'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'name is required'
|
||||
}), 400
|
||||
|
||||
if not data.get('url'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'url is required'
|
||||
}), 400
|
||||
|
||||
# Validate auth_type
|
||||
valid_auth_types = ['none', 'bearer', 'basic', 'custom']
|
||||
auth_type = data.get('auth_type', 'none')
|
||||
if auth_type not in valid_auth_types:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid auth_type. Must be one of: {", ".join(valid_auth_types)}'
|
||||
}), 400
|
||||
|
||||
try:
|
||||
webhook_service = WebhookService(current_app.db_session)
|
||||
|
||||
# Encrypt auth_token if provided
|
||||
auth_token = None
|
||||
if data.get('auth_token'):
|
||||
auth_token = webhook_service._encrypt_value(data['auth_token'])
|
||||
|
||||
# Serialize JSON fields
|
||||
alert_types = json.dumps(data['alert_types']) if data.get('alert_types') else None
|
||||
severity_filter = json.dumps(data['severity_filter']) if data.get('severity_filter') else None
|
||||
custom_headers = json.dumps(data['custom_headers']) if data.get('custom_headers') else None
|
||||
|
||||
# Create webhook
|
||||
webhook = Webhook(
|
||||
name=data['name'],
|
||||
url=data['url'],
|
||||
enabled=data.get('enabled', True),
|
||||
auth_type=auth_type,
|
||||
auth_token=auth_token,
|
||||
custom_headers=custom_headers,
|
||||
alert_types=alert_types,
|
||||
severity_filter=severity_filter,
|
||||
timeout=data.get('timeout', 10),
|
||||
retry_count=data.get('retry_count', 3),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
current_app.db_session.add(webhook)
|
||||
current_app.db_session.commit()
|
||||
|
||||
# Parse for response
|
||||
alert_types_parsed = json.loads(alert_types) if alert_types else None
|
||||
severity_filter_parsed = json.loads(severity_filter) if severity_filter else None
|
||||
custom_headers_parsed = json.loads(custom_headers) if custom_headers else None
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Webhook created successfully',
|
||||
'webhook': {
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'url': webhook.url,
|
||||
'enabled': webhook.enabled,
|
||||
'auth_type': webhook.auth_type,
|
||||
'alert_types': alert_types_parsed,
|
||||
'severity_filter': severity_filter_parsed,
|
||||
'custom_headers': custom_headers_parsed,
|
||||
'timeout': webhook.timeout,
|
||||
'retry_count': webhook.retry_count,
|
||||
'created_at': webhook.created_at.isoformat()
|
||||
}
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
current_app.db_session.rollback()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Failed to create webhook: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:webhook_id>', methods=['PUT'])
|
||||
@api_auth_required
|
||||
def update_webhook(webhook_id):
|
||||
"""
|
||||
Update an existing webhook.
|
||||
|
||||
Args:
|
||||
webhook_id: Webhook ID
|
||||
|
||||
Request body (all optional):
|
||||
name: Webhook name
|
||||
url: Webhook URL
|
||||
enabled: Whether webhook is enabled
|
||||
auth_type: Authentication type
|
||||
auth_token: Authentication token
|
||||
custom_headers: JSON object with custom headers
|
||||
alert_types: Array of alert types
|
||||
severity_filter: Array of severities
|
||||
timeout: Request timeout
|
||||
retry_count: Retry attempts
|
||||
|
||||
Returns:
|
||||
JSON response with update status
|
||||
"""
|
||||
webhook = current_app.db_session.query(Webhook).filter(Webhook.id == webhook_id).first()
|
||||
|
||||
if not webhook:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Webhook {webhook_id} not found'
|
||||
}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate auth_type if provided
|
||||
if 'auth_type' in data:
|
||||
valid_auth_types = ['none', 'bearer', 'basic', 'custom']
|
||||
if data['auth_type'] not in valid_auth_types:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid auth_type. Must be one of: {", ".join(valid_auth_types)}'
|
||||
}), 400
|
||||
|
||||
try:
|
||||
webhook_service = WebhookService(current_app.db_session)
|
||||
|
||||
# Update fields if provided
|
||||
if 'name' in data:
|
||||
webhook.name = data['name']
|
||||
if 'url' in data:
|
||||
webhook.url = data['url']
|
||||
if 'enabled' in data:
|
||||
webhook.enabled = data['enabled']
|
||||
if 'auth_type' in data:
|
||||
webhook.auth_type = data['auth_type']
|
||||
if 'auth_token' in data:
|
||||
# Encrypt new token
|
||||
webhook.auth_token = webhook_service._encrypt_value(data['auth_token'])
|
||||
if 'custom_headers' in data:
|
||||
webhook.custom_headers = json.dumps(data['custom_headers']) if data['custom_headers'] else None
|
||||
if 'alert_types' in data:
|
||||
webhook.alert_types = json.dumps(data['alert_types']) if data['alert_types'] else None
|
||||
if 'severity_filter' in data:
|
||||
webhook.severity_filter = json.dumps(data['severity_filter']) if data['severity_filter'] else None
|
||||
if 'timeout' in data:
|
||||
webhook.timeout = data['timeout']
|
||||
if 'retry_count' in data:
|
||||
webhook.retry_count = data['retry_count']
|
||||
|
||||
webhook.updated_at = datetime.now(timezone.utc)
|
||||
current_app.db_session.commit()
|
||||
|
||||
# Parse for response
|
||||
alert_types = json.loads(webhook.alert_types) if webhook.alert_types else None
|
||||
severity_filter = json.loads(webhook.severity_filter) if webhook.severity_filter else None
|
||||
custom_headers = json.loads(webhook.custom_headers) if webhook.custom_headers else None
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Webhook updated successfully',
|
||||
'webhook': {
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'url': webhook.url,
|
||||
'enabled': webhook.enabled,
|
||||
'auth_type': webhook.auth_type,
|
||||
'alert_types': alert_types,
|
||||
'severity_filter': severity_filter,
|
||||
'custom_headers': custom_headers,
|
||||
'timeout': webhook.timeout,
|
||||
'retry_count': webhook.retry_count,
|
||||
'updated_at': webhook.updated_at.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.db_session.rollback()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Failed to update webhook: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:webhook_id>', methods=['DELETE'])
|
||||
@api_auth_required
|
||||
def delete_webhook(webhook_id):
|
||||
"""
|
||||
Delete a webhook.
|
||||
|
||||
Args:
|
||||
webhook_id: Webhook ID
|
||||
|
||||
Returns:
|
||||
JSON response with deletion status
|
||||
"""
|
||||
webhook = current_app.db_session.query(Webhook).filter(Webhook.id == webhook_id).first()
|
||||
|
||||
if not webhook:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Webhook {webhook_id} not found'
|
||||
}), 404
|
||||
|
||||
try:
|
||||
# Delete webhook (delivery logs will be cascade deleted)
|
||||
current_app.db_session.delete(webhook)
|
||||
current_app.db_session.commit()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': f'Webhook {webhook_id} deleted successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.db_session.rollback()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Failed to delete webhook: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/<int:webhook_id>/test', methods=['POST'])
|
||||
@api_auth_required
|
||||
def test_webhook(webhook_id):
|
||||
"""
|
||||
Send a test payload to a webhook.
|
||||
|
||||
Args:
|
||||
webhook_id: Webhook ID
|
||||
|
||||
Returns:
|
||||
JSON response with test result
|
||||
"""
|
||||
webhook = current_app.db_session.query(Webhook).filter(Webhook.id == webhook_id).first()
|
||||
|
||||
if not webhook:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Webhook {webhook_id} not found'
|
||||
}), 404
|
||||
|
||||
# Test webhook delivery
|
||||
webhook_service = WebhookService(current_app.db_session)
|
||||
result = webhook_service.test_webhook(webhook_id)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success' if result['success'] else 'error',
|
||||
'message': result['message'],
|
||||
'status_code': result['status_code'],
|
||||
'response_body': result.get('response_body')
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/<int:webhook_id>/logs', methods=['GET'])
|
||||
@api_auth_required
|
||||
def get_webhook_logs(webhook_id):
|
||||
"""
|
||||
Get delivery logs for a specific webhook.
|
||||
|
||||
Args:
|
||||
webhook_id: Webhook ID
|
||||
|
||||
Query params:
|
||||
page: Page number (default: 1)
|
||||
per_page: Items per page (default: 20)
|
||||
status: Filter by status (success/failed)
|
||||
|
||||
Returns:
|
||||
JSON response with delivery logs
|
||||
"""
|
||||
webhook = current_app.db_session.query(Webhook).filter(Webhook.id == webhook_id).first()
|
||||
|
||||
if not webhook:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Webhook {webhook_id} not found'
|
||||
}), 404
|
||||
|
||||
# Get query parameters
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = min(request.args.get('per_page', 20, type=int), 100)
|
||||
status_filter = request.args.get('status')
|
||||
|
||||
# Build query
|
||||
query = current_app.db_session.query(WebhookDeliveryLog).filter(
|
||||
WebhookDeliveryLog.webhook_id == webhook_id
|
||||
)
|
||||
|
||||
# Apply status filter
|
||||
if status_filter:
|
||||
query = query.filter(WebhookDeliveryLog.status == status_filter)
|
||||
|
||||
# Order by most recent first
|
||||
query = query.order_by(WebhookDeliveryLog.delivered_at.desc())
|
||||
|
||||
# Paginate
|
||||
total = query.count()
|
||||
logs = query.offset((page - 1) * per_page).limit(per_page).all()
|
||||
|
||||
# Format response
|
||||
logs_data = []
|
||||
for log in logs:
|
||||
# Get alert info
|
||||
alert = current_app.db_session.query(Alert).filter(Alert.id == log.alert_id).first()
|
||||
|
||||
logs_data.append({
|
||||
'id': log.id,
|
||||
'alert_id': log.alert_id,
|
||||
'alert_type': alert.alert_type if alert else None,
|
||||
'alert_message': alert.message if alert else None,
|
||||
'status': log.status,
|
||||
'response_code': log.response_code,
|
||||
'response_body': log.response_body,
|
||||
'error_message': log.error_message,
|
||||
'attempt_number': log.attempt_number,
|
||||
'delivered_at': log.delivered_at.isoformat() if log.delivered_at else None
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'webhook_id': webhook_id,
|
||||
'webhook_name': webhook.name,
|
||||
'logs': logs_data,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'pages': (total + per_page - 1) // per_page
|
||||
})
|
||||
|
||||
|
||||
# Health check endpoint
|
||||
@bp.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""
|
||||
Health check endpoint for monitoring.
|
||||
|
||||
Returns:
|
||||
JSON response with API health status
|
||||
"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'api': 'webhooks',
|
||||
'version': '1.0.0-phase5'
|
||||
})
|
||||
Reference in New Issue
Block a user