- Flask web application for managing Minecraft whitelists - Manages all 11 game servers (TX1 + NC1) - TailwindCSS Fire & Frost themed UI - Single player and bulk operations - HTTP Basic Auth with password hashing - Nginx reverse proxy + SSL configuration - systemd service for auto-start - Complete deployment automation Components: - app.py: Main Flask application with Pterodactyl API integration - templates/index.html: Responsive web dashboard - requirements.txt: Python dependencies - .env: Configuration with API keys and credentials - deploy.sh: Automated deployment script - DEPLOYMENT.md: Step-by-step manual deployment guide - nginx.conf: Reverse proxy configuration - whitelist-manager.service: systemd service Target: Ghost VPS (64.50.188.14) Domain: whitelist.firefrostgaming.com Login: mkrause612 / Butter2018!! Transforms 15-minute manual task into 30-second web operation. Zero-error whitelist management with full visibility. Ready for deployment - FFG-STD-001 compliant
222 lines
7.5 KiB
Python
222 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Firefrost Gaming - Whitelist Manager
|
|
A web dashboard for managing Minecraft server whitelists across all game servers.
|
|
|
|
Author: Michael "Frostystyle" Krause & Claude "The Chronicler"
|
|
Version: 1.0.0
|
|
Date: 2026-02-16
|
|
"""
|
|
|
|
import os
|
|
import requests
|
|
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
|
from flask_httpauth import HTTPBasicAuth
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
app = Flask(__name__)
|
|
auth = HTTPBasicAuth()
|
|
|
|
# Configuration from environment
|
|
PTERODACTYL_URL = os.getenv('PTERODACTYL_URL', 'https://panel.firefrostgaming.com')
|
|
PTERODACTYL_API_KEY = os.getenv('PTERODACTYL_API_KEY')
|
|
ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin')
|
|
ADMIN_PASSWORD_HASH = os.getenv('ADMIN_PASSWORD_HASH')
|
|
|
|
# Server list with UUIDs (from infrastructure manifest)
|
|
MINECRAFT_SERVERS = {
|
|
# Texas Node (TX1)
|
|
'Reclamation': '1eb33479-a6bc-4e8f-b64d-d1e4bfa0a8b4',
|
|
'Stoneblock 4': 'a0efbfe8-4b97-4a90-869d-ffe6d3072bd5',
|
|
'Society: Sunlit Valley': '9310d0a6-62a6-4fe6-82c4-eb483dc68876',
|
|
'Vanilla 1.21.11': '3bed1bda-f648-4630-801a-fe9f2e3d3f27',
|
|
'All The Mons': '668a5220-7e72-4379-9165-bdbb84bc9806',
|
|
|
|
# North Carolina Node (NC1)
|
|
'The Ember Project': '124f9060-58a7-457a-b2cf-b4024fce2951',
|
|
'Minecolonies: Create and Conquer': 'a14201d2-83b2-44e6-ae48-e6c4cbc56f24',
|
|
'All The Mods 10': '82e63949-8fbf-4a44-b32a-53324e8492bf',
|
|
'Homestead': '2f85d4ef-aa49-4dd6-b448-beb3fca1db12',
|
|
'EMC Subterra Tech': '09a95f38-9f8c-404a-9557-3a7c44258223',
|
|
}
|
|
|
|
# Authentication
|
|
users = {
|
|
ADMIN_USERNAME: ADMIN_PASSWORD_HASH
|
|
}
|
|
|
|
@auth.verify_password
|
|
def verify_password(username, password):
|
|
if username in users and check_password_hash(users.get(username), password):
|
|
return username
|
|
return None
|
|
|
|
# Helper function to send Pterodactyl API requests
|
|
def send_pterodactyl_command(server_uuid, command):
|
|
"""Send a console command to a Pterodactyl server."""
|
|
url = f"{PTERODACTYL_URL}/api/client/servers/{server_uuid}/command"
|
|
headers = {
|
|
'Authorization': f'Bearer {PTERODACTYL_API_KEY}',
|
|
'Accept': 'application/vnd.pterodactyl.v1+json',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
data = {'command': command}
|
|
|
|
try:
|
|
response = requests.post(url, headers=headers, json=data, timeout=10)
|
|
response.raise_for_status()
|
|
return True, "Command sent successfully"
|
|
except requests.exceptions.RequestException as e:
|
|
return False, str(e)
|
|
|
|
def get_server_status(server_uuid):
|
|
"""Get the current status of a Pterodactyl server."""
|
|
url = f"{PTERODACTYL_URL}/api/client/servers/{server_uuid}"
|
|
headers = {
|
|
'Authorization': f'Bearer {PTERODACTYL_API_KEY}',
|
|
'Accept': 'application/vnd.pterodactyl.v1+json'
|
|
}
|
|
|
|
try:
|
|
response = requests.get(url, headers=headers, timeout=10)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
return data['attributes'].get('status', 'unknown')
|
|
except requests.exceptions.RequestException:
|
|
return 'unknown'
|
|
|
|
# Routes
|
|
@app.route('/')
|
|
@auth.login_required
|
|
def index():
|
|
"""Main dashboard showing all servers."""
|
|
servers = []
|
|
for name, uuid in MINECRAFT_SERVERS.items():
|
|
status = get_server_status(uuid)
|
|
servers.append({
|
|
'name': name,
|
|
'uuid': uuid,
|
|
'status': status
|
|
})
|
|
return render_template('index.html', servers=servers)
|
|
|
|
@app.route('/api/whitelist/toggle', methods=['POST'])
|
|
@auth.login_required
|
|
def toggle_whitelist():
|
|
"""Toggle whitelist on/off for a specific server."""
|
|
data = request.get_json()
|
|
server_uuid = data.get('server_uuid')
|
|
enabled = data.get('enabled', True)
|
|
|
|
if not server_uuid:
|
|
return jsonify({'success': False, 'error': 'No server UUID provided'}), 400
|
|
|
|
command = 'whitelist on' if enabled else 'whitelist off'
|
|
success, message = send_pterodactyl_command(server_uuid, command)
|
|
|
|
return jsonify({'success': success, 'message': message})
|
|
|
|
@app.route('/api/whitelist/add', methods=['POST'])
|
|
@auth.login_required
|
|
def add_to_whitelist():
|
|
"""Add a player to whitelist on specified servers."""
|
|
data = request.get_json()
|
|
player = data.get('player')
|
|
server_uuids = data.get('servers', [])
|
|
|
|
if not player:
|
|
return jsonify({'success': False, 'error': 'No player name provided'}), 400
|
|
|
|
if not server_uuids:
|
|
return jsonify({'success': False, 'error': 'No servers selected'}), 400
|
|
|
|
results = []
|
|
for server_uuid in server_uuids:
|
|
command = f'whitelist add {player}'
|
|
success, message = send_pterodactyl_command(server_uuid, command)
|
|
|
|
# Get server name from UUID
|
|
server_name = next((name for name, uuid in MINECRAFT_SERVERS.items() if uuid == server_uuid), 'Unknown')
|
|
results.append({
|
|
'server': server_name,
|
|
'success': success,
|
|
'message': message
|
|
})
|
|
|
|
return jsonify({'results': results})
|
|
|
|
@app.route('/api/whitelist/remove', methods=['POST'])
|
|
@auth.login_required
|
|
def remove_from_whitelist():
|
|
"""Remove a player from whitelist on specified servers."""
|
|
data = request.get_json()
|
|
player = data.get('player')
|
|
server_uuids = data.get('servers', [])
|
|
|
|
if not player:
|
|
return jsonify({'success': False, 'error': 'No player name provided'}), 400
|
|
|
|
if not server_uuids:
|
|
return jsonify({'success': False, 'error': 'No servers selected'}), 400
|
|
|
|
results = []
|
|
for server_uuid in server_uuids:
|
|
command = f'whitelist remove {player}'
|
|
success, message = send_pterodactyl_command(server_uuid, command)
|
|
|
|
# Get server name from UUID
|
|
server_name = next((name for name, uuid in MINECRAFT_SERVERS.items() if uuid == server_uuid), 'Unknown')
|
|
results.append({
|
|
'server': server_name,
|
|
'success': success,
|
|
'message': message
|
|
})
|
|
|
|
return jsonify({'results': results})
|
|
|
|
@app.route('/api/whitelist/bulk', methods=['POST'])
|
|
@auth.login_required
|
|
def bulk_operation():
|
|
"""Perform bulk add/remove operations."""
|
|
data = request.get_json()
|
|
operation = data.get('operation') # 'add' or 'remove'
|
|
players = data.get('players', []) # List of player names
|
|
server_uuids = data.get('servers', [])
|
|
|
|
if operation not in ['add', 'remove']:
|
|
return jsonify({'success': False, 'error': 'Invalid operation'}), 400
|
|
|
|
if not players:
|
|
return jsonify({'success': False, 'error': 'No players provided'}), 400
|
|
|
|
if not server_uuids:
|
|
return jsonify({'success': False, 'error': 'No servers selected'}), 400
|
|
|
|
results = []
|
|
for player in players:
|
|
for server_uuid in server_uuids:
|
|
command = f'whitelist {operation} {player}'
|
|
success, message = send_pterodactyl_command(server_uuid, command)
|
|
|
|
server_name = next((name for name, uuid in MINECRAFT_SERVERS.items() if uuid == server_uuid), 'Unknown')
|
|
results.append({
|
|
'player': player,
|
|
'server': server_name,
|
|
'success': success,
|
|
'message': message
|
|
})
|
|
|
|
return jsonify({'results': results})
|
|
|
|
@app.route('/health')
|
|
def health():
|
|
"""Health check endpoint."""
|
|
return jsonify({'status': 'healthy', 'service': 'whitelist-manager'})
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=5001, debug=False)
|