USER
import os
import subprocess
import uuid
import yaml
import paramiko
from flask import Flask, jsonify, request, render_template
from flask_cors import CORS
from flask_jwt_extended import (
JWTManager, create_access_token,
jwt_required, get_jwt_identity
)
from passlib.hash import pbkdf2_sha256
from logging.handlers import RotatingFileHandler
import json
from datetime import datetime, timedelta
import random
import logging
import string
import user_agents
import traceback
from werkzeug.exceptions import BadRequest
from apscheduler.schedulers.background import BackgroundScheduler
from jinja2 import Template
import posixpath
# Load environment variables for sensitive configurations
from dotenv import load_dotenv
load_dotenv() # Loads variables from a .env file
# SSH Configuration
SSH_CONFIG = {
'hostname': os.getenv('SSH_HOSTNAME', '78.46.62.242'),
'port': int(os.getenv('SSH_PORT', 22)),
'username': os.getenv('SSH_USERNAME', 'root'),
'password': os.getenv('SSH_PASSWORD'), # Ensure this is set
'key_filepath': os.getenv('SSH_KEY_FILEPATH') # Optional: path to SSH private key
}
# Load the server IP from the environment variable
SERVER_IP = os.getenv('SSH_HOSTNAME', None)
app = Flask(__name__, static_folder='static', template_folder='templates')
CORS(app)
# JWT Configuration
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'default_jwt_secret_key') # Set this in environment
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
jwt = JWTManager(app)
# Server Plans
SERVER_PLANS = [
{
"key": "basic",
"name": "Базовый",
"cpu": 2,
"ram": 4,
"disk": "50GB",
"price": 500
},
{
"key": "advanced",
"name": "Продвинутый",
"cpu": 4,
"ram": 8,
"disk": "100GB",
"price": 1000
},
{
"key": "premium",
"name": "Премиум",
"cpu": 8,
"ram": 16,
"disk": "200GB",
"price": 2000
}
]
OS_OPTIONS = {
'win2020': 'Windows Server 2020',
'win2022': 'Windows Server 2022',
'win2025': 'Windows Server 2025',
'win10': 'Windows 10',
'win11': 'Windows 11'
}
# Define Remote Containers Directory
REMOTE_CONTAINERS_DIR = "/root"
# Initialize IP Pool
IP_POOL_START = [192, 168, 1, 100] # Starting IP address
current_ip = IP_POOL_START.copy()
# Log directories (local)
LOGS_DIR = 'logs'
if not os.path.exists(LOGS_DIR):
os.makedirs(LOGS_DIR)
# Helper functions
def generate_unique_ip():
global current_ip
if SERVER_IP:
return SERVER_IP # Return the fixed IP from env variable
ip = '.'.join(map(str, current_ip))
current_ip[3] += 1
if current_ip[3] > 254:
current_ip[3] = 1
current_ip[2] += 1
if current_ip[2] > 254:
current_ip[2] = 1
current_ip[1] += 1
if current_ip[1] > 254:
current_ip[1] = 1
current_ip[0] += 1
if current_ip[0] > 254:
raise Exception("IP Pool Exhausted")
return ip
def get_remote_server_dir(container_name):
safe_container_name = ''.join(c for c in container_name if c.isalnum() or c in ('_', '-'))
return posixpath.join(REMOTE_CONTAINERS_DIR, safe_container_name)
@app.route('/')
def home():
return render_template('index.html')
def setup_logging():
formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
)
file_handler = RotatingFileHandler(
os.path.join(LOGS_DIR, 'app.log'),
maxBytes=1024 * 1024,
backupCount=10
)
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.INFO)
error_file_handler = RotatingFileHandler(
os.path.join(LOGS_DIR, 'error.log'),
maxBytes=1024 * 1024,
backupCount=10
)
error_file_handler.setFormatter(formatter)
error_file_handler.setLevel(logging.ERROR)
app.logger.addHandler(file_handler)
app.logger.addHandler(error_file_handler)
app.logger.setLevel(logging.INFO)
@app.before_request
def log_request_info():
app.logger.info(f'Request: {request.method} {request.url} from {request.remote_addr}')
app.logger.debug(f'Headers: {dict(request.headers)}')
@app.errorhandler(Exception)
def handle_exception(e):
app.logger.error(f'Unhandled Exception: {str(e)}')
app.logger.error(traceback.format_exc())
return jsonify({
'error': 'An internal server error occurred',
'details': str(e) if app.debug else None
}), 500
def load_db():
try:
if not os.path.exists('db.json'):
with open('db.json', 'w') as f:
json.dump({'users': {}, 'logs': []}, f)
with open('db.json', 'r') as f:
db = json.load(f)
db.setdefault('logs', [])
db.setdefault('users', {})
return db
except Exception as e:
app.logger.error(f'Database load error: {str(e)}')
app.logger.error(traceback.format_exc())
raise
def save_db(db):
with open('db.json', 'w') as f:
json.dump(db, f, indent=2)
def log_activity(username, action, user_agent_string):
try:
db = load_db()
ua = user_agents.parse(user_agent_string)
log_entry = {
'timestamp': datetime.now().isoformat(),
'username': username,
'action': action,
'browser': ua.browser.family,
'os': ua.os.family,
'device': ua.device.family,
'ip': request.remote_addr
}
db['logs'].append(log_entry)
save_db(db)
except Exception as e:
app.logger.error(f'Error logging activity: {str(e)}')
app.logger.error(traceback.format_exc())
def generate_password(length=12):
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
def connect_via_ssh():
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
connect_params = {
'hostname': SSH_CONFIG['hostname'],
'port': SSH_CONFIG.get('port', 22),
'username': SSH_CONFIG['username']
}
if SSH_CONFIG.get('key_filepath'):
connect_params['key_filename'] = SSH_CONFIG['key_filepath']
elif SSH_CONFIG.get('password'):
connect_params['password'] = SSH_CONFIG['password']
else:
raise ValueError("Either 'key_filepath' or 'password' must be provided in SSH_CONFIG.")
try:
ssh.connect(**connect_params)
app.logger.info(f"SSH connection established to {SSH_CONFIG['hostname']}")
except Exception as e:
app.logger.error(f'SSH connection failed: {e}')
return None
return ssh
def execute_ssh_command(command, ssh_client, remote_path=None):
try:
if remote_path:
command = f"cd {remote_path} && {command}"
app.logger.info(f"Executing SSH Command: {command}")
stdin, stdout, stderr = ssh_client.exec_command(command)
exit_status = stdout.channel.recv_exit_status()
stdout_str = stdout.read().decode().strip()
stderr_str = stderr.read().decode().strip()
return stdout_str, stderr_str, exit_status
except Exception as e:
app.logger.error(f'SSH command execution error: {e}')
return None, str(e), -1
def create_docker_compose_content(container_name, version, plan_details, rdp_username, rdp_password, vnc_password, public_port_8006, public_port_rdp_tcp, public_port_rdp_udp, public_port_vnc):
docker_compose_template = """
version: '3'
services:
windows:
image: dockurr/windows
container_name: {{ container_name }}
environment:
VERSION: "{{ version }}"
DISK_SIZE: "{{ disk_size }}"
RAM_SIZE: "{{ ram_size }}"
CPU_CORES: "{{ cpu_cores }}"
USERNAME: "{{ username }}"
PASSWORD: "{{ password }}"
VNC_PASSWORD: "{{ vnc_password }}"
KEYBOARD: "us"
devices:
- /dev/kvm
cap_add:
- NET_ADMIN
ports:
- "{{ public_port_8006 }}:8006/tcp"
- "{{ public_port_rdp_tcp }}:3389/tcp"
- "{{ public_port_rdp_udp }}:3389/udp"
- "{{ public_port_vnc }}:5900/tcp"
stop_grace_period: 2m
restart: unless-stopped
"""
template = Template(docker_compose_template)
return template.render(
container_name=container_name,
version=version,
disk_size=plan_details['disk'],
ram_size=plan_details['ram'],
cpu_cores=plan_details['cpu'],
username=rdp_username,
password=rdp_password,
vnc_password=vnc_password,
public_port_8006=public_port_8006,
public_port_rdp_tcp=public_port_rdp_tcp,
public_port_rdp_udp=public_port_rdp_udp,
public_port_vnc=public_port_vnc
)
def is_valid_username(username):
import re
pattern = re.compile(r'^[A-Za-z0-9_]{3,30}$')
return bool(pattern.match(username))
@app.route('/api/login', methods=['POST'])
def login():
try:
app.logger.info('Processing login request')
db = load_db()
data = request.get_json()
if not data:
app.logger.warning('No JSON data in login request')
return jsonify({'error': 'No JSON data provided'}), 400
username = data.get('username')
password = data.get('password')
app.logger.info(f'Login attempt for user: {username}')
if not username or not password:
app.logger.warning('Missing username or password in login request')
return jsonify({'error': 'Username and password are required'}), 400
user_data = db['users'].get(username)
if not user_data:
app.logger.warning(f'Login failed: User {username} not found')
return jsonify({'error': 'Invalid credentials'}), 401
if password == user_data['password'] or pbkdf2_sha256.verify(password, user_data['password']):
app.logger.info(f'Login successful for user: {username}')
access_token = create_access_token(identity=username)
try:
log_activity(username, 'login', request.headers.get('User-Agent', ''))
except Exception as e:
app.logger.error(f'Failed to log login activity: {str(e)}')
return jsonify({
'token': access_token,
'username': username,
'balance': user_data.get('balance', 0)
}), 200
app.logger.warning(f'Login failed: Invalid password for user {username}')
return jsonify({'error': 'Invalid credentials'}), 401
except Exception as e:
app.logger.error(f'Login error: {str(e)}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/register', methods=['POST'])
def register():
try:
app.logger.info('Processing registration request')
db = load_db()
data = request.get_json()
if not data:
app.logger.warning('No JSON data in registration request')
return jsonify({'error': 'No JSON data provided'}), 400
username = data.get('username')
password = data.get('password')
app.logger.info(f'Registration attempt for username: {username}')
if not username or not password:
app.logger.warning('Missing username or password in registration request')
return jsonify({'error': 'Username and password are required'}), 400
if not is_valid_username(username):
app.logger.warning(f'Invalid username attempted: {username}')
return jsonify({'error': 'Invalid username. Please use only letters, numbers, and underscores. Length between 3-30 characters.'}), 400
if username in db['users']:
app.logger.warning(f'Registration failed: Username {username} already exists')
return jsonify({'error': 'Username already taken'}), 400
hashed_password = pbkdf2_sha256.hash(password)
db['users'][username] = {
'password': hashed_password,
'servers': [],
'balance': 0,
'tickets': [],
'logs': [],
'created_at': datetime.now().isoformat()
}
save_db(db)
app.logger.info(f'Registration successful for user: {username}')
log_activity(username, 'register', request.headers.get('User-Agent', ''))
return jsonify({'message': 'Registration successful'}), 201
except Exception as e:
app.logger.error(f'Registration error: {str(e)}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/servers', methods=['GET', 'POST'])
@jwt_required()
def manage_servers():
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
app.logger.warning(f'User {current_user} not found when accessing /api/servers')
return jsonify({'error': 'User not found'}), 404
if request.method == 'GET':
servers = user_data.get('servers', [])
return jsonify(servers), 200
elif request.method == 'POST':
data = request.get_json()
if not data or 'plan' not in data or 'os' not in data or 'name' not in data:
app.logger.warning('Invalid request data for server creation')
return jsonify({'error': 'Invalid request data'}), 400
plan_key = data['plan']
os_selected = data['os']
selected_plan = next((p for p in SERVER_PLANS if p['key'] == plan_key), None)
if not selected_plan:
app.logger.error(f"Invalid plan selected: {plan_key}")
return jsonify({'error': 'Invalid plan selected'}), 400
if user_data.get('balance', 0) < selected_plan['price']:
app.logger.error(f"User {current_user} has insufficient funds.")
return jsonify({'error': 'Insufficient funds'}), 400
if os_selected not in OS_OPTIONS:
app.logger.error(f"Invalid operating system selected: {os_selected}")
return jsonify({'error': 'Invalid operating system selected'}), 400
container_name = f"server_{uuid.uuid4().hex[:8]}"
rdp_password = generate_password()
vnc_password = generate_password()
public_port_8006 = random.randint(10000, 60000)
public_port_rdp_tcp = random.randint(10000, 60000)
public_port_rdp_udp = public_port_rdp_tcp
public_port_vnc = random.randint(5900, 6000)
docker_compose_content = create_docker_compose_content(
container_name, os_selected, selected_plan,
data.get('rdp_username', 'admin'), rdp_password,
vnc_password, # Pass VNC password
public_port_8006, public_port_rdp_tcp, public_port_rdp_udp,
public_port_vnc # Pass VNC port
)
ssh_client = connect_via_ssh()
if not ssh_client:
return jsonify({'error': 'Failed to establish SSH connection'}), 500
remote_path = get_remote_server_dir(container_name)
try:
mkdir_command = f"mkdir -p {remote_path}"
app.logger.info(f"Creating remote directory: {remote_path}")
stdout_str, stderr_str, exit_status = execute_ssh_command(mkdir_command, ssh_client)
if exit_status != 0:
app.logger.error(f"Failed to create remote directory {remote_path}: {stderr_str}")
raise Exception(f"Failed to create remote directory: {stderr_str}")
app.logger.info(f"Remote directory created: {remote_path}")
docker_compose_command = f"""cat <<EOF > {remote_path}/docker-compose.yml
{docker_compose_content}
EOF
"""
app.logger.info(f"Creating docker-compose.yml on remote server for {container_name}")
stdout_str, stderr_str, exit_status = execute_ssh_command(docker_compose_command, ssh_client)
if exit_status != 0:
app.logger.error(f"Error creating docker-compose.yml on remote server: {stderr_str}")
raise Exception(f"Failed to create docker-compose.yml: {stderr_str}")
app.logger.info(f"docker-compose.yml created successfully at {remote_path}")
validate_command = f"docker-compose -f {remote_path}/docker-compose.yml config"
validate_stdout, validate_stderr, validate_exit_status = execute_ssh_command(validate_command, ssh_client)
if validate_exit_status != 0:
app.logger.error(f'Docker Compose Validation Error: {validate_stderr}')
raise Exception(f'Docker Compose validation failed: {validate_stderr}')
else:
app.logger.info("Docker Compose configuration is valid.")
deploy_command = f"docker-compose -f {remote_path}/docker-compose.yml up -d"
app.logger.info(f"Deploying server {container_name} on remote server")
stdout_str, stderr_str, deploy_exit_status = execute_ssh_command(deploy_command, ssh_client)
if deploy_exit_status != 0:
app.logger.error(f'Docker Compose Deployment Error: {stderr_str}')
raise Exception(f'Docker Compose deployment failed: {stderr_str}')
else:
app.logger.info(f"Server {container_name} deployed successfully.")
except Exception as e:
app.logger.error(f'Error during SSH operation: {e}')
return jsonify({'error': f'Failed to deploy server: {str(e)}'}), 500
finally:
ssh_client.close()
new_server = {
'id': len(user_data['servers']) + 1,
'name': data['name'],
'plan': {
'key': selected_plan['key'],
'name': selected_plan['name'],
'cpu': selected_plan['cpu'],
'ram': selected_plan['ram'],
'disk': selected_plan['disk'],
'price': selected_plan['price']
},
'os': OS_OPTIONS[os_selected],
'status': 'active',
'ip': generate_unique_ip(),
'created_at': datetime.now().isoformat(),
'container_name': container_name,
'ports': {
'8006/tcp': public_port_8006,
'3389/tcp': public_port_rdp_tcp,
'3389/udp': public_port_rdp_udp,
'5900/tcp': public_port_vnc
},
'vnc_password': vnc_password,
'vnc_address': f"http://{generate_unique_ip()}:{public_port_vnc}"
}
user_data['balance'] -= selected_plan['price']
user_data['servers'].append(new_server)
save_db(db)
log_activity(current_user, 'create_server', request.headers.get('User-Agent', ''))
return jsonify(new_server), 201
def monthly_billing_check():
app.logger.info("Checking for monthly billing...")
try:
db = load_db()
for username, user in db['users'].items():
for server in user.get('servers', []):
if server.get("next_payment_due"):
due_date = datetime.fromisoformat(server["next_payment_due"])
if datetime.now() >= due_date:
handle_billing_cycle(user, server)
save_db(db)
except Exception as e:
app.logger.error(f"Error during monthly billing check: {e}")
app.logger.error(traceback.format_exc())
def handle_billing_cycle(user, server):
plan = server["plan"]
plan_price = plan["price"]
if user.get("balance", 0) >= plan_price:
user["balance"] -= plan_price
server["next_payment_due"] = (datetime.now() + timedelta(days=30)).isoformat()
app.logger.info(f"Successfully billed user for server '{server['name']}'. Next payment due on {server['next_payment_due']}.")
else:
server["status"] = "Заморожен"
server["freeze_reason"] = "Недостаточно средств."
server["freeze_date"] = datetime.now().isoformat()
app.logger.info(f"Server '{server['name']}' frozen due to insufficient funds.")
def check_frozen_servers():
app.logger.info("Checking for frozen servers...")
try:
db = load_db()
for username, user in db['users'].items():
servers_to_remove = []
for server in user.get('servers', []):
if server["status"] == "Заморожен":
freeze_date = datetime.fromisoformat(server["freeze_date"])
if datetime.now() > freeze_date + timedelta(days=3):
ssh_client = connect_via_ssh()
if not ssh_client:
app.logger.error(f"Failed to establish SSH connection to delete frozen server '{server['name']}'.")
continue
try:
execute_command = f"docker-compose -f {get_remote_server_dir(server['container_name'])}/docker-compose.yml kill && docker-compose -f {get_remote_server_dir(server['container_name'])}/docker-compose.yml rm -f"
app.logger.info(f"Deleting frozen server '{server['name']}' from remote server.")
stdout_str, stderr_str, exit_status = execute_ssh_command(execute_command, ssh_client)
if exit_status != 0:
app.logger.error(f"Failed to delete server '{server['name']}': {stderr_str}")
continue
servers_to_remove.append(server)
app.logger.info(f"Server '{server['name']}' deleted after being frozen for over 3 days.")
finally:
ssh_client.close()
for server in servers_to_remove:
user['servers'].remove(server)
save_db(db)
except Exception as e:
app.logger.error(f"Error during frozen server check: {e}")
app.logger.error(traceback.format_exc())
def initialize_scheduler():
scheduler = BackgroundScheduler()
scheduler.add_job(func=monthly_billing_check, trigger="interval", days=30)
scheduler.add_job(func=check_frozen_servers, trigger="interval", hours=24)
scheduler.start()
app.logger.info("Scheduler started for billing and frozen server checks.")
@app.route('/api/servers/<int:server_id>', methods=['GET'])
@jwt_required()
def get_server(server_id):
try:
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
app.logger.warning(f'User {current_user} not found when accessing /api/servers/{server_id}')
return jsonify({'error': 'User not found'}), 404
server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
if not server:
app.logger.warning(f'Server with ID {server_id} not found for user {current_user}')
return jsonify({'error': 'Server not found'}), 404
plan = server.get('plan', {})
plan_key = plan.get('key')
plan_name = plan.get('name', 'Unknown')
if not plan_key:
app.logger.error(f"Server {server_id} has a plan without a 'key'. Cannot process request.")
return jsonify({'error': "Server's plan information is incomplete. Please contact support."}), 500
formatted_server = {
'id': server['id'],
'name': server['name'],
'plan': {
'key': plan_key,
'name': plan_name,
'cpu': plan.get('cpu'),
'ram': plan.get('ram'),
'disk': plan.get('disk'),
'price': plan.get('price')
},
'os': server['os'],
'status': server['status'],
'ip': server['ip'],
'created_at': server['created_at'],
'container_name': server['container_name'],
'ports': server['ports'],
'vnc_address': server.get('vnc_address'),
'vnc_password': server.get('vnc_password')
}
return jsonify(formatted_server), 200
except Exception as e:
app.logger.error(f'Error in get_server endpoint: {str(e)}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/servers/<int:server_id>/toggle', methods=['POST'])
@jwt_required()
def toggle_server(server_id):
try:
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
return jsonify({'error': 'User not found'}), 404
server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
if not server:
return jsonify({'error': 'Server not found'}), 404
container_name = server.get('container_name')
if not container_name:
app.logger.error('Container name not found for this server')
return jsonify({'error': 'Container name not found for this server. Please contact support.'}), 400
server_dir = get_remote_server_dir(container_name)
if server['status'] == 'active':
cmd = f"docker-compose -f {server_dir}/docker-compose.yml stop"
new_status = 'frozen'
else:
cmd = f"docker-compose -f {server_dir}/docker-compose.yml start"
new_status = 'active'
ssh_client = connect_via_ssh()
if not ssh_client:
return jsonify({'error': 'Failed to establish SSH connection'}), 500
try:
stdout_str, stderr_str, exit_status = execute_ssh_command(cmd, ssh_client)
if exit_status != 0:
app.logger.error(f'SSH command error: {stderr_str}')
return jsonify({'error': f'Failed to toggle server status: {stderr_str}'}), 500
server['status'] = new_status
save_db(db)
log_activity(current_user, f'toggle_server_{server_id}', request.headers.get('User-Agent', ''))
return jsonify({'message': f"Server status changed to {server['status']}"}), 200
except Exception as e:
app.logger.error(f'Error during SSH operation: {e}')
return jsonify({'error': f'Failed to toggle server status: {str(e)}'}), 500
finally:
ssh_client.close()
except Exception as e:
app.logger.error(f'Unhandled exception in toggle_server: {str(e)}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/servers/<int:server_id>', methods=['DELETE'])
@jwt_required()
def delete_server(server_id):
try:
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
return jsonify({'error': 'User not found'}), 404
server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
if not server:
return jsonify({'error': 'Server not found'}), 404
container_name = server.get('container_name')
if not container_name:
return jsonify({'error': 'Container name not found for this server'}), 400
server_dir = get_remote_server_dir(container_name)
commands = [
f"docker-compose -f {server_dir}/docker-compose.yml down",
f"rm -rf {server_dir}",
"docker system prune -f" # Clean up unused Docker objects
]
full_command = ' && '.join(commands)
ssh_client = connect_via_ssh()
if not ssh_client:
app.logger.error('Failed to establish SSH connection for deleting server.')
return jsonify({'error': 'Failed to establish SSH connection'}), 500
try:
stdout_str, stderr_str, exit_status = execute_ssh_command(full_command, ssh_client)
if exit_status != 0:
app.logger.error(f'SSH command error: {stderr_str}')
return jsonify({'error': f'Failed to delete server: {stderr_str}'}), 500
user_data['servers'].remove(server)
save_db(db)
log_activity(current_user, f'delete_server_{server_id}', request.headers.get('User-Agent', ''))
app.logger.info(f"Server {server_id} deleted successfully for user {current_user}.")
return jsonify({'message': 'Server deleted successfully'}), 200
except Exception as e:
app.logger.error(f'Error during SSH operation: {e}')
return jsonify({'error': f'Failed to delete server: {str(e)}'}), 500
finally:
ssh_client.close()
except Exception as e:
app.logger.error(f'Unhandled exception in delete_server: {str(e)}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/servers/<int:server_id>', methods=['PUT'])
@jwt_required()
def update_server(server_id):
try:
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
return jsonify({'error': 'User not found'}), 404
server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
if not server:
return jsonify({'error': 'Server not found'}), 404
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
new_name = data.get('name')
new_os = data.get('os')
if new_name:
server['name'] = new_name
app.logger.info(f"Server ID {server_id} name updated to {new_name} by user {current_user}")
if new_os:
if new_os not in OS_OPTIONS:
app.logger.error(f"Invalid operating system selected: {new_os}")
return jsonify({'error': 'Invalid operating system selected'}), 400
container_name = server.get('container_name')
server_dir = get_remote_server_dir(container_name)
docker_compose_path = posixpath.join(server_dir, 'docker-compose.yml')
docker_compose_content = create_docker_compose_content(
container_name, new_os, server['plan'],
server.get('username', 'admin'), server.get('password', rdp_password),
server.get('vnc_password', generate_password()),
server['ports']['8006/tcp'], server['ports']['3389/tcp'],
server['ports']['3389/udp'], server['ports']['5900/tcp']
)
ssh_client = connect_via_ssh()
if not ssh_client:
app.logger.error('Failed to establish SSH connection for updating server.')
return jsonify({'error': 'Failed to establish SSH connection'}), 500
try:
docker_compose_command = f"""cat <<EOF > {docker_compose_path}
{docker_compose_content}
EOF
"""
app.logger.info(f"Updating docker-compose.yml on remote server for {container_name}")
stdout_str, stderr_str, exit_status = execute_ssh_command(docker_compose_command, ssh_client)
if exit_status != 0:
app.logger.error(f"Error updating docker-compose.yml on remote server: {stderr_str}")
raise Exception(f"Failed to update docker-compose.yml: {stderr_str}")
app.logger.info(f"docker-compose.yml updated successfully at {docker_compose_path}")
restart_command = f"docker-compose -f {docker_compose_path} up -d"
app.logger.info(f"Restarting server {container_name} to apply OS changes")
stdout_str, stderr_str, exit_status = execute_ssh_command(restart_command, ssh_client)
if exit_status != 0:
app.logger.error(f"Error restarting server on remote: {stderr_str}")
raise Exception(f"Failed to restart server: {stderr_str}")
app.logger.info(f"Server {container_name} restarted successfully with new OS")
server['os'] = OS_OPTIONS[new_os]
except Exception as e:
app.logger.error(f'Error during SSH operation: {e}')
return jsonify({'error': f'Failed to update server: {str(e)}'}), 500
finally:
ssh_client.close()
save_db(db)
log_activity(current_user, f'update_server_{server_id}', request.headers.get('User-Agent', ''))
return jsonify({'message': 'Server updated successfully', 'server': server}), 200
except Exception as e:
app.logger.error(f'Error in update_server endpoint: {e}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/userinfo', methods=['GET', 'PUT'])
@jwt_required()
def userinfo():
try:
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
app.logger.warning(f'User {current_user} not found when accessing /api/userinfo')
return jsonify({'error': 'User not found'}), 404
if request.method == 'GET':
return jsonify({'username': current_user, 'balance': user_data.get('balance', 0)}), 200
elif request.method == 'PUT':
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
current_password = data.get('current_password')
new_username = data.get('new_username')
new_password = data.get('new_password')
if not current_password:
return jsonify({'error': 'Current password is required'}), 400
if not (current_password == user_data['password'] or pbkdf2_sha256.verify(current_password, user_data['password'])):
return jsonify({'error': 'Current password is incorrect'}), 401
if new_username:
if not is_valid_username(new_username):
return jsonify({'error': 'Invalid new username. Please use only letters, numbers, and underscores. Length between 3-30 characters.'}), 400
if new_username in db['users']:
return jsonify({'error': 'Username already taken'}), 400
db['users'][new_username] = db['users'].pop(current_user)
current_user = new_username
app.logger.info(f"User changed username to {new_username}")
if new_password:
db['users'][current_user]['password'] = pbkdf2_sha256.hash(new_password)
app.logger.info(f"User {current_user} updated their password")
save_db(db)
log_activity(current_user, 'update_userinfo', request.headers.get('User-Agent', ''))
return jsonify({'message': 'User information updated successfully', 'username': current_user}), 200
except Exception as e:
app.logger.error(f'Error in userinfo endpoint: {e}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/tickets', methods=['GET', 'POST'])
@jwt_required()
def tickets():
try:
current_user = get_jwt_identity()
db = load_db()
if request.method == 'GET':
user_data = db['users'].get(current_user)
if not user_data:
return jsonify({'error': 'User not found'}), 404
return jsonify(user_data.get('tickets', [])), 200
elif request.method == 'POST':
data = request.get_json()
if not data or 'title' not in data or 'description' not in data:
return jsonify({'error': 'Invalid request data'}), 400
user_data = db['users'].get(current_user)
if not user_data:
return jsonify({'error': 'User not found'}), 404
new_ticket = {
'id': len(user_data['tickets']) + 1,
'title': data['title'],
'description': data.get('description', ''),
'status': 'open',
'created_at': datetime.now().isoformat(),
'messages': []
}
user_data['tickets'].append(new_ticket)
save_db(db)
log_activity(current_user, 'create_ticket', request.headers.get('User-Agent', ''))
return jsonify(new_ticket), 201
except Exception as e:
app.logger.error(f'Error in tickets endpoint: {e}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/tickets/<int:ticket_id>', methods=['GET'])
@jwt_required()
def get_ticket(ticket_id):
try:
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
return jsonify({'error': 'User not found'}), 404
ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
if not ticket:
return jsonify({'error': 'Ticket not found'}), 404
return jsonify(ticket), 200
except Exception as e:
app.logger.error(f'Error in get_ticket endpoint: {e}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
@app.route('/api/tickets/<int:ticket_id>/reply', methods=['POST'])
@jwt_required()
def reply_ticket(ticket_id):
try:
current_user = get_jwt_identity()
db = load_db()
user_data = db['users'].get(current_user)
if not user_data:
return jsonify({'error': 'User not found'}), 404
ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
if not ticket:
return jsonify({'error': 'Ticket not found'}), 404
data = request.get_json()
if not data or 'message' not in data:
return jsonify({'error': 'Message is required'}), 400
message = {
'content': data['message'],
'sender': current_user,
'timestamp': datetime.now().isoformat()
}
ticket['messages'].append(message)
save_db(db)
log_activity(current_user, f'reply_ticket_{ticket_id}', request.headers.get('User-Agent', ''))
return jsonify(message), 201
except Exception as e:
app.logger.error(f'Error in reply_ticket endpoint: {e}')
app.logger.error(traceback.format_exc())
return jsonify({'error': 'An internal server error occurred'}), 500
# Scheduler Initialization
initialize_scheduler()
if __name__ == '__main__':
setup_logging()
app.run(host='0.0.0.0', port=5000)
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Управление серверами</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="container">
<!-- Auth forms container -->
<div id="auth-container">
<!-- Форма входа -->
<div id="login-form" class="form-container">
<h2><p align="center">Вход в систему</p></h2>
<form id="login">
<input type="text" id="login-username" placeholder="Имя пользователя" required>
<input type="password" id="login-password" placeholder="Пароль" required>
<button type="submit" class="btn primary">Войти</button>
<p>Нет аккаунта? <a href="#" id="show-register">Зарегистрироваться</a></p>
</form>
</div>
<!-- Форма регистрации -->
<div id="register-form" class="form-container hidden">
<h2><p align="center">Регистрация</p></h2>
<form id="register">
<input type="text" id="reg-username" placeholder="Имя пользователя" required>
<input type="password" id="reg-password" placeholder="Пароль" required>
<button type="submit" class="btn primary">Зарегистрироваться</button>
<p>Уже есть аккаунт? <a href="#" id="show-login">Войти</a></p>
</form>
</div>
</div>
<!-- User panel container -->
<div id="main-content" class="hidden">
<header>
<div id="user-info">
<span id="username"></span>
<span id="balance"></span>
</div>
<button id="settings-btn" class="btn secondary">Настройки</button>
<button id="logout-btn" class="btn primary">Выйти</button>
</header>
<nav>
<button class="tab-btn active" data-tab="servers">Серверы</button>
<button class="tab-btn" data-tab="shop">Магазин</button>
<button class="tab-btn" data-tab="support">Поддержка</button>
</nav>
<!-- Секция серверов -->
<section id="servers" class="tab-content">
<h2>Мои серверы</h2>
<div id="servers-list" class="grid"></div>
<button class="btn primary" id="open-buy-server">Создать сервер</button>
</section>
<!-- Секция магазина -->
<section id="shop" class="tab-content hidden">
<h2>Магазин серверов</h2>
<div class="plans-grid">
<div class="plan-card">
<h3>Базовый</h3>
<p>2 CPU / 4 GB RAM</p>
<p class="price">500 ₽/мес</p>
<button class="btn primary buy-server" data-plan="basic">Купить</button>
</div>
<div class="plan-card">
<h3>Продвинутый</h3>
<p>4 CPU / 8 GB RAM</p>
<p class="price">1000 ₽/мес</p>
<button class="btn primary buy-server" data-plan="advanced">Купить</button>
</div>
<div class="plan-card">
<h3>Премиум</h3>
<p>8 CPU / 16 GB RAM</p>
<p class="price">2000 ₽/мес</p>
<button class="btn primary buy-server" data-plan="premium">Купить</button>
</div>
</div>
</section>
<!-- Секция поддержки -->
<section id="support" class="tab-content hidden">
<h2>Поддержка</h2>
<div class="support-container">
<form id="ticket-form">
<input type="text" id="ticket-title" placeholder="Тема" required>
<textarea id="ticket-description" placeholder="Описание проблемы" required></textarea>
<button type="submit" class="btn primary">Отправить</button>
</form>
<div id="tickets-list"></div>
</div>
</section>
</div>
</div>
<!-- Notification Container -->
<div id="notification-container"></div>
<!-- Ticket Detail Modal -->
<div id="ticket-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-ticket-title">Тема Тикета</h3>
<button class="close-modal" id="close-ticket-modal">×</button>
</div>
<div class="modal-body">
<p id="modal-ticket-description">Описание</p>
<div id="modal-ticket-messages"></div>
<form id="reply-form">
<textarea id="reply-message" placeholder="Ваш ответ" required></textarea>
<button type="submit" class="btn primary">Отправить ответ</button>
</form>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>Настройки</h3>
<button class="close-modal" id="close-settings-modal">×</button>
</div>
<div class="modal-body">
<form id="settings-form">
<div class="form-group">
<label for="new-username">Новое имя пользователя</label>
<input type="text" id="new-username" placeholder="Имя пользователя">
</div>
<div class="form-group">
<label for="current-password">Текущий пароль</label>
<input type="password" id="current-password" placeholder="Текущий пароль" required>
</div>
<div class="form-group">
<label for="new-password">Новый пароль</label>
<input type="password" id="new-password" placeholder="Новый пароль">
</div>
<button type="submit" class="btn primary">Сохранить</button>
</form>
</div>
</div>
</div>
<!-- Buy Server Modal -->
<div id="buy-server-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>Создать сервер</h3>
<button class="close-modal" id="close-buy-server-modal">×</button>
</div>
<div class="modal-body">
<form id="buy-server-form">
<div class="form-group">
<label for="server-name">Имя сервера</label>
<input type="text" id="server-name" placeholder="Введите имя сервера" required>
</div>
<div class="form-group">
<label for="server-os">Операционная система</label>
<select id="server-os" required>
<option value="" disabled selected>Выберите ОС</option>
<option value="win2020">Windows Server 2020</option>
<option value="win2022">Windows Server 2022</option>
<option value="win2025">Windows Server 2025</option>
<option value="win10">Windows 10</option>
<option value="win11">Windows 11</option>
</select>
</div>
<button type="submit" class="btn primary">Купить сервер</button>
</form>
</div>
</div>
</div>
<div id="servers" class="tab-content hidden">
<h2>Ваши Серверы</h2>
<div id="servers-list">
<!-- Server cards will be injected here by JavaScript -->
</div>
</div>
<!-- Server Details Modal -->
<div id="server-details-modal" class="modal hidden">
<div class="modal-content">
<span class="close" id="close-server-details-modal">×</span>
<h2>Детали сервера</h2>
<p><strong>Имя:</strong> <span id="modal-server-name"></span></p>
<p><strong>IP:</strong> <span id="modal-server-ip"></span></p>
<p><strong>VNC Адрес:</strong> <span id="modal-server-vnc-address"></span></p>
<p><strong>VNC Пароль:</strong> <span id="modal-server-vnc-password"></span> <button class="btn show-password-btn" id="toggle-vnc-password-btn">Показать пароль</button></p>
<p><strong>План:</strong> <span id="modal-server-plan"></span></p>
<p><strong>ОС:</strong> <span id="modal-server-os"></span></p>
<p><strong>Статус:</strong> <span id="modal-server-status"></span></p>
<div id="server-ports"></div>
</div>
</div>
<!-- Update Server Modal -->
<div id="update-server-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>Update Server</h3>
<button class="close-modal" id="close-update-server-modal">×</button>
</div>
<div class="modal-body">
<form id="update-server-form">
<input type="hidden" id="update-server-id">
<div class="form-group">
<label for="update-server-os">Operating System</label>
<select id="update-server-os" required>
<option value="" disabled selected>Select OS</option>
<option value="win2020">Windows Server 2020</option>
<option value="win2022">Windows Server 2022</option>
<option value="win2025">Windows Server 2025</option>
<option value="win10">Windows 10</option>
<option value="win11">Windows 11</option>
</select>
</div>
<button type="submit" class="btn primary">Update Server</button>
</form>
</div>
</div>
</div>
<script src="/static/script.js"></script>
</body>
</html>
const API_URL = '/api';
// Plan Configs
const planConfigs = {
basic: { key: 'basic', name: 'Базовый', cpu: 2, ram: 4, disk: '50GB', price: 500 },
advanced: { key: 'advanced', name: 'Продвинутый', cpu: 4, ram: 8, disk: '100GB', price: 1000 },
premium: { key: 'premium', name: 'Премиум', cpu: 8, ram: 16, disk: '200GB', price: 2000 }
};
// Utility Functions
const showElement = (id) => document.getElementById(id).classList.remove('hidden');
const hideElement = (id) => document.getElementById(id).classList.add('hidden');
const setActiveTab = (tabId) => {
const contents = document.querySelectorAll('.tab-content');
contents.forEach(content => {
content.classList.remove('active');
content.style.opacity = 0; // Fade out
});
const newContent = document.getElementById(tabId);
newContent.classList.add('active');
setTimeout(() => {
newContent.style.opacity = 1; // Fade in
}, 50);
};
const getAuthHeaders = () => {
const token = localStorage.getItem('authToken');
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
};
function showNotification(message, type = 'error') {
const container = document.getElementById('notification-container');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.innerHTML = `
<span>${message}</span>
<button class="close-notification">×</button>
`;
container.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 500);
}, 5000);
notification.querySelector('.close-notification').addEventListener('click', () => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 500);
});
}
function logout() {
localStorage.removeItem('authToken');
localStorage.removeItem('username');
localStorage.removeItem('balance');
currentUser = null;
updateUserInterface();
location.reload(); // Reload the page after logout
}
// Registration Handler
document.getElementById('register').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('reg-username').value.trim();
const password = document.getElementById('reg-password').value.trim();
if (!username || !password) {
showNotification('Пожалуйста, заполните все поля', 'error');
return;
}
try {
const response = await fetch(`${API_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
showNotification('Регистрация успешна! Теперь вы можете войти.', 'success');
showElement('login-form');
hideElement('register-form');
} else {
showNotification(data.error || 'Ошибка регистрации', 'error');
}
} catch (error) {
console.error('Ошибка регистрации:', error);
showNotification('Ошибка при регистрации', 'error');
}
});
// Login Handler
document.getElementById('login').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value.trim();
if (!username || !password) {
showNotification('Пожалуйста, заполните все поля', 'error');
return;
}
try {
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.token) {
localStorage.setItem('authToken', data.token);
localStorage.setItem('username', data.username);
localStorage.setItem('balance', data.balance);
currentUser = {
username: data.username,
balance: data.balance
};
updateUserInterface();
setActiveTab('servers');
showNotification('Вход выполнен успешно!', 'success');
} else {
showNotification('Неверные учетные данные', 'error');
}
} catch (error) {
console.error('Ошибка входа:', error);
showNotification(`Ошибка при входе в систему: ${error.message}`, 'error');
}
});
// Authentication Check
function isAuthenticated() {
return localStorage.getItem('authToken') !== null;
}
// Manage Servers
async function loadServers() {
try {
const response = await fetch(`${API_URL}/servers`, {
method: 'GET',
headers: getAuthHeaders()
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const servers = await response.json();
displayServers(servers);
} catch (error) {
console.error('Ошибка загрузки серверов:', error);
showNotification(error.message, 'error');
}
}
function displayServers(servers) {
const serversList = document.getElementById('servers-list');
serversList.innerHTML = '';
if (!Array.isArray(servers) || servers.length === 0) {
serversList.innerHTML = '<p>У вас нет серверов.</p>';
return;
}
servers.forEach(server => {
const serverCard = document.createElement('div');
serverCard.className = 'server-card';
serverCard.innerHTML = `
<h3>${server.name}</h3>
<span class="server-status status-${server.status}">${server.status}</span>
<p><strong>IP:</strong> ${server.ip}</p>
<p><strong>VNC:</strong> <a href="${server.vnc_address}" target="_blank">${server.vnc_address}</a></p>
<p><strong>VNC Пароль:</strong> <span class="hidden-password">${server.vnc_password}</span> <button class="btn show-password-btn" data-server-id="${server.id}">Показать</button></p>
<p>План: ${server.plan.name}</p>
<p>CPU: ${server.plan.cpu} / RAM: ${server.plan.ram}GB</p>
<button class="btn primary view-server" data-server-id="${server.id}">Просмотреть</button>
<button class="btn secondary update-server" data-server-id="${server.id}">Обновить</button>
<button class="btn danger toggle-server" data-server-id="${server.id}">${server.status === 'active' ? 'Остановить' : 'Запустить'}</button>
<button class="btn danger delete-server" data-server-id="${server.id}">Удалить</button>
<button class="btn secondary restart-server" data-server-id="${server.id}">Перезапустить</button>
`;
serversList.appendChild(serverCard);
});
attachServerEventListeners();
attachShowPasswordButtons();
}
function attachServerEventListeners() {
// View Server Details
document.querySelectorAll('.view-server').forEach(button => {
button.addEventListener('click', () => {
const serverId = button.getAttribute('data-server-id');
openServerDetailsModal(serverId);
});
});
// Update Server
document.querySelectorAll('.update-server').forEach(button => {
button.addEventListener('click', () => {
const serverId = button.getAttribute('data-server-id');
openUpdateServerModal(serverId);
});
});
// Toggle Server Status
document.querySelectorAll('.toggle-server').forEach(button => {
button.addEventListener('click', () => {
const serverId = button.getAttribute('data-server-id');
const currentStatus = button.textContent.includes('Остановить') ? 'active' : 'frozen';
toggleServerStatus(serverId, currentStatus);
});
});
// Delete Server
document.querySelectorAll('.delete-server').forEach(button => {
button.addEventListener('click', () => {
const serverId = button.getAttribute('data-server-id');
deleteServer(serverId);
});
});
// Restart Server
document.querySelectorAll('.restart-server').forEach(button => {
button.addEventListener('click', () => {
const serverId = button.getAttribute('data-server-id');
restartServer(serverId);
});
});
}
function attachShowPasswordButtons() {
document.querySelectorAll('.show-password-btn').forEach(button => {
button.addEventListener('click', () => {
const passwordSpan = button.parentElement.querySelector('.hidden-password');
passwordSpan.classList.toggle('hidden-password');
passwordSpan.classList.toggle('visible-password');
button.textContent = button.textContent === 'Показать' ? 'Скрыть' : 'Показать';
});
});
}
// Function to toggle server status
async function toggleServerStatus(serverId) {
try {
const response = await fetch(`${API_URL}/servers/${serverId}/toggle`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
showNotification(result.message, 'success');
loadServers();
} catch (error) {
console.error('Ошибка при переключении сервера:', error);
showNotification(error.message, 'error');
}
}
// Function to delete a server
async function deleteServer(serverId) {
if (!confirm('Вы уверены, что хотите удалить этот сервер? Это действие нельзя отменить.')) return;
try {
const response = await fetch(`${API_URL}/servers/${serverId}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
showNotification(result.message, 'success');
loadServers();
} catch (error) {
console.error('Ошибка при удалении сервера:', error);
showNotification(error.message, 'error');
}
}
// Function to restart a server
async function restartServer(serverId) {
try {
const response = await fetch(`${API_URL}/servers/${serverId}/restart`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
showNotification(result.message, 'success');
loadServers();
} catch (error) {
console.error('Ошибка при перезагрузке сервера:', error);
showNotification(error.message, 'error');
}
}
// Manage Tickets
document.getElementById('ticket-form').addEventListener('submit', async (e) => {
e.preventDefault();
const title = document.getElementById('ticket-title').value.trim();
const description = document.getElementById('ticket-description').value.trim();
if (!title || !description) {
showNotification('Пожалуйста, заполните все поля', 'error');
return;
}
try {
const response = await fetch(`${API_URL}/tickets`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ title, description })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Ошибка при создании тикета');
}
showNotification('Тикет успешно создан!', 'success');
document.getElementById('ticket-form').reset();
loadTickets();
} catch (error) {
console.error('Ошибка создания тикета:', error);
showNotification(error.message, 'error');
}
});
async function loadTickets() {
try {
const response = await fetch(`${API_URL}/tickets`, {
method: 'GET',
headers: getAuthHeaders()
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const tickets = await response.json();
displayTickets(tickets);
} catch (error) {
console.error('Ошибка загрузки тикетов:', error);
showNotification(error.message, 'error');
}
}
function displayTickets(tickets) {
const ticketsList = document.getElementById('tickets-list');
ticketsList.innerHTML = '';
if (!Array.isArray(tickets) || tickets.length === 0) {
ticketsList.innerHTML = '<p>У вас нет тикетов.</p>';
return;
}
tickets.forEach(ticket => {
const ticketCard = document.createElement('div');
ticketCard.className = 'ticket-card';
ticketCard.innerHTML = `
<div class="ticket-header">
<h3>${ticket.title}</h3>
<span class="ticket-status status-${ticket.status}">${ticket.status}</span>
</div>
<p>${ticket.description}</p>
<small>Создан: ${new Date(ticket.created_at).toLocaleString()}</small>
<button class="btn primary view-ticket" data-ticket-id="${ticket.id}">Просмотреть</button>
`;
ticketsList.appendChild(ticketCard);
});
document.querySelectorAll('.view-ticket').forEach(button => {
button.addEventListener('click', () => {
const ticketId = button.getAttribute('data-ticket-id');
openTicketModal(ticketId);
});
});
}
async function openTicketModal(ticketId) {
try {
const response = await fetch(`${API_URL}/tickets/${ticketId}`, {
method: 'GET',
headers: getAuthHeaders()
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const ticket = await response.json();
displayTicketDetails(ticket);
} catch (error) {
console.error('Ошибка загрузки тикета:', error);
showNotification(error.message, 'error');
}
}
function displayTicketDetails(ticket) {
document.getElementById('modal-ticket-title').textContent = ticket.title;
document.getElementById('modal-ticket-description').textContent = ticket.description;
const messagesContainer = document.getElementById('modal-ticket-messages');
messagesContainer.innerHTML = '';
if (!Array.isArray(ticket.messages) || ticket.messages.length === 0) {
messagesContainer.innerHTML = '<p>Нет сообщений.</p>';
} else {
ticket.messages.forEach(msg => {
const msgDiv = document.createElement('div');
msgDiv.className = 'ticket-message';
msgDiv.innerHTML = `
<p><strong>${msg.sender}</strong> <em>${new Date(msg.timestamp).toLocaleString()}</em></p>
<p>${msg.content}</p>
`;
messagesContainer.appendChild(msgDiv);
});
}
const ticketModal = document.getElementById('ticket-modal');
ticketModal.setAttribute('data-ticket-id', ticket.id);
showElement('ticket-modal');
}
// Settings Modal Functionality
document.getElementById('settings-btn').addEventListener('click', () => {
setActiveTab('settings');
});
document.getElementById('settings-form').addEventListener('submit', async (e) => {
e.preventDefault();
const newUsername = document.getElementById('new-username').value.trim();
const currentPassword = document.getElementById('current-password').value.trim();
const newPassword = document.getElementById('new-password').value.trim();
if (!currentPassword) {
showNotification('Текущий пароль обязателен', 'error');
return;
}
if (!newUsername && !newPassword) {
showNotification('Пожалуйста, введите новое имя пользователя или пароль', 'error');
return;
}
const payload = { current_password: currentPassword };
if (newUsername) payload.new_username = newUsername;
if (newPassword) payload.new_password = newPassword;
try {
const response = await fetch(`${API_URL}/userinfo`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(payload)
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.username) {
localStorage.setItem('username', data.username);
currentUser.username = data.username;
document.getElementById('username').textContent = `Пользователь: ${currentUser.username}`;
}
if (data.balance !== undefined) {
localStorage.setItem('balance', data.balance);
currentUser.balance = data.balance;
document.getElementById('balance').textContent = `Баланс: ${currentUser.balance} ₽`;
}
showNotification('Настройки успешно обновлены!', 'success');
} catch (error) {
console.error('Ошибка обновления настроек:', error);
showNotification(error.message, 'error');
}
});
// Function to open the buy server modal
document.getElementById('open-buy-server').addEventListener('click', () => {
showElement('buy-server-modal');
});
// Handle Buy Server Form Submission
document.getElementById('buy-server-form').addEventListener('submit', async (e) => {
e.preventDefault();
const serverName = document.getElementById('server-name').value.trim();
const serverOS = document.getElementById('server-os').value;
const selectedPlanKey = document.querySelector('.buy-server.active')?.dataset.plan;
const planDetails = planConfigs[selectedPlanKey];
if (!serverName || !serverOS || !planDetails) {
showNotification('Пожалуйста, заполните все поля и выберите план', 'error');
return;
}
try {
const response = await fetch(`${API_URL}/servers`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
name: serverName,
plan: planDetails.key,
os: serverOS
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Ошибка при создании сервера');
}
const newServer = await response.json();
showNotification('Сервер успешно создан!', 'success');
hideElement('buy-server-modal');
document.getElementById('buy-server-form').reset();
loadServers();
// Update user balance
currentUser.balance -= planDetails.price;
localStorage.setItem('balance', currentUser.balance);
document.getElementById('balance').textContent = `Баланс: ${currentUser.balance} ₽`;
} catch (error) {
console.error('Ошибка покупки сервера:', error);
showNotification(error.message, 'error');
}
});
// Open server details modal
async function openServerDetailsModal(serverId) {
try {
const response = await fetch(`${API_URL}/servers/${serverId}`, {
method: 'GET',
headers: getAuthHeaders()
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const server = await response.json();
displayServerDetails(server);
} catch (error) {
console.error('Ошибка загрузки сервера:', error);
showNotification(error.message, 'error');
}
}
function displayServerDetails(server) {
document.getElementById('modal-server-name').textContent = server.name;
document.getElementById('modal-server-ip').textContent = server.ip;
document.getElementById('modal-server-vnc-address').innerHTML = `<a href="${server.vnc_address}" target="_blank">${server.vnc_address}</a>`;
document.getElementById('modal-server-vnc-password').textContent = server.vnc_password;
document.getElementById('modal-server-vnc-password').classList.add('hidden-password');
const toggleVncPasswordBtn = document.getElementById('toggle-vnc-password-btn');
toggleVncPasswordBtn.onclick = () => {
const passwordElement = document.getElementById('modal-server-vnc-password');
if (passwordElement.classList.contains('hidden-password')) {
passwordElement.classList.remove('hidden-password');
passwordElement.classList.add('visible-password');
toggleVncPasswordBtn.textContent = 'Скрыть пароль';
} else {
passwordElement.classList.remove('visible-password');
passwordElement.classList.add('hidden-password');
toggleVncPasswordBtn.textContent = 'Показать пароль';
}
};
// Add server information
document.getElementById('modal-server-plan').textContent = server.plan.name;
document.getElementById('modal-server-os').textContent = server.os;
document.getElementById('modal-server-status').textContent = server.status;
// Populate Ports
const portsInfo = `
<p><strong>Port 8006/tcp:</strong> ${server.ports["8006/tcp"]}</p>
<p><strong>RDP TCP:</strong> ${server.ports["3389/tcp"]}</p>
<p><strong>RDP UDP:</strong> ${server.ports["3389/udp"]}</p>
<p><strong>VNC TCP:</strong> ${server.ports["5900/tcp"]}</p>
`;
document.getElementById('server-ports').innerHTML = portsInfo;
showElement('server-details-modal');
}
// ----------------- Update Server Modal Functionality -----------------
function openUpdateServerModal(serverId) {
// Prefill server ID in the form
document.getElementById('update-server-id').value = serverId;
// Optionally, preselect current OS
// Fetch server details to get current OS
fetch(`${API_URL}/servers/${serverId}`, {
method: 'GET',
headers: getAuthHeaders()
})
.then(response => {
if (!response.ok) {
throw new Error('Не удалось получить информацию о сервере');
}
return response.json();
})
.then(server => {
// Assuming server.osKey exists; if not, adjust accordingly
// If the backend doesn't return osKey, you may need to map server.os back to os_key
let osKey = Object.keys(OS_OPTIONS).find(key => OS_OPTIONS[key] === server.os);
if (!osKey) osKey = 'win2022'; // Default or handle accordingly
document.getElementById('update-server-os').value = osKey;
})
.catch(error => {
console.error('Ошибка при получении информации о сервере:', error);
showNotification(error.message, 'error');
});
showElement('update-server-modal');
}
// Handle Update Server Form Submission
document.getElementById('update-server-form').addEventListener('submit', async (e) => {
e.preventDefault();
const serverId = document.getElementById('update-server-id').value;
const newOS = document.getElementById('update-server-os').value;
if (!newOS) {
showNotification('Пожалуйста, выберите операционную систему', 'error');
return;
}
try {
const response = await fetch(`${API_URL}/servers/${serverId}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify({ os: newOS })
});
const data = await response.json();
if (response.ok) {
showNotification('Сервер обновлен успешно!', 'success');
hideElement('update-server-modal');
loadServers();
} else {
throw new Error(data.error || 'Ошибка при обновлении сервера');
}
} catch (error) {
console.error('Ошибка обновления сервера:', error);
showNotification(error.message, 'error');
}
});
// ----------------- Server Authentication Check Function -----------------
function updateUserInterface() {
const authContainer = document.getElementById('auth-container');
const mainContent = document.getElementById('main-content');
const usernameElement = document.getElementById('username');
const balanceElement = document.getElementById('balance');
if (isAuthenticated()) {
if (!currentUser) {
const username = localStorage.getItem('username');
const balance = parseFloat(localStorage.getItem('balance'));
if (username && !isNaN(balance)) {
currentUser = {
username: username,
balance: balance
};
finalizeUserInterface();
} else {
// Fetch user info from backend
fetch(`${API_URL}/userinfo`, {
method: 'GET',
headers: getAuthHeaders()
})
.then(response => {
if (!response.ok) {
throw new Error('Не удалось получить информацию о пользователе');
}
return response.json();
})
.then(data => {
currentUser = {
username: data.username,
balance: data.balance
};
// Store these details in localStorage
localStorage.setItem('username', data.username);
localStorage.setItem('balance', data.balance);
// Update UI
finalizeUserInterface();
})
.catch(error => {
console.error('Не удалось получить информацию о пользователе:', error);
logout(); // Optionally, log out the user if fetching fails
});
}
} else {
finalizeUserInterface();
}
} else {
// Show auth container and hide main content
authContainer?.classList.remove('hidden');
mainContent?.classList.add('hidden');
}
}
function finalizeUserInterface() {
const authContainer = document.getElementById('auth-container');
const mainContent = document.getElementById('main-content');
const usernameElement = document.getElementById('username');
const balanceElement = document.getElementById('balance');
authContainer?.classList.add('hidden');
mainContent?.classList.remove('hidden');
// Update user information in the header
if (usernameElement) usernameElement.textContent = `Пользователь: ${currentUser.username}`;
if (balanceElement) balanceElement.textContent = `Баланс: ${currentUser.balance} ₽`;
// Load user data
loadServers();
loadTickets();
}
// ----------------- Form Toggle Handlers -----------------
document.getElementById('show-register').addEventListener('click', (e) => {
e.preventDefault();
hideElement('login-form');
showElement('register-form');
});
document.getElementById('show-login').addEventListener('click', (e) => {
e.preventDefault();
hideElement('register-form');
showElement('login-form');
});
// ----------------- Tab Navigation Handlers -----------------
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => setActiveTab(btn.dataset.tab));
});
// ----------------- Logout Button Handler -----------------
document.getElementById('logout-btn').addEventListener('click', () => {
logout();
});
// ----------------- Initialization on Page Load -----------------
document.addEventListener('DOMContentLoaded', function() {
updateUserInterface();
});
:root {
--primary-color: #941b31;
--secondary-color: #bbb;
--success-color: #46d369;
--danger-color: #f53b57;
--light-gray: #0a0909; /* Dark background color */
--dark-gray: #eaeaea; /* Light text color */
}
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.8;
color: var(--dark-gray);
background-color: var(--light-gray);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Заголовок */
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background-color: #1e1e1e;
color: var(--dark-gray);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
#user-info {
display: flex;
align-items: center;
gap: 20px;
}
/* Формы */
.form-container {
width: 800px;
min-height: 370px;
margin: 50px auto;
padding: 40px;
background-color: #1e1e1e;
color: var(--dark-gray);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.hidden {
display: none !important;
}
form {
display: flex;
flex-direction: column;
gap: 15px;
}
input,
textarea,
select {
padding: 10px;
background-color: #333;
border: 2px solid #5c5c5c;
border-radius: 12px;
color: var(--dark-gray);
font-size: 16px;
}
textarea {
min-height: 100px;
resize: vertical;
}
/* Кнопки */
.btn {
padding: 10px 20px;
border: none;
border-radius: 9px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.btn.primary {
background-color: var(--primary-color);
color: white;
}
.btn.primary:hover {
background-color: #b51844;
}
.btn.secondary {
background-color: var(--secondary-color);
color: var(--light-gray);
}
.btn.secondary:hover {
background-color: #999;
}
.btn.danger {
background-color: var(--danger-color);
color: white;
}
.btn.danger:hover {
background-color: #c00;
}
/* Навигация */
nav {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-btn {
padding: 10px 20px;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: var(--secondary-color);
border-bottom: 2px solid transparent;
}
.tab-btn.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
/* Секция серверов и Магазина */
.grid, .plans-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.server-card, .plan-card {
background-color: #1e1e1e;
color: var(--dark-gray);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.server-card h3, .plan-card h3 {
margin-bottom: 10px;
}
.price {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
margin: 15px 0;
}
.server-card p, .plan-card p {
margin-bottom: 8px;
}
/* Поддержка */
.support-container {
max-width: 800px;
margin: 0 auto;
}
.ticket-card {
background-color: #1e1e1e;
color: var(--dark-gray);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
margin-bottom: 15px;
}
.ticket-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.ticket-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
color: var(--dark-gray);
}
.status-active {
background-color: var(--success-color);
}
.status-frozen {
background-color: var(--danger-color);
}
.status-open {
background-color: var(--primary-color);
}
.status-closed {
background-color: var(--secondary-color);
}
/* Notification */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
background-color: #1e1e1e;
color: var(--dark-gray);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
display: flex;
align-items: center;
gap: 10px;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
.notification.show {
opacity: 1;
}
.notification.success {
background-color: var(--success-color);
color: white;
}
.notification.error {
background-color: var(--danger-color);
color: white;
}
.notification .close-notification {
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
}
/* Modal Styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
}
.modal.hidden {
display: none;
}
.modal-content {
background-color: #1e1e1e;
color: var(--dark-gray);
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 600px;
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.close-modal {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--secondary-color);
}
.modal-body {
max-height: 70vh;
overflow-y: auto;
}
.ticket-message, .server-message {
margin-bottom: 10px;
}
.ticket-message .sender, .server-message .sender {
font-weight: bold;
}
.ticket-message .timestamp, .server-message .timestamp {
font-size: 12px;
color: var(--secondary-color);
}
.server-password {
background-color: #333;
padding: 8px;
border-radius: 4px;
margin-top: 10px;
cursor: pointer;
}
.hidden-password {
color: transparent;
text-shadow: 0 0 8px rgba(255,255,255,0.5);
}
.show-password {
color: var(--dark-gray);
text-shadow: none;
}
#server-ports p {
margin-bottom: 8px;
}
/* Buy Server Button */
#open-buy-server {
margin-top: 20px;
}
#update-server-form .form-group {
margin-bottom: 15px;
}
#update-server-form .form-group label {
display: block;
margin-bottom: 5px;
}
#update-server-form .form-group select {
width: 100%;
padding: 8px;
border-radius: 4px;
background-color: #333;
border: 1px solid #444;
color: var(--dark-gray);
}
/* Анимации */
.tab-content {
opacity: 0;
transition: opacity 0.3s ease-in-out;
visibility: hidden;
}
.tab-content.active {
opacity: 1;
visibility: visible;
}
/* Адаптивность */
@media (max-width: 768px) {
.container {
padding: 10px;
}
header {
flex-direction: column;
text-align: center;
gap: 15px;
}
.grid, .plans-grid {
grid-template-columns: 1fr;
}
nav {
flex-wrap: wrap;
}
.tab-btn {
flex: 1;
min-width: 120px;
}
}
/* Утилиты */
.hidden {
display: none;
}}