mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
Replace generic @jwt_required() usage with role-based decorators and switch Socket.IO token handling to the auth payload. - Replace jwt_required with viewer_required/admin_required across API modules (backend/app/api/cron.py, firewall.py, ftp.py, git.py, metrics.py) and update imports accordingly. - Require JWT for previously public system/private endpoints (backend/app/api/system.py, private_urls.py). - Change Socket.IO server connection handler to accept auth payload and read token from auth (backend/app/sockets.py) to avoid token leakage in query strings. - Update frontend clients to send token via socket auth instead of query (frontend/src/pages/WordPressProject.jsx, frontend/src/services/socket.js). These changes add finer-grained RBAC control for endpoints and improve socket auth security by removing tokens from URLs/logs.
331 lines
9.6 KiB
Python
331 lines
9.6 KiB
Python
import os
|
|
import time
|
|
import urllib.request
|
|
import json
|
|
from flask import Blueprint, jsonify, request, current_app
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
from app.models import User
|
|
from app.services.system_service import SystemService
|
|
from app.services.resource_tier_service import ResourceTierService
|
|
|
|
# Cache for update check (to avoid hitting GitHub API too often)
|
|
_update_cache = {
|
|
'last_check': 0,
|
|
'result': None,
|
|
'ttl': 3600 # 1 hour cache
|
|
}
|
|
|
|
system_bp = Blueprint('system', __name__)
|
|
|
|
|
|
@system_bp.route('/version', methods=['GET'])
|
|
@jwt_required()
|
|
def get_version():
|
|
"""Get ServerKit version."""
|
|
version = '1.0.0' # Default version
|
|
|
|
# Try to read from VERSION file
|
|
version_paths = [
|
|
'/opt/serverkit/VERSION',
|
|
os.path.join(current_app.root_path, '..', 'VERSION'),
|
|
os.path.join(current_app.root_path, '..', '..', 'VERSION'),
|
|
]
|
|
|
|
for path in version_paths:
|
|
try:
|
|
if os.path.exists(path):
|
|
with open(path, 'r') as f:
|
|
version = f.read().strip()
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
return jsonify({
|
|
'version': version,
|
|
'name': 'ServerKit'
|
|
}), 200
|
|
|
|
|
|
@system_bp.route('/check-update', methods=['GET'])
|
|
@jwt_required()
|
|
def check_update():
|
|
"""Check for updates from GitHub releases."""
|
|
global _update_cache
|
|
|
|
# Return cached result if still valid
|
|
if _update_cache['result'] and (time.time() - _update_cache['last_check']) < _update_cache['ttl']:
|
|
return jsonify(_update_cache['result']), 200
|
|
|
|
# Get current version
|
|
current_version = '1.0.0'
|
|
version_paths = [
|
|
'/opt/serverkit/VERSION',
|
|
os.path.join(current_app.root_path, '..', 'VERSION'),
|
|
os.path.join(current_app.root_path, '..', '..', 'VERSION'),
|
|
]
|
|
for path in version_paths:
|
|
try:
|
|
if os.path.exists(path):
|
|
with open(path, 'r') as f:
|
|
current_version = f.read().strip()
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
result = {
|
|
'current_version': current_version,
|
|
'latest_version': None,
|
|
'update_available': False,
|
|
'release_url': None,
|
|
'release_notes': None,
|
|
'error': None
|
|
}
|
|
|
|
try:
|
|
# Fetch latest release from GitHub API
|
|
req = urllib.request.Request(
|
|
'https://api.github.com/repos/jhd3197/ServerKit/releases/latest',
|
|
headers={'User-Agent': 'ServerKit-UpdateChecker'}
|
|
)
|
|
with urllib.request.urlopen(req, timeout=10) as response:
|
|
data = json.loads(response.read().decode('utf-8'))
|
|
|
|
latest_version = data.get('tag_name', '').lstrip('v')
|
|
result['latest_version'] = latest_version
|
|
result['release_url'] = data.get('html_url')
|
|
result['release_notes'] = data.get('body', '')[:500] # Truncate release notes
|
|
|
|
# Simple version comparison (works for semver-like versions)
|
|
def parse_version(v):
|
|
try:
|
|
return tuple(int(x) for x in v.split('.'))
|
|
except:
|
|
return (0, 0, 0)
|
|
|
|
if parse_version(latest_version) > parse_version(current_version):
|
|
result['update_available'] = True
|
|
|
|
except urllib.error.HTTPError as e:
|
|
if e.code == 404:
|
|
result['error'] = 'No releases found'
|
|
else:
|
|
result['error'] = f'GitHub API error: {e.code}'
|
|
except Exception as e:
|
|
result['error'] = f'Failed to check for updates: {str(e)}'
|
|
|
|
# Cache the result
|
|
_update_cache['last_check'] = time.time()
|
|
_update_cache['result'] = result
|
|
|
|
return jsonify(result), 200
|
|
|
|
|
|
@system_bp.route('/metrics', methods=['GET'])
|
|
@jwt_required()
|
|
def get_metrics():
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
metrics = SystemService.get_all_metrics()
|
|
return jsonify(metrics), 200
|
|
|
|
|
|
@system_bp.route('/info', methods=['GET'])
|
|
@jwt_required()
|
|
def get_system_info():
|
|
"""Get system information (hostname, IP, kernel, CPU info)."""
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
return jsonify(SystemService.get_system_info()), 200
|
|
|
|
|
|
@system_bp.route('/cpu', methods=['GET'])
|
|
@jwt_required()
|
|
def get_cpu_metrics():
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
return jsonify(SystemService.get_cpu_metrics()), 200
|
|
|
|
|
|
@system_bp.route('/memory', methods=['GET'])
|
|
@jwt_required()
|
|
def get_memory_metrics():
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
return jsonify(SystemService.get_memory_metrics()), 200
|
|
|
|
|
|
@system_bp.route('/disk', methods=['GET'])
|
|
@jwt_required()
|
|
def get_disk_metrics():
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
return jsonify(SystemService.get_disk_metrics()), 200
|
|
|
|
|
|
@system_bp.route('/network', methods=['GET'])
|
|
@jwt_required()
|
|
def get_network_metrics():
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
return jsonify(SystemService.get_network_metrics()), 200
|
|
|
|
|
|
@system_bp.route('/health', methods=['GET'])
|
|
def health_check():
|
|
"""Health check endpoint - no auth required"""
|
|
return jsonify({
|
|
'status': 'healthy',
|
|
'service': 'serverkit-api'
|
|
}), 200
|
|
|
|
|
|
@system_bp.route('/resource-tier', methods=['GET'])
|
|
@jwt_required()
|
|
def get_resource_tier():
|
|
"""
|
|
Get server resource tier information.
|
|
|
|
Returns tier, specs, and feature permissions based on server resources.
|
|
Results are cached for 1 hour unless refresh=true is passed.
|
|
"""
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
force_refresh = request.args.get('refresh', 'false').lower() == 'true'
|
|
tier_info = ResourceTierService.get_tier_info(force_refresh=force_refresh)
|
|
|
|
return jsonify(tier_info), 200
|
|
|
|
|
|
@system_bp.route('/time', methods=['GET'])
|
|
@jwt_required()
|
|
def get_server_time():
|
|
"""Get current server time and timezone."""
|
|
return jsonify(SystemService.get_server_time()), 200
|
|
|
|
|
|
@system_bp.route('/timezones', methods=['GET'])
|
|
@jwt_required()
|
|
def get_timezones():
|
|
"""Get list of available timezones."""
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
timezones = SystemService.get_available_timezones()
|
|
return jsonify({'timezones': timezones}), 200
|
|
|
|
|
|
@system_bp.route('/timezone', methods=['PUT'])
|
|
@jwt_required()
|
|
def set_timezone():
|
|
"""Set server timezone (admin only)."""
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
data = request.get_json()
|
|
if not data or 'timezone' not in data:
|
|
return jsonify({'error': 'Timezone is required'}), 400
|
|
|
|
result = SystemService.set_timezone(data['timezone'])
|
|
|
|
if result['success']:
|
|
return jsonify(result), 200
|
|
else:
|
|
return jsonify(result), 400
|
|
|
|
|
|
@system_bp.route('/processes', methods=['GET'])
|
|
@jwt_required()
|
|
def get_processes():
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
processes = []
|
|
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
|
|
try:
|
|
pinfo = proc.info
|
|
if pinfo['cpu_percent'] > 0 or pinfo['memory_percent'] > 0.1:
|
|
processes.append({
|
|
'pid': pinfo['pid'],
|
|
'name': pinfo['name'],
|
|
'cpu_percent': pinfo['cpu_percent'],
|
|
'memory_percent': round(pinfo['memory_percent'], 2),
|
|
'status': pinfo['status']
|
|
})
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
pass
|
|
|
|
# Sort by CPU usage
|
|
processes.sort(key=lambda x: x['cpu_percent'], reverse=True)
|
|
|
|
return jsonify({
|
|
'processes': processes[:50] # Top 50 processes
|
|
}), 200
|
|
|
|
|
|
@system_bp.route('/services', methods=['GET'])
|
|
@jwt_required()
|
|
def get_services():
|
|
current_user_id = get_jwt_identity()
|
|
user = User.query.get(current_user_id)
|
|
|
|
if user.role != 'admin':
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
# Common services to check
|
|
service_names = ['nginx', 'mysql', 'postgresql', 'redis', 'docker', 'php-fpm']
|
|
services = []
|
|
|
|
for name in service_names:
|
|
running = False
|
|
for proc in psutil.process_iter(['name']):
|
|
try:
|
|
if name.lower() in proc.info['name'].lower():
|
|
running = True
|
|
break
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
pass
|
|
|
|
services.append({
|
|
'name': name,
|
|
'status': 'running' if running else 'stopped'
|
|
})
|
|
|
|
return jsonify({'services': services}), 200
|