mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 13:35:12 +09:00
Introduce programmatic API features and observability: add API Key management, API usage analytics, webhook event subscriptions, and OpenAPI docs. New models (ApiKey, ApiUsageLog, ApiUsageSummary, EventSubscription, EventDelivery), services (api_analytics, api_key, event, openapi), and blueprints (/api-keys, /api-analytics, /event-subscriptions, /docs) were added and registered in create_app. Middleware for API key authentication, buffered analytics logging (with a flush thread), and enhanced rate limiting (custom key function and response headers) were implemented; RBAC was updated to accept API key auth via an auth_required decorator. Background threads for hourly aggregation and event retry were added; security CSP updated to allow resources for Swagger UI. Frontend settings UI and styles were added/updated for API keys and webhooks, and requirements were updated.
231 lines
8.0 KiB
Python
231 lines
8.0 KiB
Python
"""Service for generating OpenAPI 3.0 specification."""
|
|
import re
|
|
from flask import current_app
|
|
|
|
|
|
class OpenAPIService:
|
|
"""Auto-generates OpenAPI 3.0 spec from registered Flask routes."""
|
|
|
|
# Blueprint name to tag mapping
|
|
TAG_MAP = {
|
|
'auth': 'Authentication',
|
|
'apps': 'Applications',
|
|
'domains': 'Domains',
|
|
'docker': 'Docker',
|
|
'databases': 'Databases',
|
|
'system': 'System',
|
|
'processes': 'Processes',
|
|
'logs': 'Logs',
|
|
'nginx': 'Nginx',
|
|
'ssl': 'SSL Certificates',
|
|
'php': 'PHP',
|
|
'wordpress': 'WordPress',
|
|
'wordpress_sites': 'WordPress Sites',
|
|
'python': 'Python',
|
|
'monitoring': 'Monitoring',
|
|
'notifications': 'Notifications',
|
|
'backups': 'Backups',
|
|
'deploy': 'Deployment',
|
|
'builds': 'Builds',
|
|
'templates': 'Templates',
|
|
'files': 'File Manager',
|
|
'ftp': 'FTP Server',
|
|
'firewall': 'Firewall',
|
|
'git': 'Git Server',
|
|
'security': 'Security',
|
|
'cron': 'Cron Jobs',
|
|
'email': 'Email',
|
|
'uptime': 'Uptime',
|
|
'admin': 'Admin',
|
|
'metrics': 'Metrics',
|
|
'workflows': 'Workflows',
|
|
'servers': 'Servers',
|
|
'api_keys': 'API Keys',
|
|
'api_analytics': 'API Analytics',
|
|
'event_subscriptions': 'Event Subscriptions',
|
|
'two_factor': 'Two-Factor Auth',
|
|
'sso': 'SSO / OAuth',
|
|
'migrations': 'Database Migrations',
|
|
'env_vars': 'Environment Variables',
|
|
'private_urls': 'Private URLs',
|
|
}
|
|
|
|
@staticmethod
|
|
def generate_spec():
|
|
"""Generate the full OpenAPI 3.0 spec."""
|
|
app = current_app
|
|
|
|
spec = {
|
|
'openapi': '3.0.3',
|
|
'info': {
|
|
'title': 'ServerKit API',
|
|
'description': 'Server control panel API for managing web applications, databases, Docker containers, and security.',
|
|
'version': '1.0.0',
|
|
'contact': {
|
|
'name': 'ServerKit',
|
|
},
|
|
},
|
|
'servers': [
|
|
{
|
|
'url': '/api/v1',
|
|
'description': 'API v1',
|
|
},
|
|
],
|
|
'components': {
|
|
'securitySchemes': {
|
|
'BearerAuth': {
|
|
'type': 'http',
|
|
'scheme': 'bearer',
|
|
'bearerFormat': 'JWT',
|
|
'description': 'JWT access token',
|
|
},
|
|
'ApiKeyAuth': {
|
|
'type': 'apiKey',
|
|
'in': 'header',
|
|
'name': 'X-API-Key',
|
|
'description': 'API key (sk_...)',
|
|
},
|
|
},
|
|
'schemas': {
|
|
'Error': {
|
|
'type': 'object',
|
|
'properties': {
|
|
'error': {
|
|
'type': 'string',
|
|
'description': 'Error message',
|
|
},
|
|
},
|
|
},
|
|
'Message': {
|
|
'type': 'object',
|
|
'properties': {
|
|
'message': {
|
|
'type': 'string',
|
|
'description': 'Success message',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'responses': {
|
|
'401': {
|
|
'description': 'Unauthorized',
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {'$ref': '#/components/schemas/Error'},
|
|
},
|
|
},
|
|
},
|
|
'403': {
|
|
'description': 'Forbidden',
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {'$ref': '#/components/schemas/Error'},
|
|
},
|
|
},
|
|
},
|
|
'404': {
|
|
'description': 'Not found',
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {'$ref': '#/components/schemas/Error'},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'security': [
|
|
{'BearerAuth': []},
|
|
{'ApiKeyAuth': []},
|
|
],
|
|
'paths': {},
|
|
'tags': [],
|
|
}
|
|
|
|
# Collect tags and paths from registered routes
|
|
tags_seen = set()
|
|
paths = {}
|
|
|
|
for rule in app.url_map.iter_rules():
|
|
# Only include API routes
|
|
if not rule.rule.startswith('/api/v1/'):
|
|
continue
|
|
|
|
# Skip static and internal routes
|
|
if rule.endpoint == 'static' or rule.rule.endswith('/openapi.json'):
|
|
continue
|
|
|
|
# Get blueprint name
|
|
parts = rule.endpoint.split('.')
|
|
bp_name = parts[0] if len(parts) > 1 else None
|
|
|
|
# Convert Flask URL params to OpenAPI format
|
|
path = rule.rule.replace('/api/v1', '')
|
|
path = re.sub(r'<(?:int:|string:|float:)?(\w+)>', r'{\1}', path)
|
|
|
|
# Get tag
|
|
tag = OpenAPIService.TAG_MAP.get(bp_name, bp_name or 'Other')
|
|
if tag not in tags_seen:
|
|
tags_seen.add(tag)
|
|
|
|
# Get view function docstring
|
|
view_func = app.view_functions.get(rule.endpoint)
|
|
description = ''
|
|
if view_func and view_func.__doc__:
|
|
description = view_func.__doc__.strip()
|
|
|
|
# Build path item
|
|
if path not in paths:
|
|
paths[path] = {}
|
|
|
|
methods = [m.lower() for m in rule.methods if m not in ('HEAD', 'OPTIONS')]
|
|
for method in methods:
|
|
operation = {
|
|
'tags': [tag],
|
|
'summary': description or f'{method.upper()} {path}',
|
|
'operationId': rule.endpoint.replace('.', '_'),
|
|
'responses': {
|
|
'200': {
|
|
'description': 'Success',
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {'type': 'object'},
|
|
},
|
|
},
|
|
},
|
|
'401': {'$ref': '#/components/responses/401'},
|
|
'403': {'$ref': '#/components/responses/403'},
|
|
},
|
|
}
|
|
|
|
# Add path parameters
|
|
params = re.findall(r'\{(\w+)\}', path)
|
|
if params:
|
|
operation['parameters'] = []
|
|
for param in params:
|
|
operation['parameters'].append({
|
|
'name': param,
|
|
'in': 'path',
|
|
'required': True,
|
|
'schema': {'type': 'integer' if param.endswith('_id') or param == 'id' else 'string'},
|
|
})
|
|
|
|
# Add request body for POST/PUT/PATCH
|
|
if method in ('post', 'put', 'patch'):
|
|
operation['requestBody'] = {
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {'type': 'object'},
|
|
},
|
|
},
|
|
}
|
|
|
|
if len(methods) > 1:
|
|
operation['operationId'] = f'{rule.endpoint.replace(".", "_")}_{method}'
|
|
|
|
paths[path][method] = operation
|
|
|
|
spec['paths'] = dict(sorted(paths.items()))
|
|
spec['tags'] = [{'name': t} for t in sorted(tags_seen)]
|
|
|
|
return spec
|