Files
anchorhost-backend/routes/admin.py
T
wfalkowski 82fa50e9d5
Build and publish Docker image / build-and-publish (push) Successful in 7s
fix(terminal): authenticate host shell sessions
2026-10-20 17:29:05 +00:00

955 lines
39 KiB
Python

"""Admin API routes for AnchorHost."""
import bcrypt
from database_service import database_connection, dict_from_row
from domain_runtime import domain_manager
from flask import Blueprint, jsonify, request
import json
from kubernetes import client
from kubernetes.client.rest import ApiException
from kubernetes_service import custom_objects_api, k8s_connected, k8s_manager, v1
from runtime_config import (
DATABASE_URL,
logger,
)
from server_billing import charge_monthly_fees, get_user_credit_history, get_user_monthly_fees
from server_capacity import parse_resource_to_mebibytes, parse_resource_to_millicores, parse_storage_to_gb
from server_lifecycle import delete_server, restart_server, start_server, stop_server
from server_records import get_server_by_id, get_user_servers
from showcase_service import _is_valid_http_url, _normalize_showcase_url, get_homepage_showcase_links, save_homepage_showcase_links
from user_service import authenticate_user, get_bearer_token, get_panel_settings, get_pricing, require_admin, set_site_setting
import uuid
__all__ = ['admin_ban_user', 'admin_delete_server', 'admin_delete_user', 'admin_disable_user_2fa', 'admin_get_all_servers', 'admin_get_cluster_nodes', 'admin_get_stats', 'admin_get_user_billing_history', 'admin_get_user_credit_history', 'admin_get_user_servers', 'admin_get_users', 'admin_homepage_showcase', 'admin_panel_settings', 'admin_pricing', 'admin_reset_user_password', 'admin_restart_server', 'admin_start_server', 'admin_stop_server', 'admin_unban_user', 'admin_update_credits', 'create_announcement', 'debug_domains', 'debug_k8s', 'debug_servers', 'debug_users', 'delete_announcement', 'list_namespaces', 'run_manual_billing', 'update_announcement']
bp = Blueprint("admin", __name__)
@bp.route('/api/admin/billing/run-manual', methods=['POST'])
def run_manual_billing():
"""Run manual billing collection (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
result = charge_monthly_fees()
return jsonify(result), 200
except Exception as e:
logger.error(f"Error running manual billing: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users', methods=['GET'])
def admin_get_users():
"""Get all users (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
with database_connection() as conn:
cursor = conn.cursor()
# Get all users
cursor.execute('''
SELECT id, username, email, role, credits, created_at, email_verified, last_login, login_count
FROM users
ORDER BY created_at DESC
''')
users = []
for row in cursor.fetchall():
user_dict = dict_from_row(row)
users.append({
'id': user_dict['id'],
'username': user_dict['username'],
'email': user_dict['email'],
'role': user_dict['role'],
'credits': user_dict['credits'],
'created_at': user_dict['created_at'],
'email_verified': bool(user_dict['email_verified']),
'last_login': user_dict['last_login'],
'login_count': user_dict['login_count']
})
return jsonify({'success': True, 'users': users}), 200
except Exception as e:
logger.error(f"Error getting users: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/ban', methods=['POST'])
def admin_ban_user(user_id):
"""Ban a user (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
if user['id'] == user_id:
return jsonify({'success': False, 'error': 'Cannot ban yourself'}), 400
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('UPDATE users SET role = ? WHERE id = ?', ('banned', user_id))
conn.commit()
logger.info(f"User {user_id} banned by admin {user['username']}")
return jsonify({'success': True, 'message': 'User banned'}), 200
except Exception as e:
logger.error(f"Error banning user: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/unban', methods=['POST'])
def admin_unban_user(user_id):
"""Unban a user (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('UPDATE users SET role = ? WHERE id = ?', ('user', user_id))
conn.commit()
logger.info(f"User {user_id} unbanned by admin {user['username']}")
return jsonify({'success': True, 'message': 'User unbanned'}), 200
except Exception as e:
logger.error(f"Error unbanning user: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/credits', methods=['POST'])
def admin_update_credits(user_id):
"""Update user credits (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
data = request.get_json()
amount = data.get('amount')
operation = data.get('operation', 'add') # add, set, subtract
if amount is None:
return jsonify({'success': False, 'error': 'Amount required'}), 400
try:
amount = int(amount)
except ValueError:
return jsonify({'success': False, 'error': 'Amount must be an integer'}), 400
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT credits, username FROM users WHERE id = ?', (user_id,))
current_user = dict_from_row(cursor.fetchone())
if not current_user:
return jsonify({'success': False, 'error': 'User not found'}), 404
current_credits = current_user['credits']
new_credits = current_credits
if operation == 'add':
new_credits += amount
elif operation == 'subtract':
new_credits -= amount
elif operation == 'set':
new_credits = amount
cursor.execute('UPDATE users SET credits = ? WHERE id = ?', (new_credits, user_id))
conn.commit()
logger.info(f"Credits updated for user {current_user['username']} by admin {user['username']}: {current_credits} -> {new_credits}")
return jsonify({'success': True, 'message': 'Credits updated', 'new_credits': new_credits}), 200
except Exception as e:
logger.error(f"Error updating credits: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/servers', methods=['GET'])
def admin_get_user_servers(user_id):
"""Get servers for a specific user (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
servers = get_user_servers(user_id)
# Admin server management does not need connection credentials.
servers = [
{key: value for key, value in server.items() if key != 'sftp_password'}
for server in servers
]
return jsonify({'success': True, 'servers': servers}), 200
except Exception as e:
logger.error(f"Error getting user servers: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/credits/history', methods=['GET'])
def admin_get_user_credit_history(user_id):
"""Get credit transaction history for a specific user (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
history = get_user_credit_history(user_id, limit=50) # Use existing function
return jsonify({'success': True, 'history': history}), 200
except Exception as e:
logger.error(f"Error getting user credit history: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/billing/history', methods=['GET'])
def admin_get_user_billing_history(user_id):
"""Get billing history for a specific user (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
history = get_user_monthly_fees(user_id, limit=50) # Use existing function
return jsonify({'success': True, 'history': history}), 200
except Exception as e:
logger.error(f"Error getting user billing history: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/servers/<server_id>/start', methods=['POST'])
def admin_start_server(server_id):
"""Start a server (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
# Get server to find owner
server = get_server_by_id(server_id)
if not server:
return jsonify({'success': False, 'error': 'Server not found'}), 404
if start_server(server_id, server['owner_id']):
return jsonify({'success': True, 'message': 'Server started'}), 200
return jsonify({'success': False, 'error': 'Failed to start server'}), 500
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/servers/<server_id>/stop', methods=['POST'])
def admin_stop_server(server_id):
"""Stop a server (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
# Get server to find owner
server = get_server_by_id(server_id)
if not server:
return jsonify({'success': False, 'error': 'Server not found'}), 404
if stop_server(server_id, server['owner_id']):
return jsonify({'success': True, 'message': 'Server stopped'}), 200
return jsonify({'success': False, 'error': 'Failed to stop server'}), 500
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/servers/<server_id>/restart', methods=['POST'])
def admin_restart_server(server_id):
"""Restart a server (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
# Get server to find owner
server = get_server_by_id(server_id)
if not server:
return jsonify({'success': False, 'error': 'Server not found'}), 404
if restart_server(server_id, server['owner_id']):
return jsonify({'success': True, 'message': 'Server restarted'}), 200
return jsonify({'success': False, 'error': 'Failed to restart server'}), 500
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/servers/<server_id>', methods=['DELETE'])
def admin_delete_server(server_id):
"""Delete a server (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
# Get server to find owner
server = get_server_by_id(server_id)
if not server:
return jsonify({'success': False, 'error': 'Server not found'}), 404
if delete_server(server_id, server['owner_id']):
return jsonify({'success': True, 'message': 'Server deleted'}), 200
return jsonify({'success': False, 'error': 'Failed to delete server'}), 500
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/servers', methods=['GET'])
def admin_get_all_servers():
"""Get all servers with owner details (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT s.id, s.name, s.cpu, s.ram, s.storage, s.image, s.type, s.version, s.loader, s.java_version,
s.is_demo, s.demo_expires_at,
s.status, s.ports, s.created_at, u.username as owner, u.id as owner_id,
s.sftp_username, s.sftp_port
FROM servers s
JOIN users u ON s.owner_id = u.id
ORDER BY u.username, s.name
''')
servers = [dict_from_row(row) for row in cursor.fetchall()]
return jsonify({'success': True, 'servers': servers}), 200
except Exception as e:
logger.error(f"Error getting all servers: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/stats', methods=['GET'])
def admin_get_stats():
"""Get platform stats (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) as count FROM users')
res = dict_from_row(cursor.fetchone())
total_users = res['count'] if res else 0
cursor.execute('SELECT COUNT(*) as count FROM servers')
res = dict_from_row(cursor.fetchone())
total_servers = res['count'] if res else 0
cursor.execute("SELECT COUNT(*) as count FROM servers WHERE status = 'Running'")
res = dict_from_row(cursor.fetchone())
active_servers = res['count'] if res else 0
cursor.execute('SELECT SUM(credits) as total FROM users')
res = dict_from_row(cursor.fetchone())
total_credits = res['total'] if res and res['total'] is not None else 0
stats = {
'total_users': total_users,
'total_servers': total_servers,
'active_servers': active_servers,
'total_credits': total_credits
}
return jsonify({'success': True, 'stats': stats}), 200
except Exception as e:
logger.error(f"Error getting stats: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>', methods=['DELETE'])
def admin_delete_user(user_id):
"""Delete a user (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
with database_connection() as conn:
cursor = conn.cursor()
# Check if user exists
cursor.execute('SELECT username FROM users WHERE id = ?', (user_id,))
target_user = cursor.fetchone()
if not target_user:
return jsonify({'success': False, 'error': 'User not found'}), 404
# Delete user
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
logger.info(f"User {user_id} deleted by admin {user['username']}")
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Error deleting user: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/password', methods=['POST'])
def admin_reset_user_password(user_id):
"""Reset user password (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
data = request.json
new_password = data.get('password')
if not new_password or len(new_password) < 6:
return jsonify({'success': False, 'error': 'Password must be at least 6 characters'}), 400
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('UPDATE users SET password_hash = ?, password_changed_at = CURRENT_TIMESTAMP WHERE id = ?', (hashed_password, user_id))
conn.commit()
logger.info(f"Password reset for user {user_id} by admin {user['username']}")
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Error resetting password: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/users/<user_id>/2fa/disable', methods=['POST'])
def admin_disable_user_2fa(user_id):
"""Disable user 2FA (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('UPDATE users SET two_factor_enabled = FALSE, two_factor_secret = NULL WHERE id = ?', (user_id,))
conn.commit()
logger.info(f"2FA disabled for user {user_id} by admin {user['username']}")
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Error disabling 2FA: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/cluster/nodes', methods=['GET'])
def admin_get_cluster_nodes():
"""Get detailed cluster node stats (admin only)"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user or user.get('role') != 'admin':
return jsonify({'success': False, 'error': 'Admin access required'}), 403
if not k8s_connected or not v1:
return jsonify({'success': False, 'error': 'Kubernetes not connected'}), 503
nodes = v1.list_node()
node_stats = []
# Try to get metrics if available
metrics = {}
try:
# Use global custom_objects_api if available, else try to instantiate
api_instance = custom_objects_api if custom_objects_api else client.CustomObjectsApi()
metrics_data = api_instance.list_cluster_custom_object(
group="metrics.k8s.io",
version="v1beta1",
plural="nodes"
)
for item in metrics_data.get('items', []):
metrics[item['metadata']['name']] = item['usage']
except Exception as e:
logger.warning(f"Could not fetch node metrics: {e}")
# Get pod allocations for allocated stats
all_pods = v1.list_pod_for_all_namespaces()
pod_allocations = {}
for pod in all_pods.items:
if pod.spec.node_name and pod.status.phase in ['Running', 'Pending']:
node_name = pod.spec.node_name
if node_name not in pod_allocations:
pod_allocations[node_name] = {'cpu': 0, 'memory': 0, 'storage': 0.0}
for container in pod.spec.containers:
if container.resources and container.resources.requests:
if 'cpu' in container.resources.requests:
pod_allocations[node_name]['cpu'] += parse_resource_to_millicores(container.resources.requests['cpu'])
if 'memory' in container.resources.requests:
pod_allocations[node_name]['memory'] += parse_resource_to_mebibytes(container.resources.requests['memory'])
if 'ephemeral-storage' in container.resources.requests:
pod_allocations[node_name]['storage'] += parse_storage_to_gb(container.resources.requests['ephemeral-storage'])
for node in nodes.items:
name = node.metadata.name
# Specs
allocatable = node.status.allocatable or {}
capacity = node.status.capacity or {}
node_info = node.status.node_info
cpu_allocatable = parse_resource_to_millicores(allocatable.get('cpu', '0'))
memory_allocatable = parse_resource_to_mebibytes(allocatable.get('memory', '0'))
storage_capacity = parse_storage_to_gb(capacity.get('ephemeral-storage', '0Gi'))
storage_allocatable = parse_storage_to_gb(allocatable.get('ephemeral-storage', capacity.get('ephemeral-storage', '0Gi')))
# Roles
labels = node.metadata.labels or {}
roles = []
if 'node-role.kubernetes.io/control-plane' in labels or 'node-role.kubernetes.io/master' in labels:
roles.append('Control Plane')
else:
roles.append('Worker')
# OS Info
os_image = node_info.os_image
kernel_version = node_info.kernel_version
# Extract CPU Model (if available from labels)
cpu_model = "Unknown CPU"
if 'cpu-model.node.kubevirt.io/Haswell-noTSX' in labels:
cpu_model = "Intel Haswell"
elif 'cpu-model.node.kubevirt.io/Nehalem' in labels:
cpu_model = "Intel Nehalem"
elif any(k.startswith('cpu-model.node.kubevirt.io/') for k in labels):
cpu_model = [k.split('/')[-1] for k in labels if k.startswith('cpu-model.node.kubevirt.io/')][0]
# Fallback if vendor is known
if cpu_model == "Unknown CPU":
if 'cpu-vendor.node.kubevirt.io/Intel' in labels:
cpu_model = "Intel CPU"
elif 'cpu-vendor.node.kubevirt.io/AMD' in labels:
cpu_model = "AMD CPU"
# Allocated
allocated = pod_allocations.get(name, {'cpu': 0, 'memory': 0, 'storage': 0.0})
storage_allocated = allocated.get('storage', 0.0)
disk_alloc_percent = round((storage_allocated / storage_allocatable * 100), 1) if storage_allocatable > 0 else 0.0
# Usage (from metrics if available)
usage = {'cpu': 0, 'memory': 0}
if name in metrics:
usage['cpu'] = parse_resource_to_millicores(metrics[name].get('cpu', '0'))
usage['memory'] = parse_resource_to_mebibytes(metrics[name].get('memory', '0'))
node_stats.append({
'name': name,
'status': 'Ready' if any(c.type == 'Ready' and c.status == 'True' for c in node.status.conditions) else 'NotReady',
'role': roles[0],
'os': {
'name': node_info.operating_system,
'image': os_image,
'kernel': kernel_version,
'arch': node_info.architecture,
'icon': 'linux' if node_info.operating_system.lower() == 'linux' else 'windows'
},
'specs': {
'cpu': f"{int(cpu_allocatable/1000)} Cores",
'cpu_model': cpu_model,
'ram': f"{memory_allocatable/1024:.1f} GB",
'disk': f"{storage_capacity:.1f} GB"
},
'usage': {
'cpu': f"{usage['cpu']/1000:.2f} Cores",
'cpu_display': f"{usage['cpu']/1000:.2f} Cores" if usage['cpu'] > 0 else "0.00 Cores",
'cpu_percent': round(usage['cpu'] / cpu_allocatable * 100, 1) if cpu_allocatable > 0 else 0,
'ram': f"{usage['memory']/1024:.2f} GB",
'ram_display': f"{usage['memory']/1024:.2f} GB" if usage['memory'] > 0 else "0.00 GB",
'ram_percent': round(usage['memory'] / memory_allocatable * 100, 1) if memory_allocatable > 0 else 0
},
'allocated': {
'cpu': f"{allocated['cpu']/1000:.2f} Cores",
'cpu_display': f"{allocated['cpu']/1000:.2f} Cores",
'cpu_percent': round(allocated['cpu'] / cpu_allocatable * 100, 1) if cpu_allocatable > 0 else 0,
'ram': f"{allocated['memory']/1024:.2f} GB",
'ram_display': f"{allocated['memory']/1024:.2f} GB",
'ram_percent': round(allocated['memory'] / memory_allocatable * 100, 1) if memory_allocatable > 0 else 0,
'disk': f"{storage_allocated:.2f} GB",
'disk_display': f"{storage_allocated:.2f} GB / {storage_capacity:.1f} GB",
'disk_percent': disk_alloc_percent
}
})
return jsonify({'success': True, 'nodes': node_stats}), 200
except Exception as e:
logger.error(f"Error getting cluster nodes: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/debug/users', methods=['GET'])
def debug_users():
if not require_admin():
return jsonify({'error': 'Unauthorized'}), 403
try:
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT id, username, email, role, credits, created_at FROM users')
users_list = [dict_from_row(row) for row in cursor.fetchall()]
return jsonify({
'users': users_list,
'count': len(users_list),
'kubernetes': {
'connected': k8s_manager.connected,
'lastError': str(k8s_manager.last_error) if k8s_manager.last_error else None
}
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/debug/servers', methods=['GET'])
def debug_servers():
if not require_admin():
return jsonify({'error': 'Unauthorized'}), 403
try:
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, name, type, status, cpu, ram, storage, ip, ports,
owner_id, namespace, k8s_name, is_demo, demo_expires_at,
created_at
FROM servers
''')
servers_list = [dict_from_row(row) for row in cursor.fetchall()]
for server in servers_list:
if server and 'ports' in server:
server['ports'] = json.loads(server['ports'])
return jsonify({
'servers': servers_list,
'count': len(servers_list)
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/debug/domains', methods=['GET'])
def debug_domains():
if not require_admin():
return jsonify({'error': 'Unauthorized'}), 403
try:
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM user_domains')
domains_list = [dict_from_row(row) for row in cursor.fetchall()]
return jsonify({
'domains': domains_list,
'count': len(domains_list),
'domain_manager': 'available' if domain_manager else 'unavailable'
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/debug/k8s', methods=['GET'])
def debug_k8s():
if not require_admin():
return jsonify({'error': 'Unauthorized'}), 403
return jsonify({
'connected': k8s_manager.connected,
'lastError': str(k8s_manager.last_error) if k8s_manager.last_error else None
}), 200
@bp.route('/namespaces', methods=['GET'])
def list_namespaces():
if not require_admin():
return jsonify({'error': 'Unauthorized'}), 403
try:
if k8s_manager.connected:
namespaces = v1.list_namespace()
namespace_list = [ns.metadata.name for ns in namespaces.items]
return jsonify({
'success': True,
'namespaces': namespace_list,
'count': len(namespace_list)
}), 200
else:
return jsonify({
'success': True,
'namespaces': ['user-testuser'],
'count': 1,
'message': 'Kubernetes not connected'
}), 200
except ApiException as e:
return jsonify({
'success': False,
'error': f'Failed to list namespaces: {e.reason}',
'status': e.status
}), e.status
@bp.route('/api/admin/announcements', methods=['POST'])
def create_announcement():
"""Create a new announcement (Admin only)"""
try:
user = require_admin()
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
data = request.json
if not data or not data.get('title') or not data.get('content'):
return jsonify({'success': False, 'error': 'Title and content are required'}), 400
title = data['title']
content = data['content']
tags = data.get('tags', [])
# Ensure tags is a list
if isinstance(tags, str):
tags = [tags]
# Serialize tags to JSON for storage
tags_json = json.dumps(tags)
announcement_id = f"ann-{str(uuid.uuid4())[:8]}"
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO announcements (id, title, content, tags, author_id)
VALUES (?, ?, ?, ?, ?)
''', (announcement_id, title, content, tags_json, user['id']))
conn.commit()
# Fetch the created announcement to return it
cursor.execute('''
SELECT a.*, u.username as author_name
FROM announcements a
JOIN users u ON a.author_id = u.id
WHERE a.id = ?
''', (announcement_id,))
new_announcement = dict_from_row(cursor.fetchone())
if new_announcement and new_announcement.get('tags'):
try:
new_announcement['tags'] = json.loads(new_announcement['tags'])
except:
new_announcement['tags'] = []
return jsonify({'success': True, 'announcement': new_announcement})
except Exception as e:
logger.error(f"Error creating announcement: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/announcements/<announcement_id>', methods=['PUT'])
def update_announcement(announcement_id):
"""Update an announcement (Admin only)"""
try:
user = require_admin()
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
data = request.json
updates = []
params = []
if 'title' in data:
updates.append("title = ?")
params.append(data['title'])
if 'content' in data:
updates.append("content = ?")
params.append(data['content'])
if 'tags' in data:
tags = data['tags']
if isinstance(tags, str):
tags = [tags]
updates.append("tags = ?")
params.append(json.dumps(tags))
if not updates:
return jsonify({'success': False, 'error': 'No fields to update'}), 400
updates.append("updated_at = CURRENT_TIMESTAMP")
params.append(announcement_id)
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute(f'''
UPDATE announcements
SET {', '.join(updates)}
WHERE id = ?
''', params)
conn.commit()
if cursor.rowcount == 0:
return jsonify({'success': False, 'error': 'Announcement not found'}), 404
return jsonify({'success': True, 'message': 'Announcement updated successfully'})
except Exception as e:
logger.error(f"Error updating announcement: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/announcements/<announcement_id>', methods=['DELETE'])
def delete_announcement(announcement_id):
"""Delete an announcement (Admin only)"""
try:
user = require_admin()
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('DELETE FROM announcements WHERE id = ?', (announcement_id,))
conn.commit()
if cursor.rowcount == 0:
return jsonify({'success': False, 'error': 'Announcement not found'}), 404
return jsonify({'success': True, 'message': 'Announcement deleted successfully'})
except Exception as e:
logger.error(f"Error deleting announcement: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/homepage-showcase', methods=['GET', 'PUT'])
def admin_homepage_showcase():
"""Read or update homepage showcase links (admin only)."""
try:
user = require_admin()
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
if request.method == 'GET':
return jsonify({'success': True, 'links': get_homepage_showcase_links()})
data = request.json or {}
raw_links = data.get('links')
if not isinstance(raw_links, list):
return jsonify({'success': False, 'error': 'links must be an array'}), 400
normalized = []
for i in range(3):
entry = raw_links[i] if i < len(raw_links) and isinstance(raw_links[i], dict) else {}
label = (entry.get('label') or f'Site {i + 1}').strip()[:160]
url = _normalize_showcase_url(entry.get('url', ''))
if not _is_valid_http_url(url):
return jsonify({'success': False, 'error': f'Invalid URL for slot {i + 1}'}), 400
normalized.append({"label": label, "url": url})
save_homepage_showcase_links(normalized)
return jsonify({'success': True, 'links': normalized})
except Exception as e:
logger.error(f"admin_homepage_showcase: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/pricing', methods=['GET', 'POST'])
def admin_pricing():
"""Read or update service pricing (admin only)."""
try:
user = require_admin()
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
if request.method == 'GET':
return jsonify({'success': True, 'pricing': get_pricing()})
data = request.json or {}
with database_connection() as conn:
cursor = conn.cursor()
for key, value in data.items():
if key.startswith('price_') or key.startswith('multiplier_'):
# Skip None values (JSON NaN becomes None)
if value is None:
continue
if 'postgres' in DATABASE_URL:
cursor.execute(
"INSERT INTO site_settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP",
(key, str(value))
)
else:
# Check if key exists
cursor.execute("SELECT 1 FROM site_settings WHERE key = ?", (key,))
if cursor.fetchone():
cursor.execute(
"UPDATE site_settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?",
(str(value), key)
)
else:
cursor.execute(
"INSERT INTO site_settings (key, value) VALUES (?, ?)",
(key, str(value))
)
conn.commit()
return jsonify({'success': True, 'pricing': get_pricing()})
except Exception as e:
logger.error(f"admin_pricing: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/admin/settings', methods=['GET', 'POST', 'PUT'])
@bp.route('/api/admin/panel-settings', methods=['GET', 'POST', 'PUT'])
def admin_panel_settings():
"""Read or update panel settings (admin only)."""
try:
user = require_admin()
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
if request.method == 'GET':
return jsonify({'success': True, 'settings': get_panel_settings()})
data = request.json or {}
if 'demo_servers_enabled' in data:
demo_val = 'true' if data['demo_servers_enabled'] in (True, 'true', '1', 1) else 'false'
set_site_setting('demo_servers_enabled', demo_val)
if 'new_user_credits' in data:
try:
credits_val = max(0, int(float(data['new_user_credits'])))
set_site_setting('new_user_credits', str(credits_val))
except (ValueError, TypeError):
pass
return jsonify({'success': True, 'settings': get_panel_settings()})
except Exception as e:
logger.error(f"admin_panel_settings: {e}")
return jsonify({'success': False, 'error': str(e)}), 500