Files
pic/api/app.py
T
2025-09-14 03:31:14 -05:00

2006 lines
75 KiB
Python

#!/usr/bin/env python3
"""
Personal Internet Cell API Server
Provides REST API endpoints for managing:
- Cell status and configuration
- Network services (DNS, DHCP, NTP)
- WireGuard VPN and peer management
- Email, Calendar, and File services
- Routing and VPN gateway
- Vault and trust management (Phase 6)
"""
import os
import json
import logging
from datetime import datetime
from flask import Flask, request, jsonify, current_app
from flask_cors import CORS
import threading
import time
from collections import deque
import json as pyjson
from logging.handlers import RotatingFileHandler
import uuid
import contextvars
# Track API start time for uptime calculation
API_START_TIME = time.time()
from network_manager import NetworkManager
from wireguard_manager import WireGuardManager
from peer_registry import PeerRegistry
from email_manager import EmailManager
from calendar_manager import CalendarManager
from file_manager import FileManager
from routing_manager import RoutingManager
from cell_manager import CellManager
from vault_manager import VaultManager
from container_manager import ContainerManager
from config_manager import ConfigManager
from service_bus import ServiceBus, EventType
from log_manager import LogManager
# Context variable for request info
request_context = contextvars.ContextVar('request_context', default={})
# Set default log level and log file if not already defined
LOG_LEVEL = globals().get('LOG_LEVEL', 'INFO')
LOG_FILE = globals().get('LOG_FILE', 'picell.log')
class ContextFilter(logging.Filter):
def filter(self, record):
ctx = request_context.get({})
for k, v in ctx.items():
setattr(record, k, v)
return True
class JsonFormatter(logging.Formatter):
def format(self, record):
log_record = {
'timestamp': self.formatTime(record, self.datefmt),
'level': record.levelname,
'name': record.name,
'message': record.getMessage(),
'request_id': getattr(record, 'request_id', None),
'client_ip': getattr(record, 'client_ip', None),
'method': getattr(record, 'method', None),
'path': getattr(record, 'path', None),
'status': getattr(record, 'status', None),
'user': getattr(record, 'user', None),
}
if record.exc_info:
log_record['exception'] = self.formatException(record.exc_info)
return pyjson.dumps({k: v for k, v in log_record.items() if v is not None})
json_formatter = JsonFormatter()
context_filter = ContextFilter()
handlers = [logging.StreamHandler()]
try:
file_handler = RotatingFileHandler(LOG_FILE, maxBytes=5_000_000, backupCount=5, encoding='utf-8')
file_handler.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
file_handler.setFormatter(json_formatter)
file_handler.addFilter(context_filter)
handlers.append(file_handler)
except Exception as e:
print(f"Warning: Could not create rotating log file handler: {e}")
for h in handlers:
h.setFormatter(json_formatter)
h.addFilter(context_filter)
logging.basicConfig(
level=getattr(logging, LOG_LEVEL, logging.INFO),
handlers=handlers
)
logger = logging.getLogger('picell')
# Flask app setup
app = Flask(__name__)
CORS(app)
# Development mode flag
app.config['DEVELOPMENT_MODE'] = True # Set to True for development, False for production
# Initialize enhanced components
config_manager = ConfigManager(config_file='./config/cell_config.json', data_dir='./data')
service_bus = ServiceBus()
log_manager = LogManager(log_dir='./data/logs')
# Initialize service loggers
service_log_configs = {
'network': {'level': 'INFO', 'formatter': 'json', 'console': False},
'wireguard': {'level': 'INFO', 'formatter': 'json', 'console': False},
'email': {'level': 'INFO', 'formatter': 'json', 'console': False},
'calendar': {'level': 'INFO', 'formatter': 'json', 'console': False},
'files': {'level': 'INFO', 'formatter': 'json', 'console': False},
'routing': {'level': 'INFO', 'formatter': 'json', 'console': False},
'vault': {'level': 'INFO', 'formatter': 'json', 'console': False},
'api': {'level': 'INFO', 'formatter': 'json', 'console': True}
}
for service, config in service_log_configs.items():
log_manager.add_service_logger(service, config)
# Start service bus
service_bus.start()
@app.before_request
def enrich_log_context():
req_id = str(uuid.uuid4())
client_ip = request.remote_addr
method = request.method
path = request.path
user = getattr(getattr(request, 'user', None), 'id', None) or 'anonymous'
request_context.set({
'request_id': req_id,
'client_ip': client_ip,
'method': method,
'path': path,
'user': user
})
@app.after_request
def log_request(response):
ctx = request_context.get({})
ctx['status'] = response.status_code
logger.info(f"{ctx.get('method')} {ctx.get('path')} {ctx.get('status')}")
return response
@app.teardown_request
def clear_log_context(exc):
request_context.set({})
# Initialize managers with proper directories
network_manager = NetworkManager(data_dir='/app/data', config_dir='/app/config')
wireguard_manager = WireGuardManager(data_dir='/app/data', config_dir='/app/config')
peer_registry = PeerRegistry(data_dir='/app/data', config_dir='/app/config')
email_manager = EmailManager(data_dir='/app/data', config_dir='/app/config')
calendar_manager = CalendarManager(data_dir='/app/data', config_dir='/app/config')
file_manager = FileManager(data_dir='/app/data', config_dir='/app/config')
routing_manager = RoutingManager(data_dir='/app/data', config_dir='/app/config')
cell_manager = CellManager(data_dir='/app/data', config_dir='/app/config')
app.vault_manager = VaultManager(data_dir='/app/data', config_dir='/app/config')
container_manager = ContainerManager(data_dir='/app/data', config_dir='/app/config')
# Register services with service bus
service_bus.register_service('network', network_manager)
service_bus.register_service('wireguard', wireguard_manager)
service_bus.register_service('email', email_manager)
service_bus.register_service('calendar', calendar_manager)
service_bus.register_service('files', file_manager)
service_bus.register_service('routing', routing_manager)
service_bus.register_service('vault', app.vault_manager)
service_bus.register_service('container', container_manager)
# Unified health monitoring
HEALTH_HISTORY_SIZE = 100
health_history = deque(maxlen=HEALTH_HISTORY_SIZE)
health_monitor_running = True
# Health alerting configuration
HEALTH_ALERT_THRESHOLD = 3 # Number of consecutive failures before alert
service_alert_counters = {}
def perform_health_check():
"""Perform a unified health check of all services, with alerting."""
try:
# Use service bus to get health from all services
result = {
'timestamp': datetime.utcnow().isoformat(),
'alerts': []
}
# Get health from each service
for service_name in service_bus.list_services():
try:
service = service_bus.get_service(service_name)
if hasattr(service, 'health_check'):
health = service.health_check()
else:
health = service.get_status()
result[service_name] = health
except Exception as e:
result[service_name] = {'error': str(e), 'status': 'offline'}
# Health alerting logic - improved to be more robust
global service_alert_counters
for service_name in service_bus.list_services():
if service_name in result:
status = result[service_name]
healthy = True
# Improved health determination logic
if isinstance(status, dict):
# Check for explicit healthy field first
if 'healthy' in status:
healthy = status['healthy']
# Check for running status
elif 'running' in status:
healthy = status['running']
# Check for status field with various healthy values
elif 'status' in status:
status_value = status['status']
if isinstance(status_value, str):
healthy = status_value.lower() in ('ok', 'healthy', 'online', 'active')
else:
healthy = bool(status_value)
# Check for error field
elif 'error' in status:
healthy = False
# If no health indicators, assume healthy if service exists
else:
healthy = True
else:
# If status is not a dict, assume it's a boolean
healthy = bool(status)
# Only count as unhealthy if we're certain it's down
if not healthy:
service_alert_counters[service_name] = service_alert_counters.get(service_name, 0) + 1
if service_alert_counters[service_name] >= HEALTH_ALERT_THRESHOLD:
alert_msg = f"ALERT: {service_name} unhealthy for {service_alert_counters[service_name]} consecutive checks."
logger.warning(alert_msg)
result['alerts'].append(alert_msg)
# Publish alert event
service_bus.publish_event(EventType.ERROR_OCCURRED, service_name, {
'error': alert_msg,
'service': service_name,
'consecutive_failures': service_alert_counters[service_name]
})
else:
# Reset counter if service is healthy
if service_alert_counters.get(service_name, 0) > 0:
logger.info(f"Service {service_name} recovered, resetting alert counter")
service_alert_counters[service_name] = 0
logger.info(f"Unified health check: {result}")
return result
except Exception as e:
logger.error(f"Unified health check failed: {e}")
return {'error': str(e), 'timestamp': datetime.utcnow().isoformat()}
def health_monitor_loop():
while health_monitor_running:
with app.app_context():
health_result = perform_health_check()
health_history.appendleft(health_result)
# Publish health check event
service_bus.publish_event(EventType.HEALTH_CHECK, 'api', health_result)
time.sleep(60) # Check every 60 seconds
# Start health monitor thread
health_monitor_thread = threading.Thread(target=health_monitor_loop, daemon=True)
health_monitor_thread.start()
def is_local_request():
# Allow requests from localhost, Docker networks, and internal IPs
remote_addr = request.remote_addr
forwarded_for = request.headers.get('X-Forwarded-For', '')
# Check direct remote address
if remote_addr in ('127.0.0.1', '::1', 'localhost'):
return True
# Check forwarded address (for reverse proxy scenarios)
if forwarded_for:
forwarded_ips = [ip.strip() for ip in forwarded_for.split(',')]
for ip in forwarded_ips:
if ip in ('127.0.0.1', '::1', 'localhost'):
return True
# Allow Docker internal networks (172.x.x.x, 192.168.x.x, 10.x.x.x)
if remote_addr:
try:
import ipaddress
ip = ipaddress.ip_address(remote_addr)
if ip.is_private or ip.is_loopback:
return True
except:
pass
return False
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint."""
try:
return jsonify({
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"version": "1.0.0"
})
except Exception as e:
logger.error(f"Health check failed: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/status', methods=['GET'])
def get_cell_status():
"""Get overall cell status."""
try:
# Use service bus to get status from all services
services_status = {}
for service_name in service_bus.list_services():
try:
service = service_bus.get_service(service_name)
services_status[service_name] = service.get_status()
except Exception as e:
services_status[service_name] = {'error': str(e)}
peers = peer_registry.list_peers()
# Calculate actual uptime
current_time = time.time()
uptime_seconds = int(current_time - API_START_TIME)
return jsonify({
"cell_name": "personal-internet-cell",
"domain": "cell.local",
"uptime": uptime_seconds,
"peers_count": len(peers),
"services": services_status,
"timestamp": datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error getting cell status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/config', methods=['GET'])
def get_config():
"""Get cell configuration."""
try:
return jsonify(config_manager.get_all_configs())
except Exception as e:
logger.error(f"Error getting config: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/config', methods=['PUT'])
def update_config():
"""Update cell configuration."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
# Update configuration using config manager
for service, config in data.items():
if service in config_manager.service_schemas:
success = config_manager.update_service_config(service, config)
if success:
# Publish config change event
service_bus.publish_event(EventType.CONFIG_CHANGED, service, {
'service': service,
'config': config
})
logger.info(f"Updated config: {data}")
return jsonify({"message": "Configuration updated successfully"})
except Exception as e:
logger.error(f"Error updating config: {e}")
return jsonify({"error": str(e)}), 500
# Configuration management endpoints
@app.route('/api/config/backup', methods=['POST'])
def create_config_backup():
"""Create configuration backup."""
try:
backup_id = config_manager.backup_config()
service_bus.publish_event(EventType.BACKUP_CREATED, 'api', {
'backup_id': backup_id,
'timestamp': datetime.utcnow().isoformat()
})
return jsonify({"backup_id": backup_id})
except Exception as e:
logger.error(f"Error creating backup: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/config/backups', methods=['GET'])
def list_config_backups():
"""List available backups."""
try:
backups = config_manager.list_backups()
return jsonify(backups)
except Exception as e:
logger.error(f"Error listing backups: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/config/restore/<backup_id>', methods=['POST'])
def restore_config(backup_id):
"""Restore configuration from backup."""
try:
success = config_manager.restore_config(backup_id)
if success:
service_bus.publish_event(EventType.RESTORE_COMPLETED, 'api', {
'backup_id': backup_id,
'timestamp': datetime.utcnow().isoformat()
})
return jsonify({"message": f"Configuration restored from backup: {backup_id}"})
else:
return jsonify({"error": f"Failed to restore backup: {backup_id}"}), 500
except Exception as e:
logger.error(f"Error restoring backup: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/config/export', methods=['GET'])
def export_config():
"""Export configuration."""
try:
format = request.args.get('format', 'json')
config_data = config_manager.export_config(format)
return jsonify({"config": config_data, "format": format})
except Exception as e:
logger.error(f"Error exporting config: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/config/import', methods=['POST'])
def import_config():
"""Import configuration."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
config_data = data.get('config')
format = data.get('format', 'json')
success = config_manager.import_config(config_data, format)
if success:
return jsonify({"message": "Configuration imported successfully"})
else:
return jsonify({"error": "Failed to import configuration"}), 500
except Exception as e:
logger.error(f"Error importing config: {e}")
return jsonify({"error": str(e)}), 500
# Service bus endpoints
@app.route('/api/services/bus/status', methods=['GET'])
def get_service_bus_status():
"""Get service bus status."""
try:
return jsonify(service_bus.get_service_status_summary())
except Exception as e:
logger.error(f"Error getting service bus status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/services/bus/events', methods=['GET'])
def get_service_bus_events():
"""Get service bus event history."""
try:
event_type = request.args.get('type')
source = request.args.get('source')
limit = int(request.args.get('limit', 100))
events = service_bus.get_event_history(
EventType(event_type) if event_type else None,
source,
limit
)
# Convert events to serializable format
serializable_events = []
for event in events:
serializable_events.append({
'event_id': event.event_id,
'event_type': event.event_type.value,
'source': event.source,
'data': event.data,
'timestamp': event.timestamp.isoformat()
})
return jsonify(serializable_events)
except Exception as e:
logger.error(f"Error getting service bus events: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/services/bus/services/<service_name>/start', methods=['POST'])
def start_service(service_name):
"""Start a service with orchestration."""
try:
success = service_bus.orchestrate_service_start(service_name)
if success:
return jsonify({"message": f"Service {service_name} started successfully"})
else:
return jsonify({"error": f"Failed to start service {service_name}"}), 500
except Exception as e:
logger.error(f"Error starting service {service_name}: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/services/bus/services/<service_name>/stop', methods=['POST'])
def stop_service(service_name):
"""Stop a service with orchestration."""
try:
success = service_bus.orchestrate_service_stop(service_name)
if success:
return jsonify({"message": f"Service {service_name} stopped successfully"})
else:
return jsonify({"error": f"Failed to stop service {service_name}"}), 500
except Exception as e:
logger.error(f"Error stopping service {service_name}: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/services/bus/services/<service_name>/restart', methods=['POST'])
def restart_service(service_name):
"""Restart a service with orchestration."""
try:
success = service_bus.orchestrate_service_restart(service_name)
if success:
return jsonify({"message": f"Service {service_name} restarted successfully"})
else:
return jsonify({"error": f"Failed to restart service {service_name}"}), 500
except Exception as e:
logger.error(f"Error restarting service {service_name}: {e}")
return jsonify({"error": str(e)}), 500
# Logging endpoints
@app.route('/api/logs/services/<service>', methods=['GET'])
def get_service_logs(service):
"""Get logs for a specific service."""
try:
level = request.args.get('level', 'INFO')
lines = int(request.args.get('lines', 50))
logs = log_manager.get_service_logs(service, level, lines)
return jsonify({"service": service, "logs": logs})
except Exception as e:
logger.error(f"Error getting logs for {service}: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/logs/search', methods=['POST'])
def search_logs():
"""Search logs across all services."""
try:
data = request.get_json(silent=True) or {}
query = data.get('query', '')
services = data.get('services')
level = data.get('level')
time_range = data.get('time_range')
results = log_manager.search_logs(query, time_range, services, level)
return jsonify({"results": results, "count": len(results)})
except Exception as e:
logger.error(f"Error searching logs: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/logs/export', methods=['POST'])
def export_logs():
"""Export logs in specified format."""
try:
data = request.get_json(silent=True) or {}
format = data.get('format', 'json')
filters = data.get('filters', {})
log_data = log_manager.export_logs(format, filters)
return jsonify({"logs": log_data, "format": format})
except Exception as e:
logger.error(f"Error exporting logs: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/logs/statistics', methods=['GET'])
def get_log_statistics():
"""Get log statistics."""
try:
service = request.args.get('service')
stats = log_manager.get_log_statistics(service)
return jsonify(stats)
except Exception as e:
logger.error(f"Error getting log statistics: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/logs/rotate', methods=['POST'])
def rotate_logs():
"""Manually rotate logs."""
try:
data = request.get_json(silent=True) or {}
service = data.get('service')
log_manager.rotate_logs(service)
return jsonify({"message": "Logs rotated successfully"})
except Exception as e:
logger.error(f"Error rotating logs: {e}")
return jsonify({"error": str(e)}), 500
# Network Services API
@app.route('/api/dns/records', methods=['GET'])
def get_dns_records():
"""Get DNS records."""
try:
records = network_manager.get_dns_records()
return jsonify(records)
except Exception as e:
logger.error(f"Error getting DNS records: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/dns/records', methods=['POST'])
def add_dns_record():
"""Add DNS record."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = network_manager.add_dns_record(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error adding DNS record: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/dns/records', methods=['DELETE'])
def remove_dns_record():
"""Remove DNS record."""
try:
data = request.get_json(silent=True)
result = network_manager.remove_dns_record(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error removing DNS record: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/dhcp/leases', methods=['GET'])
def get_dhcp_leases():
"""Get DHCP leases."""
try:
leases = network_manager.get_dhcp_leases()
return jsonify(leases)
except Exception as e:
logger.error(f"Error getting DHCP leases: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/dhcp/reservations', methods=['POST'])
def add_dhcp_reservation():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = network_manager.add_dhcp_reservation(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error adding DHCP reservation: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/dhcp/reservations', methods=['DELETE'])
def remove_dhcp_reservation():
"""Remove DHCP reservation."""
try:
data = request.get_json(silent=True)
result = network_manager.remove_dhcp_reservation(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error removing DHCP reservation: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/ntp/status', methods=['GET'])
def get_ntp_status():
"""Get NTP status."""
try:
status = network_manager.get_ntp_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting NTP status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/network/info', methods=['GET'])
def get_network_info():
"""Get general network info (interfaces, gateway, DNS, etc.)"""
try:
info = network_manager.get_network_info()
return jsonify(info)
except Exception as e:
logger.error(f"Error getting network info: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/dns/status', methods=['GET'])
def get_dns_status():
"""Get DNS service status and summary info."""
try:
status = network_manager.get_dns_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting DNS status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/network/test', methods=['POST'])
def test_network():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = network_manager.test_connectivity(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error testing network: {e}")
return jsonify({"error": str(e)}), 500
# WireGuard API
@app.route('/api/wireguard/keys', methods=['GET'])
def get_wireguard_keys():
"""Get WireGuard keys."""
try:
# For now, return empty keys - this would need to be implemented
return jsonify({"error": "Not implemented yet"}), 501
except Exception as e:
logger.error(f"Error getting WireGuard keys: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/keys/peer', methods=['POST'])
def generate_peer_keys():
"""Generate peer keys."""
try:
data = request.get_json(silent=True)
if data is None or 'peer_name' not in data:
return jsonify({"error": "Missing peer_name"}), 400
result = wireguard_manager.generate_peer_keys(data['peer_name'])
return jsonify(result)
except Exception as e:
logger.error(f"Error generating peer keys: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/config', methods=['GET'])
def get_wireguard_config():
"""Get WireGuard configuration."""
try:
# For now, return empty config - this would need to be implemented
return jsonify({"error": "Not implemented yet"}), 501
except Exception as e:
logger.error(f"Error getting WireGuard config: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/peers', methods=['GET'])
def get_wireguard_peers():
"""Get WireGuard peers."""
try:
peers = wireguard_manager.get_wireguard_peers()
return jsonify(peers)
except Exception as e:
logger.error(f"Error getting WireGuard peers: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/peers', methods=['POST'])
def add_wireguard_peer():
"""Add WireGuard peer."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
required_fields = ['name', 'public_key', 'allowed_ips']
for field in required_fields:
if field not in data:
return jsonify({"error": f"Missing required field: {field}"}), 400
result = wireguard_manager.add_wireguard_peer(
name=data['name'],
public_key=data['public_key'],
allowed_ips=data['allowed_ips'],
endpoint=data.get('endpoint', ''),
persistent_keepalive=data.get('persistent_keepalive', 25)
)
return jsonify({"success": result})
except Exception as e:
logger.error(f"Error adding WireGuard peer: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/peers', methods=['DELETE'])
def remove_wireguard_peer():
"""Remove WireGuard peer."""
try:
data = request.get_json(silent=True)
if data is None or 'name' not in data:
return jsonify({"error": "Missing peer name"}), 400
result = wireguard_manager.remove_wireguard_peer(data['name'])
return jsonify({"success": result})
except Exception as e:
logger.error(f"Error removing WireGuard peer: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/status', methods=['GET'])
def get_wireguard_status():
"""Get WireGuard status."""
try:
status = wireguard_manager.get_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting WireGuard status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/connectivity', methods=['POST'])
def test_wireguard_connectivity():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = wireguard_manager.test_connectivity(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error testing WireGuard connectivity: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/peers/ip', methods=['PUT'])
def update_peer_ip():
"""Update peer IP."""
try:
data = request.get_json(silent=True)
if data is None or 'name' not in data or 'ip' not in data:
return jsonify({"error": "Missing peer name or IP"}), 400
# For now, return not implemented - this would need to be implemented
return jsonify({"error": "Not implemented yet"}), 501
except Exception as e:
logger.error(f"Error updating peer IP: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/peers/status', methods=['POST'])
def get_peer_status():
"""Get WireGuard peer status."""
try:
data = request.get_json(silent=True)
if data is None or 'public_key' not in data:
return jsonify({"error": "Missing public key"}), 400
public_key = data['public_key']
status = wireguard_manager.get_peer_status(public_key)
return jsonify(status)
except Exception as e:
logger.error(f"Error getting peer status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/network/setup', methods=['POST'])
def setup_network():
"""Setup network configuration for internet access."""
try:
success = wireguard_manager.setup_network_configuration()
if success:
return jsonify({"message": "Network configuration setup completed successfully"})
else:
return jsonify({"error": "Failed to setup network configuration"}), 500
except Exception as e:
logger.error(f"Error setting up network configuration: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/network/status', methods=['GET'])
def get_network_status():
"""Get network configuration status."""
try:
status = wireguard_manager.get_network_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting network status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/peers/config', methods=['POST'])
def get_peer_config():
try:
data = request.get_json(silent=True)
if data is None or 'name' not in data:
return jsonify({"error": "Missing peer name"}), 400
peer_name = data['name']
# Get peer from peer registry
peer = peer_registry.get_peer(peer_name)
if not peer:
return jsonify({"config": "Peer not found"})
# Get server configuration
server_config = wireguard_manager.get_server_config()
# Check if IP already has a subnet mask, if not add /32
peer_ip = peer.get('ip', '10.0.0.2')
peer_address = peer_ip if '/' in peer_ip else f"{peer_ip}/32"
# Generate client configuration using peer registry data
config = f"""[Interface]
PrivateKey = {peer.get('private_key', 'YOUR_PRIVATE_KEY_HERE')}
Address = {peer_address}
DNS = 8.8.8.8, 1.1.1.1
[Peer]
PublicKey = {server_config.get('public_key', 'SERVER_PUBLIC_KEY_PLACEHOLDER')}
Endpoint = {server_config.get('endpoint', 'YOUR_SERVER_IP:51820')}
AllowedIPs = {peer.get('allowed_ips', '0.0.0.0/0')}
PersistentKeepalive = {peer.get('persistent_keepalive', 25)}"""
return jsonify({"config": config})
except Exception as e:
logger.error(f"Error getting peer config: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wireguard/server-config', methods=['GET'])
def get_server_config():
try:
# Get server configuration from WireGuard manager
config = wireguard_manager.get_server_config()
return jsonify(config)
except Exception as e:
logger.error(f"Error getting server config: {e}")
return jsonify({"error": str(e)}), 500
# Peer Registry API
@app.route('/api/peers', methods=['GET'])
def get_peers():
"""Get all peers."""
try:
peers = peer_registry.list_peers()
return jsonify(peers)
except Exception as e:
logger.error(f"Error getting peers: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/peers', methods=['POST'])
def add_peer():
"""Add a peer."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
# Validate required fields
required_fields = ['name', 'ip', 'public_key']
for field in required_fields:
if field not in data:
return jsonify({"error": f"Missing required field: {field}"}), 400
# Add peer to registry with all provided fields
peer_info = {
'peer': data['name'],
'ip': data['ip'],
'public_key': data['public_key'],
'private_key': data.get('private_key'),
'server_public_key': data.get('server_public_key'),
'server_endpoint': data.get('server_endpoint'),
'allowed_ips': data.get('allowed_ips'),
'persistent_keepalive': data.get('persistent_keepalive'),
'description': data.get('description')
}
success = peer_registry.add_peer(peer_info)
if success:
return jsonify({"message": f"Peer {data['name']} added successfully"}), 201
else:
return jsonify({"error": f"Peer {data['name']} already exists"}), 400
except Exception as e:
logger.error(f"Error adding peer: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/peers/<peer_name>', methods=['DELETE'])
def remove_peer(peer_name):
"""Remove a peer."""
try:
success = peer_registry.remove_peer(peer_name)
if success:
return jsonify({"message": f"Peer {peer_name} removed successfully"})
else:
return jsonify({"message": f"Peer {peer_name} not found or already removed"})
except Exception as e:
logger.error(f"Error removing peer: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/peers/register', methods=['POST'])
def register_peer():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = peer_registry.register_peer(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error registering peer: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/peers/<peer_name>/unregister', methods=['DELETE'])
def unregister_peer(peer_name):
"""Unregister a peer."""
try:
result = peer_registry.unregister_peer(peer_name)
return jsonify(result)
except Exception as e:
logger.error(f"Error unregistering peer: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/peers/<peer_name>/update-ip', methods=['PUT'])
def update_peer_ip_registry(peer_name):
"""Update peer IP."""
try:
data = request.get_json(silent=True)
new_ip = data.get('ip') if data else None
if not new_ip:
return jsonify({"error": "Missing ip"}), 400
success = peer_registry.update_peer_ip(peer_name, new_ip)
if success:
# Update routing and WireGuard configs
try:
routing_manager.update_peer_ip(peer_name, new_ip)
except Exception as e:
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
try:
# For now, skip WireGuard update - method not implemented
logger.warning(f"WireGuardManager update_peer_ip not implemented yet")
except Exception as e:
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
return jsonify({"message": f"IP update received for {peer_name}"})
else:
return jsonify({"error": f"Peer {peer_name} not found"}), 404
except Exception as e:
logger.error(f"Error updating peer IP: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/ip-update', methods=['POST'])
def ip_update():
"""Handle IP update from peer."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
peer_name = data.get('peer')
new_ip = data.get('ip')
if not peer_name or not new_ip:
return jsonify({"error": "Missing peer or ip"}), 400
success = peer_registry.update_peer_ip(peer_name, new_ip)
if success:
# Update routing and WireGuard configs
try:
routing_manager.update_peer_ip(peer_name, new_ip)
except Exception as e:
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
try:
# For now, skip WireGuard update - method not implemented
logger.warning(f"WireGuardManager update_peer_ip not implemented yet")
except Exception as e:
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
return jsonify({"message": f"IP update received for {peer_name}"})
else:
return jsonify({"error": f"Peer {peer_name} not found"}), 404
except Exception as e:
logger.error(f"Error handling IP update: {e}")
return jsonify({"error": str(e)}), 500
# Email Services API
@app.route('/api/email/users', methods=['GET'])
def get_email_users():
"""Get email users."""
try:
users = email_manager.get_users()
return jsonify(users)
except Exception as e:
logger.error(f"Error getting email users: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/email/users', methods=['POST'])
def create_email_user():
"""Create email user."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = email_manager.create_user(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error creating email user: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/email/users/<username>', methods=['DELETE'])
def delete_email_user(username):
"""Delete email user."""
try:
result = email_manager.delete_user(username)
return jsonify(result)
except Exception as e:
logger.error(f"Error deleting email user: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/email/status', methods=['GET'])
def get_email_status():
"""Get email service status."""
try:
status = email_manager.get_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting email status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/email/connectivity', methods=['GET'])
def test_email_connectivity():
"""Test email connectivity."""
try:
result = email_manager.test_connectivity()
return jsonify(result)
except Exception as e:
logger.error(f"Error testing email connectivity: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/email/send', methods=['POST'])
def send_email():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = email_manager.send_email(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error sending email: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/email/mailbox/<username>', methods=['GET'])
def get_mailbox_info(username):
"""Get mailbox information."""
try:
result = email_manager.get_mailbox_info(username)
return jsonify(result)
except Exception as e:
logger.error(f"Error getting mailbox info: {e}")
return jsonify({"error": str(e)}), 500
# Calendar Services API
@app.route('/api/calendar/users', methods=['GET'])
def get_calendar_users():
"""Get calendar users."""
try:
users = calendar_manager.get_users()
return jsonify(users)
except Exception as e:
logger.error(f"Error getting calendar users: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/calendar/users', methods=['POST'])
def create_calendar_user():
"""Create calendar user."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = calendar_manager.create_user(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error creating calendar user: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/calendar/users/<username>', methods=['DELETE'])
def delete_calendar_user(username):
"""Delete calendar user."""
try:
result = calendar_manager.delete_user(username)
return jsonify(result)
except Exception as e:
logger.error(f"Error deleting calendar user: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/calendar/calendars', methods=['POST'])
def create_calendar():
"""Create calendar."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = calendar_manager.create_calendar(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error creating calendar: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/calendar/events', methods=['POST'])
def add_calendar_event():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = calendar_manager.add_event(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error adding calendar event: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/calendar/events/<username>/<calendar_name>', methods=['GET'])
def get_calendar_events(username, calendar_name):
"""Get calendar events."""
try:
params = request.args.to_dict()
result = calendar_manager.get_events(username, calendar_name, params)
return jsonify(result)
except Exception as e:
logger.error(f"Error getting calendar events: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/calendar/status', methods=['GET'])
def get_calendar_status():
"""Get calendar service status."""
try:
status = calendar_manager.get_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting calendar status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/calendar/connectivity', methods=['GET'])
def test_calendar_connectivity():
"""Test calendar connectivity."""
try:
result = calendar_manager.test_connectivity()
return jsonify(result)
except Exception as e:
logger.error(f"Error testing calendar connectivity: {e}")
return jsonify({"error": str(e)}), 500
# File Services API
@app.route('/api/files/users', methods=['GET'])
def get_file_users():
"""Get file storage users."""
try:
users = file_manager.get_users()
return jsonify(users)
except Exception as e:
logger.error(f"Error getting file users: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/users', methods=['POST'])
def create_file_user():
"""Create file storage user."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = file_manager.create_user(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error creating file user: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/users/<username>', methods=['DELETE'])
def delete_file_user(username):
"""Delete file storage user."""
try:
result = file_manager.delete_user(username)
return jsonify(result)
except Exception as e:
logger.error(f"Error deleting file user: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/folders', methods=['POST'])
def create_folder():
"""Create folder."""
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = file_manager.create_folder(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error creating folder: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/folders/<username>/<path:folder_path>', methods=['DELETE'])
def delete_folder(username, folder_path):
"""Delete folder."""
try:
result = file_manager.delete_folder(username, folder_path)
return jsonify(result)
except Exception as e:
logger.error(f"Error deleting folder: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/upload/<username>', methods=['POST'])
def upload_file(username):
"""Upload file."""
try:
if 'file' not in request.files:
return jsonify({"error": "No file provided"}), 400
file = request.files['file']
path = request.form.get('path', '')
result = file_manager.upload_file(username, file, path)
return jsonify(result)
except Exception as e:
logger.error(f"Error uploading file: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/download/<username>/<path:file_path>', methods=['GET'])
def download_file(username, file_path):
"""Download file."""
try:
result = file_manager.download_file(username, file_path)
return jsonify(result)
except Exception as e:
logger.error(f"Error downloading file: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/delete/<username>/<path:file_path>', methods=['DELETE'])
def delete_file(username, file_path):
"""Delete file."""
try:
result = file_manager.delete_file(username, file_path)
return jsonify(result)
except Exception as e:
logger.error(f"Error deleting file: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/list/<username>', methods=['GET'])
def list_files(username):
"""List files."""
try:
folder = request.args.get('folder', '')
result = file_manager.list_files(username, folder)
return jsonify(result)
except Exception as e:
logger.error(f"Error listing files: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/status', methods=['GET'])
def get_file_status():
"""Get file service status."""
try:
status = file_manager.get_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting file status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/files/connectivity', methods=['GET'])
def test_file_connectivity():
"""Test file service connectivity."""
try:
result = file_manager.test_connectivity()
return jsonify(result)
except Exception as e:
logger.error(f"Error testing file connectivity: {e}")
return jsonify({"error": str(e)}), 500
# Routing API
@app.route('/api/routing/status', methods=['GET'])
def get_routing_status():
"""Get routing status."""
try:
status = routing_manager.get_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting routing status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/nat', methods=['POST'])
def add_nat_rule():
"""Add NAT rule.
JSON fields:
- source_network (CIDR)
- target_interface (str)
- masquerade (bool, default True)
- nat_type (MASQUERADE, SNAT, DNAT)
- protocol (TCP, UDP, ALL)
- external_port (str, optional)
- internal_ip (str, optional)
- internal_port (str, optional)
"""
try:
data = request.get_json(silent=True) or {}
result = routing_manager.add_nat_rule(
source_network=data.get('source_network'),
target_interface=data.get('target_interface'),
masquerade=data.get('masquerade', True),
nat_type=data.get('nat_type', 'MASQUERADE'),
protocol=data.get('protocol', 'ALL'),
external_port=data.get('external_port'),
internal_ip=data.get('internal_ip'),
internal_port=data.get('internal_port')
)
return jsonify({'success': result})
except Exception as e:
logger.error(f"Error adding NAT rule: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/nat/<rule_id>', methods=['DELETE'])
def remove_nat_rule(rule_id):
"""Remove NAT rule."""
try:
result = routing_manager.remove_nat_rule(rule_id)
return jsonify(result)
except Exception as e:
logger.error(f"Error removing NAT rule: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/peers', methods=['POST'])
def add_peer_route():
"""Add peer route."""
try:
data = request.get_json(silent=True)
result = routing_manager.add_peer_route(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error adding peer route: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/peers/<peer_name>', methods=['DELETE'])
def remove_peer_route(peer_name):
"""Remove peer route."""
try:
result = routing_manager.remove_peer_route(peer_name)
return jsonify(result)
except Exception as e:
logger.error(f"Error removing peer route: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/exit-nodes', methods=['POST'])
def add_exit_node():
"""Add exit node."""
try:
data = request.get_json(silent=True)
result = routing_manager.add_exit_node(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error adding exit node: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/bridge', methods=['POST'])
def add_bridge_route():
"""Add bridge route."""
try:
data = request.get_json(silent=True)
result = routing_manager.add_bridge_route(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error adding bridge route: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/split', methods=['POST'])
def add_split_route():
"""Add split route."""
try:
data = request.get_json(silent=True)
result = routing_manager.add_split_route(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error adding split route: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/firewall', methods=['POST'])
def add_firewall_rule():
"""Add firewall rule.
JSON fields:
- rule_type (INPUT, OUTPUT, FORWARD)
- source (CIDR)
- destination (CIDR)
- action (ACCEPT, DROP, REJECT)
- protocol (TCP, UDP, ICMP, ALL)
- port (str, optional)
- port_range (str, optional, e.g. '1000-2000')
"""
try:
data = request.get_json(silent=True) or {}
result = routing_manager.add_firewall_rule(
rule_type=data.get('rule_type'),
source=data.get('source'),
destination=data.get('destination'),
action=data.get('action', 'ACCEPT'),
port=data.get('port'),
protocol=data.get('protocol', 'ALL'),
port_range=data.get('port_range')
)
return jsonify({'success': result})
except Exception as e:
logger.error(f"Error adding firewall rule: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/connectivity', methods=['POST'])
def test_routing_connectivity():
"""Test routing connectivity."""
try:
data = request.get_json(silent=True)
result = routing_manager.test_connectivity(data)
return jsonify(result)
except Exception as e:
logger.error(f"Error testing routing connectivity: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/logs', methods=['GET'])
def get_routing_logs():
"""Get routing logs."""
try:
lines = request.args.get('lines', 50, type=int)
result = routing_manager.get_logs(lines)
return jsonify(result)
except Exception as e:
logger.error(f"Error getting routing logs: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/nat', methods=['GET'])
def get_nat_rules():
"""Get all NAT rules."""
try:
rules = routing_manager.get_nat_rules()
return jsonify({"nat_rules": rules})
except Exception as e:
logger.error(f"Error getting NAT rules: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/peers', methods=['GET'])
def get_peer_routes():
"""Get all peer routes."""
try:
routes = routing_manager.get_peer_routes()
return jsonify({"peer_routes": routes})
except Exception as e:
logger.error(f"Error getting peer routes: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/routing/firewall', methods=['GET'])
def get_firewall_rules():
"""Get all firewall rules."""
try:
rules = routing_manager.get_firewall_rules()
return jsonify({"firewall_rules": rules})
except Exception as e:
logger.error(f"Error getting firewall rules: {e}")
return jsonify({"error": str(e)}), 500
# Vault & Trust API (Phase 6)
@app.route('/api/vault/status', methods=['GET'])
def get_vault_status():
"""Get vault status."""
try:
status = current_app.vault_manager.get_status()
return jsonify(status)
except Exception as e:
logger.error(f"Error getting vault status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/certificates', methods=['GET'])
def get_certificates():
"""Get all certificates."""
try:
certificates = current_app.vault_manager.list_certificates()
return jsonify(certificates)
except Exception as e:
logger.error(f"Error getting certificates: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/certificates', methods=['POST'])
def generate_certificate():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = current_app.vault_manager.generate_certificate(
common_name=data['common_name'],
domains=data.get('domains', []),
key_size=data.get('key_size', 2048),
days=data.get('days', 365)
)
return jsonify(result)
except Exception as e:
logger.error(f"Error generating certificate: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/certificates/<common_name>', methods=['DELETE'])
def revoke_certificate(common_name):
"""Revoke certificate."""
try:
result = current_app.vault_manager.revoke_certificate(common_name)
return jsonify({"revoked": result})
except Exception as e:
logger.error(f"Error revoking certificate: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/ca/certificate', methods=['GET'])
def get_ca_certificate():
"""Get CA certificate."""
try:
cert = current_app.vault_manager.get_ca_certificate()
return jsonify({"certificate": cert})
except Exception as e:
logger.error(f"Error getting CA certificate: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/age/public-key', methods=['GET'])
def get_age_public_key():
"""Get Age public key."""
try:
key = current_app.vault_manager.get_age_public_key()
return jsonify({"public_key": key})
except Exception as e:
logger.error(f"Error getting Age public key: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/trust/keys', methods=['GET'])
def get_trusted_keys():
"""Get trusted keys."""
try:
keys = current_app.vault_manager.get_trusted_keys()
return jsonify(keys)
except Exception as e:
logger.error(f"Error getting trusted keys: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/trust/keys', methods=['POST'])
def add_trusted_key():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = current_app.vault_manager.add_trusted_key(
name=data['name'],
public_key=data['public_key'],
trust_level=data.get('trust_level', 'direct')
)
return jsonify({"added": result})
except Exception as e:
logger.error(f"Error adding trusted key: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/trust/keys/<name>', methods=['DELETE'])
def remove_trusted_key(name):
"""Remove trusted key."""
try:
result = current_app.vault_manager.remove_trusted_key(name)
return jsonify({"removed": result})
except Exception as e:
logger.error(f"Error removing trusted key: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/trust/verify', methods=['POST'])
def verify_trust_chain():
try:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
result = current_app.vault_manager.verify_trust_chain(
peer_name=data['peer_name'],
signature=data['signature'],
data=data['data']
)
return jsonify({"verified": result})
except Exception as e:
logger.error(f"Error verifying trust chain: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/vault/trust/chains', methods=['GET'])
def get_trust_chains():
"""Get trust chains."""
try:
chains = current_app.vault_manager.get_trust_chains()
return jsonify(chains)
except Exception as e:
logger.error(f"Error getting trust chains: {e}")
return jsonify({"error": str(e)}), 500
# Services API
@app.route('/api/services/status', methods=['GET'])
def get_all_services_status():
"""Get status of all services."""
try:
# Use service bus to get status from all services
services_status = {}
for service_name in service_bus.list_services():
try:
service = service_bus.get_service(service_name)
status = service.get_status()
# Clean up status for UI consumption
if isinstance(status, dict):
# Extract core status information
clean_status = {
'status': status.get('status', 'unknown'),
'running': status.get('running', False),
'timestamp': status.get('timestamp', datetime.utcnow().isoformat())
}
# Add service-specific metrics
if service_name == 'network':
clean_status.update({
'dns_status': status.get('dns_running', False),
'dhcp_status': status.get('dhcp_running', False),
'ntp_status': status.get('ntp_running', False)
})
elif service_name == 'wireguard':
clean_status.update({
'peers_count': status.get('peers_count', 0),
'interface': status.get('interface', 'unknown')
})
elif service_name == 'email':
clean_status.update({
'users_count': status.get('users_count', 0),
'domain': status.get('domain', 'unknown')
})
elif service_name == 'calendar':
clean_status.update({
'users_count': status.get('users_count', 0),
'calendars_count': status.get('calendars_count', 0)
})
elif service_name == 'files':
clean_status.update({
'users_count': status.get('users_count', 0),
'storage_used': status.get('total_storage_used', {})
})
elif service_name == 'routing':
clean_status.update({
'nat_rules_count': status.get('nat_rules_count', 0),
'peer_routes_count': status.get('peer_routes_count', 0),
'firewall_rules_count': status.get('firewall_rules_count', 0)
})
elif service_name == 'vault':
clean_status.update({
'certificates_count': status.get('certificates_count', 0),
'trusted_keys_count': status.get('trusted_keys_count', 0)
})
services_status[service_name] = clean_status
else:
services_status[service_name] = {'status': str(status), 'running': bool(status)}
except Exception as e:
services_status[service_name] = {'error': str(e), 'status': 'offline', 'running': False}
return jsonify({
"network": services_status.get('network', {}),
"wireguard": services_status.get('wireguard', {}),
"email": services_status.get('email', {}),
"calendar": services_status.get('calendar', {}),
"files": services_status.get('files', {}),
"routing": services_status.get('routing', {}),
"vault": services_status.get('vault', {}),
"timestamp": datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error getting all services status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/services/connectivity', methods=['GET'])
def test_all_services_connectivity():
"""Test connectivity of all services."""
try:
# Use service bus to test connectivity
connectivity_results = {}
for service_name in service_bus.list_services():
try:
service = service_bus.get_service(service_name)
if hasattr(service, 'test_connectivity'):
connectivity_results[service_name] = service.test_connectivity()
else:
connectivity_results[service_name] = {'status': 'ok', 'message': 'No connectivity test available'}
except Exception as e:
connectivity_results[service_name] = {'status': 'error', 'message': str(e)}
return jsonify({
"network": connectivity_results.get('network', {}),
"wireguard": connectivity_results.get('wireguard', {}),
"email": connectivity_results.get('email', {}),
"calendar": connectivity_results.get('calendar', {}),
"files": connectivity_results.get('files', {}),
"routing": connectivity_results.get('routing', {}),
"timestamp": datetime.utcnow().isoformat()
})
except Exception as e:
logger.error(f"Error testing all services connectivity: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/health/history', methods=['GET'])
def get_health_history():
"""Get recent unified health check results."""
return jsonify(list(health_history))
@app.route('/api/logs', methods=['GET'])
def get_backend_logs():
"""Get backend log file contents (last N lines)."""
log_file = os.path.join(os.path.dirname(__file__), 'picell.log')
lines = int(request.args.get('lines', 100))
try:
if not os.path.exists(log_file):
return jsonify({"error": "Log file not found."}), 404
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
all_lines = f.readlines()
tail_lines = all_lines[-lines:] if lines > 0 else all_lines
return jsonify({"log": ''.join(tail_lines)})
except Exception as e:
logger.error(f"Error reading log file: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/containers', methods=['GET'])
def list_containers():
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
try:
containers = container_manager.list_containers()
return jsonify(containers)
except Exception as e:
logger.error(f"Error listing containers: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/containers/<name>/start', methods=['POST'])
def start_container(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
try:
success = container_manager.start_container(name)
return jsonify({'started': success})
except Exception as e:
logger.error(f"Error starting container {name}: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/containers/<name>/stop', methods=['POST'])
def stop_container(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
try:
success = container_manager.stop_container(name)
return jsonify({'stopped': success})
except Exception as e:
logger.error(f"Error stopping container {name}: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/containers/<name>/restart', methods=['POST'])
def restart_container(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
try:
success = container_manager.restart_container(name)
return jsonify({'restarted': success})
except Exception as e:
logger.error(f"Error restarting container {name}: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/containers/<name>/logs', methods=['GET'])
def get_container_logs(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
tail = request.args.get('tail', default=100, type=int)
try:
logs = container_manager.get_container_logs(name, tail=tail)
return jsonify({'logs': logs})
except Exception as e:
logger.error(f"Error getting logs for container {name}: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/containers/<name>/stats', methods=['GET'])
def get_container_stats(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
try:
stats = container_manager.get_container_stats(name)
return jsonify(stats)
except Exception as e:
logger.error(f"Error getting stats for container {name}: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/vault/secrets', methods=['GET'])
def list_secrets():
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
secrets = app.vault_manager.list_secrets()
return jsonify({'secrets': secrets})
@app.route('/api/vault/secrets', methods=['POST'])
def store_secret():
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
data = request.get_json(silent=True)
if not data or 'name' not in data or 'value' not in data:
return jsonify({'error': 'Missing name or value'}), 400
app.vault_manager.store_secret(data['name'], data['value'])
return jsonify({'stored': True})
@app.route('/api/vault/secrets/<name>', methods=['GET'])
def get_secret(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
value = app.vault_manager.get_secret(name)
if value is None:
return jsonify({'error': 'Not found'}), 404
return jsonify({'name': name, 'value': value})
@app.route('/api/vault/secrets/<name>', methods=['DELETE'])
def delete_secret(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
result = app.vault_manager.delete_secret(name)
return jsonify({'deleted': result})
# Enhance container creation to support secrets
@app.route('/api/containers', methods=['POST'])
def create_container():
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
data = request.get_json(silent=True)
if not data or 'image' not in data:
return jsonify({'error': 'Missing image parameter'}), 400
name = data.get('name', '')
env = data.get('env', {})
# If 'secrets' is provided, resolve secret values and add to env
secrets = data.get('secrets', [])
if secrets:
for secret_name in secrets:
secret_value = app.vault_manager.get_secret(secret_name)
if secret_value is not None:
env[secret_name] = secret_value
volumes = data.get('volumes', {})
command = data.get('command', '')
ports = data.get('ports', {})
result = container_manager.create_container(
image=data['image'],
name=name,
env=env,
volumes=volumes,
command=command,
ports=ports
)
if 'error' in result:
return jsonify(result), 500
return jsonify(result)
@app.route('/api/containers/<name>', methods=['DELETE'])
def remove_container(name):
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
force = request.args.get('force', default=False, type=bool)
success = container_manager.remove_container(name, force=force)
return jsonify({'removed': success})
@app.route('/api/images', methods=['GET'])
def list_images():
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
images = container_manager.list_images()
return jsonify(images)
@app.route('/api/images/pull', methods=['POST'])
def pull_image():
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
data = request.get_json(silent=True)
if not data or 'image' not in data:
return jsonify({'error': 'Missing image parameter'}), 400
result = container_manager.pull_image(data['image'])
if 'error' in result:
return jsonify(result), 500
return jsonify(result)
@app.route('/api/images/<image>', methods=['DELETE'])
def remove_image(image):
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
force = request.args.get('force', default=False, type=bool)
success = container_manager.remove_image(image, force=force)
return jsonify({'removed': success})
@app.route('/api/volumes', methods=['GET'])
def list_volumes():
# Temporarily disable access control for debugging
# if not is_local_request():
# return jsonify({'error': 'Access denied'}), 403
volumes = container_manager.list_volumes()
return jsonify(volumes)
@app.route('/api/volumes', methods=['POST'])
def create_volume():
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
data = request.get_json(silent=True)
if not data or 'name' not in data:
return jsonify({'error': 'Missing name parameter'}), 400
result = container_manager.create_volume(data['name'])
if 'error' in result:
return jsonify(result), 500
return jsonify(result)
@app.route('/api/volumes/<name>', methods=['DELETE'])
def remove_volume(name):
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
force = request.args.get('force', default=False, type=bool)
success = container_manager.remove_volume(name, force=force)
return jsonify({'removed': success})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000, debug=True)